🚀 引言:为什么需要函数?
在编程中,DRY (Don't Repeat Yourself) 是核心原则。如果你发现自己在复制粘贴相同的代码块,那就是需要函数的信号。
函数不仅仅是代码的封装,它是逻辑的抽象。一个好的函数应该像是一个清晰的命令,告诉读者"做什么",而不是"怎么做"。
本章我们将深入 Python 函数的核心机制,并学习如何组织大型项目的代码结构。
1. 函数定义与类型注解 (Function Definition & Type Hints)
1.1 基础语法
使用 def 关键字定义函数。现代 Python 开发中,必须配合类型提示。
# ✅ 现代标准写法
def greet(name: str, times: int = 1) -> str:
"""
生成问候语
Args:
name: 用户姓名
times: 重复次数,默认 1
Returns:
生成的问候字符串
"""
return f"Hello {name}! " * times
# 调用
message: str = greet("Alice", 2)
print(message) # Hello Alice! Hello Alice!
1.2 参数传递方式
Python 支持多种参数传递方式,灵活但需要规范。
def create_user(username: str, age: int, is_active: bool = True) -> dict[str, str | int | bool]:
return {
"username": username,
"age": age,
"is_active": is_active
}
# 1. 位置参数 (Positional Arguments) - 按顺序
user1 = create_user("Bob", 25)
# 2. 关键字参数 (Keyword Arguments) - 按名称,推荐!
user2 = create_user(age=30, username="Alice")
# 3. 混合使用 - 位置参数必须在关键字参数之前
user3 = create_user("Charlie", 35, is_active=False)
💡 最佳实践:调用函数时,尽量使用关键字参数。这样代码可读性更高,且不受参数顺序变化的影响。
# ❌ 可读性差 connect("localhost", 8080, True, 30)
✅ 可读性强
connect(host="localhost", port=8080, use_ssl=True, timeout=30) ```
2. 高级参数:*args 与 **kwargs
当参数数量不确定时,使用可变参数。
2.1 *args (可变位置参数)
收集多余的位置参数到一个元组 (tuple) 中。
def sum_all(*numbers: int) -> int:
"""计算任意数量数字的和"""
return sum(numbers)
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
2.2 **kwargs (可变关键字参数)
收集多余的关键字参数到一个字典 (dict) 中。
def print_config(**config: str) -> None:
"""打印任意配置项"""
for key, value in config.items():
print(f"{key}: {value}")
print_config(host="localhost", port="8080", debug="true")
# 输出:
# host: localhost
# port: 8080
# debug: true
2.3 强制关键字参数 (Keyword-Only Arguments)
在 *args 之后定义的参数,必须使用关键字传递。这是 Python 3 的优秀特性。
def create_order(item: str, quantity: int, *, discount: float = 0.0) -> float:
"""
创建订单
discount 必须使用关键字传递:create_order("apple", 5, discount=0.1)
"""
price = 10.0 * quantity * (1 - discount)
return price
# ✅ 正确
create_order("apple", 5, discount=0.1)
# ❌ 报错:TypeError: create_order() takes 2 positional arguments but 3 were given
# create_order("apple", 5, 0.1)
3. 作用域规则 (Scope Rules)
Python 遵循 LEGB 规则 查找变量:
Local (局部):函数内部
Enclosing (嵌套):外层函数内部
Global (全局):模块级别
Built-in (内置):Python 内置名称
3.1 局部与全局
count: int = 0 # 全局变量
def increment() -> None:
# count = count + 1 # ❌ 报错:UnboundLocalError
# 如果在函数内赋值,Python 认为它是局部变量,但读取时还没赋值
global count # ✅ 声明使用全局变量
count += 1
increment()
print(count) # 1
⚠️ 避坑指南:尽量避免使用
global。它会让代码状态难以追踪。推荐通过返回值或类属性来管理状态。
3.2 嵌套作用域 (nonlocal)
用于在内层函数修改外层函数的变量。
def outer() -> callable:
count: int = 0
def inner() -> int:
nonlocal count # ✅ 声明使用外层变量
count += 1
return count
return inner
counter = outer()
print(counter()) # 1
print(counter()) # 2
4. 模块与包 (Modules & Packages)
4.1 模块导入
将代码分割到多个文件中,便于维护。
# 方式 1:导入整个模块
import math
print(math.sqrt(16))
# 方式 2:导入特定对象 (推荐,命名空间更清晰)
from math import sqrt, pi
print(sqrt(16))
# 方式 3:导入并重命名 (避免冲突)
from numpy import array as np_array
4.2 __name__ == "__main__" 守卫
这是 Python 脚本的入口点标识。
# script.py
def main() -> None:
print("程序正在运行...")
# 只有直接运行该文件时才会执行,被 import 时不会执行
if __name__ == "__main__":
main()
为什么需要它?
防止副作用:当你的模块被其他文件
import时,不会自动执行测试代码或启动服务。单元测试友好:测试框架导入模块时不会触发主逻辑。
代码复用:该文件既可以是脚本,也可以是库。
4.3 包结构示例
my_project/
├── pyproject.toml # 项目配置与依赖
├── src/
│ └── my_package/
│ ├── __init__.py # 标记这是一个包
│ ├── main.py # 入口文件
│ └── utils.py # 工具函数
└── tests/ # 测试文件
5. 现代包管理 (Package Management)
5.1 为什么需要虚拟环境?
不同项目可能需要不同版本的库。虚拟环境可以隔离依赖,避免冲突。
5.2 工具选择:uv vs pip
pip: Python 官方默认工具,稳定但速度较慢。
uv: 基于 Rust 编写的现代工具,极速,兼容 pip 接口,正在成为新标准。
5.3 常用命令 (推荐使用 uv)
💡 建议:新项目直接使用 uv。它会自动管理虚拟环境,无需手动激活。
# 初始化新项目
uv init my_project
cd my_project
# 添加依赖
uv add requests
# 运行代码
uv run src/main.py
6. 最佳实践与避坑指南
6.1 函数设计原则
单一职责:一个函数只做一件事。
短小精悍:函数长度尽量控制在 50 行以内。
命名清晰:动词 + 名词 (如
get_user,calculate_total)。避免副作用:尽量使用纯函数 (输入相同,输出始终相同,不修改外部状态)。
6.2 可变默认参数陷阱 (经典面试题)
❌ 错误写法:默认参数是可变对象 (列表、字典)。
def add_item(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items
# 第一次调用
print(add_item("apple")) # ['apple']
# 第二次调用 (意外!列表保留了上次的状态)
print(add_item("banana")) # ['apple', 'banana']
✅ 正确写法:使用 None 作为默认值。
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
6.3 文档字符串 (Docstring)
使用三引号编写函数文档,遵循 Google 或 NumPy 风格。
def connect(host: str, port: int) -> bool:
"""连接到服务器
Args:
host: 服务器地址
port: 端口号
Returns:
连接是否成功
Raises:
ConnectionError: 当连接失败时
"""
...
7. 本章实战练习
练习 1:带类型检查的计算器
编写一个函数 calculate,接收两个数字和一个运算符 (+, -, *, /)。
要求:添加完整的类型提示。
要求:处理除零错误。
要求:使用字典映射运算符,避免冗长的
if-elif。
练习 2:模块化重构
将练习 1 的代码拆分:
calculator.py: 包含calculate函数。main.py: 包含if __name__ == "__main__"块,调用calculator并打印结果。
练习 3:配置管理器
编写一个函数 load_config,使用 **kwargs 接收配置项。
要求:返回一个字典。
要求:如果传入了
debug参数,打印所有配置。要求:
timeout参数必须是关键字参数。
📝 本章总结
🔑 核心心智
"函数是代码的积木。好的积木接口清晰 (类型提示)、功能单一 (单一职责)、且不会意外污染周围环境 (无副作用)。"
🚀 下一步
在下一章 高级编程特性 中,我们将探索 Python 更强大的一面:
装饰器 (Decorator):如何在不修改原代码的情况下增强函数功能?
生成器 (Generator):如何用
yield处理海量数据而不内存溢出?上下文管理器:如何用
with优雅地管理资源?
💡 学习建议:
从今天开始,所有新函数都必须写类型提示。
尝试使用
uv初始化你的下一个练习项目。遇到“可变默认参数”问题,立刻条件反射使用
None替代。
优雅、明确、简单——这才是 Python 之道。