Python 的函数不只是“把几行代码包起来”。函数本身也是对象,可以被传递、返回和装饰;for、生成器和 with 看似不同,背后也都建立在明确的协议之上。
本文以 CPython 3.14 为基线,讲清参数模型、作用域、闭包、装饰器、迭代器、生成器和上下文管理器。理解这些机制后,很多“Python 魔法”会变成普通的方法调用。
1. 函数是对象
1
2
3
4
5
6
7
| def add(left: int, right: int) -> int:
"""返回两个整数之和。"""
return left + right
operation = add
print(operation(2, 3)) # 5
|
名称 add 绑定到一个函数对象,因此它可以像其他对象一样保存到容器、传给另一个函数或作为返回值。
类型标注不会自动阻止错误调用:
1
| print(add("a", "b")) # 运行时得到 'ab'
|
标注主要服务于静态分析、编辑器和读者。真正的运行时约束仍来自函数体执行的操作;类型系统会在第七篇详细讨论。
2. Python 的参数模型
函数定义可以明确哪些参数只能按位置传入,哪些只能按关键字传入:
1
2
3
4
5
6
7
8
9
| def connect(
host: str,
port: int = 5432,
/,
*,
timeout: float = 5.0,
use_tls: bool = True,
) -> str:
return f"{host}:{port}, timeout={timeout}, tls={use_tls}"
|
/ 之前是仅限位置参数;* 之后是仅限关键字参数;- 中间区域可按位置或关键字传入。
调用方式:
1
| connect("db.example.com", 5432, timeout=2.0)
|
仅限关键字参数能让布尔值和多个同类型参数更易读,也为以后添加可选参数留下空间。
可变参数收集额外实参:
1
2
3
4
5
6
7
| def summarize(*values: float, precision: int = 2) -> str:
average = sum(values) / len(values)
return f"{average:.{precision}f}"
options = {"precision": 3}
print(summarize(1.0, 2.0, 3.0, **options))
|
*values 得到元组,**kwargs 得到字典。它们适合转发参数和开放扩展点,但不应代替清晰的接口;业务函数如果所有参数都藏在 **kwargs 里,类型检查和调用文档都会变差。
3. 传参是对象共享,不是按引用传变量
调用函数时,形参名称绑定到实参对象。函数可以修改传入的可变对象,但在函数内重新绑定形参不会改变调用者的名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def append_item(items: list[int]) -> None:
items.append(3)
def replace_list(items: list[int]) -> None:
items = [99]
values = [1, 2]
append_item(values)
assert values == [1, 2, 3]
replace_list(values)
assert values == [1, 2, 3]
|
与其争论“值传递还是引用传递”,更准确的说法是:实参对象被绑定到新的局部名称。接口设计时还应明确函数是否会修改传入对象。
4. 作用域与闭包
名称查找常用 LEGB 概括:Local、Enclosing、Global、Builtins,即局部、外层函数、模块全局和内置作用域。
1
2
3
4
5
6
7
8
9
10
11
12
| from collections.abc import Callable
def make_counter() -> Callable[[], int]:
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return increment
|
这里 increment() 闭包保留了外层的 count。nonlocal 表示重新绑定最近的外层函数作用域名称;global 则绑定模块全局名称。大量依赖可变全局状态会让测试和并发控制困难,通常应把状态封装到对象或显式依赖中。
闭包捕获的是名称,不是定义时的值。循环中创建函数要特别留意晚绑定:
1
2
3
4
5
6
7
| # 错误示例:三个函数最终都读取同一个 i
bad = [lambda: i for i in range(3)]
assert [func() for func in bad] == [2, 2, 2]
# 默认参数在定义时求值,可用于冻结当前值
good = [lambda i=i: i for i in range(3)]
assert [func() for func in good] == [0, 1, 2]
|
这里利用默认参数是有意保存快照,与无意共享可变默认值是两种不同场景。
5. 高阶函数与装饰器
接收或返回函数的函数称为高阶函数:
1
2
3
4
5
6
7
8
| from collections.abc import Callable
def apply_twice(operation: Callable[[int], int], value: int) -> int:
return operation(operation(value))
assert apply_twice(lambda x: x + 1, 10) == 12
|
装饰器是在函数定义后替换绑定对象的语法糖:
1
2
3
| @trace
def calculate() -> int:
return 42
|
大致等价于:
1
2
3
4
5
| def calculate() -> int:
return 42
calculate = trace(calculate)
|
一个保留元数据的计时装饰器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from collections.abc import Callable
from functools import wraps
from time import perf_counter
from typing import Any
def timed(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
started = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - started
print(f"{func.__name__}: {elapsed:.6f}s")
return wrapper
|
functools.wraps 会复制名称、文档等元数据,并通过 __wrapped__ 保留原函数链。更精确地保留参数类型需要 ParamSpec,将在类型系统一篇介绍。
装饰器在函数定义执行时应用,不是每次调用时重新创建。带参数的装饰器则是“返回装饰器的函数”,不要把它和被装饰函数的调用参数混在一起。
6. 可迭代对象与迭代器
for 的核心不是下标,而是迭代协议:
- 对目标调用
iter(obj) 获得迭代器; - 反复调用
next(iterator); - 捕获
StopIteration 后正常结束。
1
2
3
4
5
6
| values = [10, 20, 30]
iterator = iter(values)
print(next(iterator)) # 10
print(next(iterator)) # 20
print(next(iterator)) # 30
|
列表是可重复迭代的容器,每次 iter(values) 可产生新迭代器;迭代器通常是一次性的,并保存当前进度。
自定义倒计时迭代器:
1
2
3
4
5
6
7
8
9
10
11
12
13
| class Countdown:
def __init__(self, start: int) -> None:
self.current = start
def __iter__(self) -> "Countdown":
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
|
不过大多数顺序生成任务使用生成器更简单。
7. 生成器:把状态机交给语言
函数体中出现 yield 时,调用它会返回生成器对象,而不是立即执行到结束:
1
2
3
4
5
6
7
8
9
10
11
12
| from collections.abc import Iterator
def countdown(start: int) -> Iterator[int]:
current = start
while current > 0:
yield current
current -= 1
for value in countdown(3):
print(value)
|
每次 next() 会从上次 yield 后继续,局部状态自动保留。生成器适合流式读取、管道处理和大数据集,但也有边界:
- 通常只能消费一次;
- 错误可能推迟到迭代时才发生;
- 打开的外部资源必须明确关闭;
- 惰性管道若从不消费,就不会执行其中逻辑。
yield from 可以委托另一个可迭代对象:
1
2
3
| def flatten(groups: list[list[int]]) -> Iterator[int]:
for group in groups:
yield from group
|
它不等于任意层级递归展开,只是把当前子迭代器的值依次转发。
8. 上下文管理协议
with 用于表达“进入—执行—退出”的资源生命周期:
1
2
| with open("data.txt", encoding="utf-8") as file:
content = file.read()
|
对象通过 __enter__() 和 __exit__() 实现同步上下文管理协议。即使代码块抛出异常,__exit__() 仍会执行;它是否抑制异常取决于返回值。
简单场景可使用 contextlib.contextmanager:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from collections.abc import Iterator
from contextlib import contextmanager
from time import perf_counter
@contextmanager
def measure(name: str) -> Iterator[None]:
started = perf_counter()
try:
yield
finally:
elapsed = perf_counter() - started
print(f"{name}: {elapsed:.6f}s")
with measure("load"):
sum(range(100_000))
|
yield 前是进入逻辑,yield 后是退出逻辑。清理必须放在 finally 中,否则代码块抛异常时可能跳过清理。
9. 组合一个惰性处理管道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from collections.abc import Iterable, Iterator
def parse_numbers(lines: Iterable[str]) -> Iterator[float]:
for line_number, line in enumerate(lines, start=1):
text = line.strip()
if not text or text.startswith("#"):
continue
try:
yield float(text)
except ValueError as error:
raise ValueError(f"第 {line_number} 行不是数字:{text!r}") from error
with open("measurements.txt", encoding="utf-8") as file:
values = parse_numbers(file)
total = sum(values) # 实际项目计算均值还需同时记录数量
|
这段管道不会把整个文件读入内存,错误也保留了原始异常链。注意生成器消费发生在 with 内部;如果把 values 带出代码块后再迭代,文件已经关闭。
10. 小结
- 参数是新的局部名称绑定;修改共享可变对象与重新绑定形参是两回事。
/ 和 * 能把位置参数、关键字参数的接口边界写清楚。- 闭包捕获名称,循环创建闭包时要理解晚绑定。
- 装饰器是重新绑定函数对象,编写包装器时使用
functools.wraps。 for 建立在迭代协议上,生成器用 yield 保存迭代状态。with 建立在上下文管理协议上,清理逻辑必须覆盖异常路径。
下一篇将进入类、继承、组合、特殊方法和 dataclass,把对象模型用于领域建模。
参考资料