Python迭代器、生成器与装饰器
迭代器、生成器和装饰器经常被归入“Python 进阶语法”,但它们并不是三个互不相关的技巧:
- 迭代器定义了一种按需获取数据的统一协议;
- 生成器用“可暂停的函数”简化了迭代器的实现;
- 装饰器利用函数是一等对象这一特性,在不修改核心逻辑的前提下包装行为。
它们共同体现了 Python 的几个核心设计思想:协议优先、惰性计算、函数是一等对象,以及用语法糖隐藏重复的结构代码。
- 迭代器:把“如何遍历”抽象为协议
1.1 为什么需要迭代器
列表当然可以通过下标访问:
numbers = [10, 20, 30]
index = 0
while index < len(numbers):
print(numbers[index])
index += 1
但并不是所有数据都适合用下标表示:
set没有稳定的整数下标;dict更自然的遍历单位是键;- 文件可能很大,不应一次性读入内存;
- 网络流的数据可能尚未全部到达;
- 无限序列根本不存在“最后一个下标”。
如果 for 循环只理解“长度 + 下标”,每一种容器都要适配这套模型。Python 选择了更通用的做法:只要对象能持续返回“下一个值”,并能表示“已经结束”,它就可以被遍历。
这就是迭代器协议(iterator protocol)。
1.2 三个容易混淆的概念
可迭代对象(iterable)
能够交给 iter() 取得迭代器的对象称为可迭代对象。常见例子包括:
list
tuple
str
dict
set
range
从协议角度看,一个对象通常满足以下任一条件就可以被迭代:
- 实现
__iter__(),并返回一个迭代器; - 作为兼容旧式序列协议的后备方案,实现从
0开始的__getitem__(),越界时抛出IndexError。
日常自定义类型应优先实现 __iter__(),不要依赖旧式后备协议。
迭代器(iterator)
迭代器是表示一次具体遍历过程的对象。它必须实现:
iterator.__iter__() # 返回迭代器自身
iterator.__next__() # 返回下一个元素;耗尽时抛出 StopIteration
因此可以粗略地写成:
可迭代对象 --iter()--> 迭代器 --next()--> 下一个值
└--> StopIteration(结束)
容器(container)
容器强调“是否包含某个元素”,通常实现 __contains__(),可使用 in 判断。容器与可迭代对象高度重合,但概念并不相同:一个概念描述成员测试,另一个描述遍历能力。
1.3 for循环实际上做了什么
下面的代码:
for item in [10, 20, 30]:
print(item)
在语义上近似于:
items = [10, 20, 30]
iterator = iter(items)
while True:
try:
item = next(iterator)
except StopIteration:
break
else:
print(item)
这里有两个关键的内置函数:
iter(obj)通常会调用obj.__iter__();next(iterator)会调用iterator.__next__()。
StopIteration 不是程序错误,而是迭代协议规定的正常结束信号。for 循环会在内部捕获它,因此一般看不到这个异常。
1.4 可迭代对象不一定是迭代器
numbers = [10, 20, 30]
print(iter(numbers) is numbers) # False
iterator = iter(numbers)
print(iter(iterator) is iterator) # True
列表是可迭代对象,却不是迭代器;列表迭代器才是迭代器。
这种分离很重要。一个列表可以同时发生多次互不干扰的遍历:
numbers = [10, 20, 30]
first = iter(numbers)
second = iter(numbers)
print(next(first)) # 10
print(next(first)) # 20
print(next(second)) # 10
列表负责保存数据,每个迭代器分别保存自己的遍历位置。
1.5 手写一个迭代器
下面实现一个产生 [start, stop) 范围内整数的迭代器:
class NumberIterator:
def __init__(self, start: int, stop: int) -> None:
self.current = start
self.stop = stop
def __iter__(self) -> "NumberIterator":
return self
def __next__(self) -> int:
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
iterator = NumberIterator(2, 5)
print(next(iterator)) # 2
print(next(iterator)) # 3
print(next(iterator)) # 4
# next(iterator) # 抛出 StopIteration
这个类同时保存了两类状态:
- 固定边界
stop; - 随迭代改变的位置
current。
迭代器通常是有状态、一次性、向前消费的对象。耗尽之后,再次遍历它不会自动回到开头。
1.6 更合理的设计:把“数据”与“游标”分开
如果将某个业务对象自身设计成迭代器,它往往只能遍历一次,也难以支持嵌套遍历。更稳妥的方式是让业务对象作为可迭代对象,每次调用 __iter__() 都创建一个新迭代器:
class NumberRange:
def __init__(self, start: int, stop: int) -> None:
self.start = start
self.stop = stop
def __iter__(self) -> NumberIterator:
return NumberIterator(self.start, self.stop)
numbers = NumberRange(1, 4)
print(list(numbers)) # [1, 2, 3]
print(list(numbers)) # [1, 2, 3],可以重新开始
职责现在变得清晰:
NumberRange描述可遍历的数据范围;NumberIterator描述某一次遍历当前走到了哪里。
1.7 用iter()生成迭代器
由于生成器本身就是一种迭代器,自定义可迭代对象通常不必再写单独的迭代器类:
class NumberRange:
def __init__(self, start: int, stop: int) -> None:
self.start = start
self.stop = stop
def __iter__(self):
current = self.start
while current < self.stop:
yield current
current += 1
这正是生成器与迭代器之间的连接点,后文会详细解释 yield。
1.8 iter() 的第二种形式:哨兵迭代器
iter() 还有一个不太常见但很实用的重载:
iter(callable, sentinel)
它会反复调用无参数的 callable,直到返回值等于 sentinel,但不会产生哨兵值本身。
from functools import partial
with open("data.bin", "rb") as file:
# 每次读取 4096 字节,读到 b"" 时结束
for block in iter(partial(file.read, 4096), b""):
process(block)
这可以避免手写 while True 和结束判断,非常适合分块读取。
1.9 迭代器的惰性与内存优势
考虑一百万个整数的平方:
squares_list = [x * x for x in range(1_000_000)]
squares_iterator = map(lambda x: x * x, range(1_000_000))
列表推导式立即创建并保存一百万个结果;map 返回迭代器,在调用 next() 时才计算一个结果。
惰性求值带来三项重要能力:
- 不保存全部中间结果,降低峰值内存;
- 下游提前停止时,上游不再做无用计算;
- 可以表示无限序列。
但“惰性”不等于“永远更快”。迭代器逐项执行 Python 代码可能增加调用开销,而且不能像列表那样随机访问。选择时应关注数据规模、访问方式和是否需要重复遍历。
1.10 迭代器常见陷阱
陷阱一:迭代器只能消费一次
iterator = iter([1, 2, 3])
print(list(iterator)) # [1, 2, 3]
print(list(iterator)) # []
如果确实需要重复使用,可以保存原始可迭代对象并重新 iter(),或者在数据量可接受时使用 list() 物化。
陷阱二:成员测试也可能消费迭代器
iterator = iter([1, 2, 3, 4])
print(3 in iterator) # True,1、2、3 已经被消费
print(list(iterator)) # [4]
陷阱三:并行遍历时共享了同一个迭代器
iterator = iter([1, 2, 3, 4])
pairs = zip(iterator, iterator)
print(list(pairs)) # [(1, 2), (3, 4)]
这有时是巧妙用法,有时却是难以发现的错误。关键是要意识到两个位置正在推进同一个状态。
陷阱四:遍历时修改容器
遍历列表时增删元素会改变后续下标,遍历字典时改变其大小通常会触发 RuntimeError。常见解决方案是遍历副本或先收集要修改的项:
users = {"alice": True, "bob": False}
for name, active in list(users.items()):
if not active:
del users[name]
1.11 常用迭代工具
Python 标准库 itertools 提供了大量惰性组合工具:
from itertools import chain, count, islice
print(list(chain([1, 2], [3, 4]))) # [1, 2, 3, 4]
print(list(islice(count(10, 2), 5))) # [10, 12, 14, 16, 18]
常用函数包括:
chain:连接多个可迭代对象;islice:对迭代器做惰性切片;count、cycle、repeat:构造无限迭代器;takewhile、dropwhile:按条件截取;groupby:对相邻且键相同的元素分组;tee:从一个输入创建多个逻辑迭代器。
要注意 tee 需要缓存各副本进度差之间的数据。如果其中一个副本远远落后,缓存可能持续增长。
- 生成器:能够暂停和恢复的函数
- 生成器是什么
只要函数体中包含 yield,它就是生成器函数(generator function)。调用生成器函数不会立即执行函数体,而是创建一个生成器对象(generator object):
def simple_generator():
print("开始执行")
yield 10
print("继续执行")
yield 20
print("即将结束")
generator = simple_generator()
print(generator) # <generator object ...>
print("函数体尚未执行")
print(next(generator)) # 打印“开始执行”,返回 10
print(next(generator)) # 打印“继续执行”,返回 20
# next(generator) # 打印“即将结束”,抛出 StopIteration
必须区分:
simple_generator是生成器函数;simple_generator()返回的generator是生成器对象;- 生成器对象实现了迭代器协议,所以它也是迭代器。
关系可以写成:
生成器函数 --调用--> 生成器对象
生成器对象 ⊂ 迭代器 ⊂ 可迭代对象
并非所有迭代器都是生成器,例如 list_iterator 就不是生成器。
yield与return的本质区别
普通函数执行 return 时:
- 计算返回值;
- 销毁本次调用的活动执行上下文;
- 将控制权永久交还给调用方。
生成器执行 yield 时:
- 产生一个值;
- 暂停执行;
- 保存指令位置、局部变量和异常处理状态;
- 下一次
next()时从暂停点之后继续。
因此,生成器更像一台有多个暂停点的状态机。
def countdown(start: int):
current = start
while current > 0:
yield current
current -= 1
print(list(countdown(3))) # [3, 2, 1]
编写者只需描述“产生一个值,然后从这里继续”,解释器会替我们保存和恢复 current 与执行位置。
- 生成器如何实现状态机
生成器暂停时,其执行帧仍保存着:
- 当前执行到的字节码位置;
- 局部变量;
- 求值栈;
- 当前异常处理上下文;
- 正在等待恢复还是已经结束的状态。
可以检查生成器状态:
from inspect import getgeneratorstate
def demo():
yield 1
generator = demo()
print(getgeneratorstate(generator)) # GEN_CREATED
next(generator)
print(getgeneratorstate(generator)) # GEN_SUSPENDED
try:
next(generator)
except StopIteration:
pass
print(getgeneratorstate(generator)) # GEN_CLOSED
常见状态有:
GEN_CREATED:已经创建,尚未开始;GEN_RUNNING:正在执行;GEN_SUSPENDED:停在yield;GEN_CLOSED:正常结束、异常退出或被关闭。
从实现角度看,生成器不是“后台同时运行的线程”。调用方和生成器仍在同一个线程中交替执行;每次切换都发生在明确的恢复或暂停位置。
- 生成器结束与
StopIteration.value
生成器可以使用 return value 结束。这个值不会作为普通迭代元素产生,而是放在 StopIteration.value 中:
def generator_with_result():
yield 1
return "done"
generator = generator_with_result()
print(next(generator)) # 1
try:
next(generator)
except StopIteration as error:
print(error.value) # done
普通 for 循环会丢弃这个最终返回值。它主要用于生成器委托,也就是后文的 yield from。
不要在生成器内部手动抛出 StopIteration 来结束生成器。现代 Python 会将意外泄漏的 StopIteration 转换为 RuntimeError。正常结束应使用 return 或让函数自然执行完毕。
- 生成器表达式
列表推导式会立即构造列表:
squares = [x * x for x in range(10)]
将方括号换为圆括号就得到生成器表达式:
squares = (x * x for x in range(10))
两者的差别不是只有括号:
| 特性 | 列表推导式 | 生成器表达式 |
| 求值时机 | 立即计算全部结果 | 按需逐项计算 |
| 结果类型 | list | 生成器对象 |
| 重复遍历 | 可以 | 耗尽后不可以 |
| 随机访问 | 支持下标 | 不支持 |
| 内存 | 与结果数量相关 | 通常近似固定 |
生成器表达式作为函数的唯一位置参数时,可以省略额外括号:
total = sum(x * x for x in range(1_000_000))
- 惰性流水线
生成器特别适合将大数据处理拆成流水线:
from collections.abc import Iterable, Iterator
def read_nonempty_lines(path: str) -> Iterator[str]:
with open(path, encoding="utf-8") as file:
for line in file:
stripped = line.strip()
if stripped:
yield stripped
def parse_integers(lines: Iterable[str]) -> Iterator[int]:
for line in lines:
yield int(line)
def only_even(numbers: Iterable[int]) -> Iterator[int]:
for number in numbers:
if number % 2 == 0:
yield number
result = sum(only_even(parse_integers(read_nonempty_lines("numbers.txt"))))
数据沿着 读取 → 解析 → 筛选 → 求和 逐项流动。只要每一层保持惰性,文件大小就不直接决定中间结果的内存占用。
还应注意资源生命周期:上例中的文件会一直保持打开,直到生成器耗尽、关闭或被回收。若调用者提前停止,最好显式关闭生成器,或者在更外层管理文件上下文。
2.7yield from:委托给子迭代器
如果只想逐项转发另一个可迭代对象,可以写:
def flatten(groups):
for group in groups:
for item in group:
yield item
使用 yield from 更简洁:
def flatten(groups):
for group in groups:
yield from group
print(list(flatten([[1, 2], [3], [4, 5]])))
# [1, 2, 3, 4, 5]
但 yield from 不只是循环的缩写。它会在调用方与子生成器之间建立完整的委托通道,包括:
- 转发子生成器产生的值;
- 转发
send()发送的值; - 转发
throw()注入的异常; - 处理
close(); - 接收子生成器
return所携带的最终结果。
def child():
yield 1
yield 2
return 3
def parent():
child_result = yield from child()
yield child_result * 10
print(list(parent())) # [1, 2, 30]
2.8 双向通信:`send()`
yield 既可以向外产生值,也可以作为表达式接收外部发送进来的值:
def running_average():
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
average = running_average()
print(next(average)) # None,预激活到第一个 yield
print(average.send(10)) # 10.0
print(average.send(20)) # 15.0
print(average.send(30)) # 20.0
average.close()
执行 generator.send(value) 时,value 会成为当前暂停处 yield 表达式的结果,然后生成器继续运行,直到下一个 yield。
刚创建的生成器尚未停在 yield 表达式上,因此不能直接发送非 None 值。通常先调用:
next(generator)
# 等价于 generator.send(None)
这种生成器有时称为协程式生成器。现代异步编程通常应使用 async def、await 和 asyncio;理解 send() 仍有助于理解 Python 协程机制的演进。
2.9`throw()` 与 `close()`
throw() 可以在生成器暂停的位置注入异常:
def resilient_generator():
while True:
try:
value = yield
print("收到:", value)
except ValueError as error:
print("处理错误:", error)
generator = resilient_generator()
next(generator)
generator.send("hello")
generator.throw(ValueError("bad value"))
generator.close()
close() 会在暂停处注入 GeneratorExit。生成器可以用 finally 释放资源,但不应捕获 GeneratorExit 后继续产生值,否则会触发 RuntimeError:
def managed_resource():
print("获取资源")
try:
yield "resource"
finally:
print("释放资源")
generator = managed_resource()
print(next(generator))
generator.close() # 保证执行 finally
2.10 无限序列
惰性求值使生成器可以表示无限序列:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
无限生成器必须由下游限制:
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
不要直接执行 list(fibonacci()),因为它永远不会结束,并会持续占用内存。
2.11 类型标注
只向外产生值、不接收值也不返回最终结果时,通常使用 Iterator[T]:
from collections.abc import Iterator
def countdown(start: int) -> Iterator[int]:
while start > 0:
yield start
start -= 1
如果需要完整表达“产生类型、接收类型、返回类型”,可以使用:
from collections.abc import Generator
def worker() -> Generator[int, str, bool]:
message = yield 1 # yield int,send str
return message == "ok" # 最终返回 bool
Generator[YieldType, SendType, ReturnType] 的三个参数不要颠倒。
2.12 何时使用生成器,何时使用列表
优先考虑生成器的情形:
- 数据量大,且可以逐项处理;
- 只需要遍历一次;
- 计算成本高,消费者可能提前停止;
- 输入是流、文件或无限序列;
- 多个处理阶段可以组成惰性流水线。
优先考虑列表的情形:
- 需要重复遍历;
- 需要下标、切片或倒序访问;
- 结果规模小,直接保存更清晰;
- 需要在多个位置共享一份稳定快照;
- 希望错误在构造阶段立即暴露,而不是推迟到消费阶段。
生成器的错误也是惰性的。调用生成器函数时可能一切正常,真正迭代到有问题的数据时才抛异常。这会影响调试位置和资源管理。
三、装饰器:在定义阶段包装对象
3.1 前提:函数是一等对象
在 Python 中,函数可以像整数和字符串一样:
- 赋值给变量;
- 作为参数传入另一个函数;
- 作为返回值;
- 存入列表或字典;
- 在运行时动态创建。
def greet(name: str) -> str:
return f"Hello, {name}"
alias = greet
print(alias("Alice"))
def execute(function, argument):
return function(argument)
print(execute(greet, "Bob"))
装饰器正是接收一个对象并返回替代对象的可调用对象。最常见的是“函数装饰器”,也存在类装饰器。
3.2`@decorator` 是什么语法糖
@decorator
def target():
pass
基本等价于:
def target():
pass
target = decorator(target)
关键点是:装饰器通常在包含这段定义的模块或类体执行时应用,而不是每次调用 target() 时重新应用。
装饰后,名字 target 已经被重新绑定到 decorator(target) 的返回值。返回值一般是包装函数,也可以是任何其他对象。
3.3 最小装饰器
def trace(function):
def wrapper(*args, **kwargs):
print(f"调用 {function.__name__}")
result = function(*args, **kwargs)
print(f"{function.__name__} 返回 {result!r}")
return result
return wrapper
@trace
def add(left: int, right: int) -> int:
return left + right
print(add(2, 3))
闭包让 wrapper 在 trace 已经返回之后仍能记住原函数 function。调用链是:
add(2, 3)
↓ 实际调用
wrapper(2, 3)
↓ 内部调用
原始 add(2, 3)
*args, **kwargs 让包装器可以转发任意位置参数与关键字参数;返回原函数结果则保持调用语义。
3.4 为什么必须使用`functools.wraps`
上面的装饰器会破坏元数据:
print(add.__name__) # wrapper,而不是 add
这会影响文档生成、调试、日志、类型检查辅助和依赖函数签名的框架。正确写法是:
from functools import wraps
def trace(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"调用 {function.__name__}")
return function(*args, **kwargs)
return wrapper
wraps(function) 会复制常用元数据,并设置 wrapper.__wrapped__ = function。inspect.signature() 等工具可以沿着 __wrapped__ 找到原函数签名。
3.5 带参数的装饰器
如果希望写成:
@repeat(times=3)
def greet():
print("hello")
就需要三层函数:
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar
R = TypeVar("R")
def repeat(times: int):
if times < 1:
raise ValueError("times 必须大于等于 1")
def decorator(function: Callable[..., R]) -> Callable[..., R]:
@wraps(function)
def wrapper(*args: Any, **kwargs: Any) -> R:
result: R
for _ in range(times):
result = function(*args, **kwargs)
return result
return wrapper
return decorator
三层分别承担:
repeat(times=3)接收装饰器配置;decorator(function)接收被装饰函数;wrapper(*args, **kwargs)接收真正调用时的参数。
其展开形式是:
greet = repeat(times=3)(greet)
不要把“创建装饰器的参数”和“调用被装饰函数的参数”混在一起。
3.6 多个装饰器的顺序
@outer
@inner
def target():
pass
定义阶段从下往上组合:
target = outer(inner(target))
调用时则由最外层先收到调用:
进入 outer wrapper
进入 inner wrapper
调用原函数
离开 inner wrapper
离开 outer wrapper
因此认证、事务、缓存、重试等装饰器的顺序可能直接改变行为,不能随意交换。
3.7 实用示例一:记录耗时
from functools import wraps
from time import perf_counter
def timed(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f"{function.__qualname__} 耗时 {elapsed:.6f}s")
return wrapper
@timed
def compute() -> int:
return sum(range(1_000_000))
将日志放在 finally 中,即使原函数抛异常也会记录耗时。
在生产项目中,通常应使用 logging 而不是 print,并明确是否允许日志中出现敏感参数。
3.8 实用示例二:缓存
可以手写一个简单缓存装饰器:
from functools import wraps
def memoize(function):
cache = {}
@wraps(function)
def wrapper(*args):
if args not in cache:
cache[args] = function(*args)
return cache[args]
return wrapper
但工程中更推荐标准库实现:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(number: int) -> int:
if number < 2:
return number
return fibonacci(number - 1) + fibonacci(number - 2)
缓存键要求参数可哈希。缓存还会延长参数与结果对象的生命周期,并可能返回同一个可变结果对象,所以不是所有函数都适合缓存。带外部副作用、依赖时间或全局状态的函数尤其需要谨慎。
3.9 实用示例三:重试
from functools import wraps
from time import sleep
def retry(*, attempts: int = 3, delay: float = 0.0,
exceptions: tuple[type[Exception], ...] = (Exception,)):
if attempts < 1:
raise ValueError("attempts 必须大于等于 1")
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, attempts + 1):
try:
return function(*args, **kwargs)
except exceptions as error:
last_error = error
if attempt < attempts:
sleep(delay)
assert last_error is not None
raise last_error
return wrapper
return decorator
真正的重试还应考虑指数退避、随机抖动、最大总时长、可观测性与幂等性。对“扣款”“创建订单”等非幂等操作直接重试可能造成重复副作用。
3.10 类作为装饰器
只要对象可调用,就可以作为装饰器。实现 __call__() 的类特别适合保存跨调用状态:
from functools import update_wrapper
class CountCalls:
def __init__(self, function):
self.function = function
self.count = 0
update_wrapper(self, function)
def __call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用")
return self.function(*args, **kwargs)
@CountCalls
def greet(name: str) -> str:
return f"Hello, {name}"
不过,将有状态装饰器用于实例方法时还涉及描述符绑定问题。普通函数定义在类中时实现了描述符协议,会自动把实例绑定为 self;自定义可调用对象默认未必有相同行为。需要装饰方法时,闭包式装饰器通常更省心,或者为装饰器类实现 __get__()。
3.11 装饰实例方法时发生了什么
def trace(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(function.__qualname__)
return function(*args, **kwargs)
return wrapper
class Calculator:
@trace
def add(self, left, right):
return left + right
类体执行期间,Calculator.add 被替换为 wrapper。由于 wrapper 本身仍是普通函数,它也实现描述符协议。访问 calculator.add 时,实例会自动绑定给 wrapper,随后包装器把包括 self 在内的全部参数转发给原函数。
这也是为什么通用包装器通常使用 *args, **kwargs,而不应自行猜测第一个参数是否为 self。
3.12 类装饰器
装饰器也可以接收并返回类:
def add_repr(cls):
def __repr__(self):
fields = ", ".join(
f"{name}={value!r}" for name, value in vars(self).items()
)
return f"{cls.__name__}({fields})"
cls.__repr__ = __repr__
return cls
@add_repr
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
标准库的 @dataclass 就是著名的类装饰器。它读取类上的类型标注和配置,再为类补充 __init__、__repr__、比较方法等行为。
3.13 标准库中常见的装饰器
#### `@property`
把方法包装为托管属性:
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
@property
def area(self) -> float:
return 3.1415926 * self.radius ** 2
#### `@classmethod`
访问类时绑定类对象 cls,常用于替代构造器:
class User:
def __init__(self, name: str) -> None:
self.name = name
@classmethod
def anonymous(cls):
return cls("anonymous")
#### `@staticmethod`
关闭普通方法的实例绑定,不自动传入 self 或 cls。它适合逻辑上属于该类命名空间、但不依赖实例与类状态的函数。
#### `@dataclass`
根据字段定义生成常用样板方法,是类装饰器的代表。
#### `@lru_cache` / `@cache`
保存纯函数或近似纯函数的计算结果,避免重复计算。
3.14 装饰同步函数与异步函数
普通包装函数不能正确替代异步函数的 await 行为。异步装饰器的包装层也应该使用 async def:
from functools import wraps
from time import perf_counter
def async_timed(function):
@wraps(function)
async def wrapper(*args, **kwargs):
start = perf_counter()
try:
return await function(*args, **kwargs)
finally:
print(f"耗时 {perf_counter() - start:.6f}s")
return wrapper
如果一个装饰器同时接受同步与异步函数,通常需要使用 inspect.iscoroutinefunction() 在装饰阶段选择不同包装器。
3.15 装饰生成器函数的特殊陷阱
假设直接用普通计时装饰器包装生成器函数:
@timed
def numbers():
yield 1
yield 2
调用 numbers() 只创建生成器对象,并未执行生成器主体,因此计时装饰器测到的几乎只是创建时间。真正工作发生在后续迭代期间。
如果希望测量完整消费过程,包装器自身也应是生成器,并用 yield from 包裹迭代:
from functools import wraps
from time import perf_counter
def timed_generator(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
yield from function(*args, **kwargs)
finally:
print(f"完整迭代耗时 {perf_counter() - start:.6f}s")
return wrapper
这里也揭示了本文三个主题之间的真实联系:装饰器改变调用边界,生成器把真正执行推迟到迭代边界;如果不了解两者的执行时机,监控、事务和资源管理就可能覆盖错范围。
3.16 保留类型签名
Callable[..., R] 能表达返回值,却会丢失参数的精确信息。现代类型标注可以用 ParamSpec 保留参数类型:
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def trace(function: Callable[P, R]) -> Callable[P, R]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(function.__qualname__)
return function(*args, **kwargs)
return wrapper
这样,类型检查器仍知道装饰后函数原本需要哪些参数。
3.17 装饰器的适用边界
装饰器适合横切关注点,例如:
- 日志与指标;
- 权限校验;
- 缓存;
- 重试;
- 参数验证;
- 事务边界;
- 注册路由或插件。
但装饰器不是越多越好。以下情况应谨慎:
- 包装层太多,调用链难以追踪;
- 装饰器暗中改变返回类型或异常语义;
- 依赖全局状态,测试之间互相影响;
- 装饰发生在导入期,带来昂贵或不可逆副作用;
- 多个装饰器顺序敏感,却没有明确文档;
- 为了复用几行普通逻辑而引入难读的元编程。
好的装饰器应该有清晰单一的职责,保留原函数元数据,尽量保持调用契约,并为顺序与副作用提供文档。
四、三者如何协同
4.1 关系总结
迭代器协议:规定如何逐项取得值
↑
生成器:自动实现迭代器协议,并保存暂停状态
装饰器:包装函数或类,改变名字最终绑定的对象
↓
可包装普通函数、生成器函数、异步函数或类
生成器解决的是“执行如何暂停并逐项返回”;装饰器解决的是“如何在对象外层统一增加行为”。它们关注点不同,但可以组合。
4.2 综合实例:记录数据流的数量与耗时
from collections.abc import Callable, Iterator
from functools import wraps
from time import perf_counter
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def monitor_stream(
function: Callable[P, Iterator[T]],
) -> Callable[P, Iterator[T]]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Iterator[T]:
count = 0
start = perf_counter()
try:
for item in function(*args, **kwargs):
count += 1
yield item
finally:
elapsed = perf_counter() - start
print(f"{function.__qualname__}: {count} 项,{elapsed:.6f}s")
return wrapper
@monitor_stream
def read_numbers(limit: int) -> Iterator[int]:
for number in range(limit):
yield number
for number in read_numbers(5):
print(number)
这个例子中:
read_numbers是生成器函数;- 每次调用返回生成器对象,也就是迭代器;
monitor_stream是装饰器;- 包装器也是生成器,因此监控覆盖实际迭代过程;
finally确保正常耗尽、异常退出或显式关闭时都尽量输出统计。
如果调用者创建生成器之后从不开始迭代,包装器函数体也不会启动,自然不会记录任何内容。这不是错误,而是生成器惰性执行语义的结果。
五、与 C++ 的对比
Python 与 C++ 都有迭代抽象,但两种语言的传统实现方式和成本模型明显不同。下面以现代 C++(主要指 C++20)为参照。
5.1 Python 迭代器与 C++ 迭代器
Python:通过运行时协议取“下一个值”
iterator = iter(container)
value = next(iterator)
Python 迭代器通常在对象内部保存当前位置,结束通过 StopIteration 表示。for 循环直接消费迭代器,不需要显式操作结束迭代器。
C++:通过当前位置与终点比较
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3};
for (auto it = values.begin(); it != values.end(); ++it) {
std::cout << *it << '\n';
}
}
C++ 传统迭代器更像泛化指针:
*it解引用取得元素;++it前进;it != end判断是否结束。
范围 for:
for (const auto& value : values) {
std::cout << value << '\n';
}
会由编译器展开为基于 begin() 和 end() 的循环。C++20 ranges 又把范围与算法组合提升到更接近声明式流水线的层次。
核心差异
| 维度 | Python | C++ |
| 主要抽象 | iter / next 协议 | begin / end、解引用和递增 |
| 结束方式 | StopIteration | 与 sentinel/end 比较 |
| 多趟遍历 | 取决于对象;迭代器常为单趟 | 由 iterator/range category 约束 |
| 随机访问 | 普通迭代器协议不保证 | random-access iterator 可支持 |
| 多态方式 | 运行时鸭子类型 | 模板、concepts,主要在编译期 |
| 常见开销 | 动态分派、Python 对象和解释器开销 | 常可内联、零成本抽象,但依实现而定 |
C++ 对迭代器能力划分更细,例如 input、forward、bidirectional、random access、contiguous 等类别。Python 的基本迭代器协议只承诺“能取下一个值”,更高级能力通常通过对象的其他协议单独表达。
5.2 Python 生成器与 C++20 协程
Python:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
C++20 提供 co_yield、co_await 和 co_return 等协程语言设施,但标准库并未在 C++20 中直接提供一个与 Python 生成器同样开箱即用的通用 std::generator。项目通常使用库实现,或者自行定义 promise type;更新的标准/实现可提供相应生成器设施,实际可用性应以所用编译器和标准库为准。
概念示意如下:
generator<int> fibonacci() {
int a = 0;
int b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
两者相似之处:
- 都可以暂停并保存局部状态;
- 都支持惰性地产生值;
- 都适合表达状态机和数据流;
- 都需要关注协程帧/生成器对象的生命周期。
差异主要在抽象层级:Python 为生成器提供统一且直接的语言和运行时协议;C++ 协程机制更底层,通过 promise、awaiter 等定制点允许库设计不同语义,因此更灵活,也更复杂。
5.3 Python 生成器表达式与 C++ ranges views
Python 惰性流水线:
result = sum(
number * number
for number in range(100)
if number % 2 == 0
)
C++20 ranges 风格:
#include <ranges>
auto values = std::views::iota(0, 100)
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
二者都强调惰性组合,但 Python 的生成器可以包含任意控制流、异常处理和双向通信;C++ view 更偏向对范围的可组合转换,并利用静态类型与编译器优化。
5.4 Python 装饰器与 C++ 的对应手段
C++ 没有与 Python @decorator 完全等价的通用函数装饰器语法,但可以用多种机制实现相似目标。
高阶函数 / lambda 包装
#include <iostream>
#include <utility>
template <typename F>
auto trace(F function) {
return [function = std::move(function)](auto&&... args) -> decltype(auto) {
std::cout << "before\n";
decltype(auto) result = function(
std::forward<decltype(args)>(args)...
);
std::cout << "after\n";
return result;
};
}
它与 Python 闭包装饰器相似:接收 callable,返回包装后的 callable。但在 C++ 中需要仔细处理模板推导、值类别、生命周期、重载和 void 返回类型;上面的简化示例并未覆盖所有 callable。
RAII
Python 常用装饰器管理计时或资源边界,C++ 许多同类需求更自然地使用 RAII:对象构造时获取资源,析构时释放资源。RAII 的作用域由静态块结构决定,对异常安全尤其重要。
Python 与之更直接对应的工具通常是上下文管理器:
with managed_resource() as resource:
use(resource)
因此,不应机械地把所有 Python 装饰器用法映射为 C++ callable wrapper;要根据目标选择模板、高阶函数、RAII、宏、属性或框架注册机制。
Attributes 与宏
C++ 的 [[attribute]] 为编译器提供声明信息,宏可以在预处理阶段变换源码形式。它们表面上也写在函数附近,但与 Python 装饰器的运行时“接收对象并返回对象”模型不同。
5.5 两种语言的思维差异
Python 更强调:
- 用少量协议实现广泛互操作;
- 在运行时组合行为;
- 优先表达清晰和开发效率;
- 由解释器管理生成器帧与对象生命周期。
C++ 更强调:
- 在编译期表达能力与约束;
- 控制对象布局、所有权与生命周期;
- 通过模板与内联追求零成本抽象;
- 让库作者能够深度定制协程和迭代语义。
两者并无绝对优劣。Python 的模型更统一易用,C++ 的模型更精细且可控制;选择取决于性能边界、团队经验和系统约束。
六、常见面试题与判断题
6.1`range` 是迭代器吗
不是。range 是可迭代对象,每次 iter(range_obj) 都能得到新的 range 迭代器。它还支持长度、成员测试和下标访问,但本身没有 __next__()。
6.2 生成器是迭代器吗
是。生成器对象实现 __iter__() 和 __next__(),并且 iter(generator) is generator。
6.3 迭代器一定是生成器吗
不是。任何正确实现迭代器协议的对象都是迭代器,生成器只是其中一种便捷实现。
6.4`yield` 会立即返回值吗
更准确地说,yield 会向本次 next() 或 send() 的调用方产生一个值并暂停生成器。下一次恢复时,函数从暂停点继续,而不是从头开始。
6.5 调用生成器函数时,参数校验会立即执行吗
如果校验代码位于生成器函数体内,通常不会。函数体要到第一次迭代才开始执行:
def generate(limit):
if limit < 0:
raise ValueError("limit 不能为负数")
yield from range(limit)
generator = generate(-1) # 此处通常不抛错
next(generator) # 此处才抛错
若必须在调用时立即校验,可以让普通外层函数完成校验,再返回内部生成器。
6.6 装饰器是在什么时候执行的
装饰器表达式及应用通常发生在函数定义所在的模块或类体执行期间。包装器中的调用逻辑则在调用装饰后的函数时执行。这两个阶段必须区分。
6.7 装饰器为什么经常需要闭包
包装函数需要在外层装饰器返回以后继续访问原函数和配置参数。闭包正好保存这些自由变量。
6.8 为什么装饰器要返回`wrapper`
因为装饰语法最终会把原函数名重新绑定到装饰器返回值。如果没有显式返回,返回值是 None,原函数名也会变成 None,无法再调用。
6.9`yield from` 与 `for … yield` 完全相同吗
仅在单纯转发元素的简单情况下结果相似。涉及 send()、throw()、close() 和子生成器最终返回值时,yield from 还负责完整的委托协议。
6.10 惰性求值一定更省内存吗
通常可以降低中间结果的峰值内存,但不是无条件保证。例如 itertools.tee 为进度落后的副本缓存数据,某些分组或排序操作也必须保存内容。应分析整个流水线,而不只是其中某一层。
七、实践建议清单
迭代器
- 将数据集合与一次遍历的游标状态分开;
- 牢记迭代器通常只能向前消费一次;
- 需要多次遍历时,保留可迭代源或显式物化;
- 大数据处理优先组合标准库迭代工具;
- 警惕成员测试、
zip、list等操作对迭代器的消费。
生成器
- 用
yield表达逐项产生的数据,而不是手写状态机类; - 对简单映射/筛选考虑生成器表达式;
- 用
yield from做子生成器委托; - 为文件和连接等资源设计明确的关闭路径;
- 无限生成器必须搭配明确的下游终止条件;
- 不要在生成器内部手动抛
StopIteration; - 记住函数体中的异常和副作用会推迟到迭代时发生。
装饰器
- 默认使用
functools.wraps; - 完整转发参数和返回值;
- 明确装饰发生阶段与调用发生阶段;
- 明确多个装饰器的顺序;
- 分别处理同步函数、异步函数和生成器函数的执行边界;
- 缓存与重试要考虑副作用、幂等性和资源占用;
- 使用
ParamSpec和TypeVar改善类型检查体验; - 不要让包装层暗中改变调用契约。
八、结语
理解这三个概念的关键,不是背诵语法,而是抓住执行模型:
1. 迭代器是一份协议:调用 `next()` 获取值,以 `StopIteration` 表示结束;
2. 生成器是一种可暂停执行机制:解释器替我们保存状态,并自动实现迭代器协议;
3. 装饰器是一种定义期对象变换:`@decorator` 本质上重新绑定函数或类的名字;
4. 惰性会改变执行时机:创建对象、开始执行、产生值和释放资源可能发生在不同时间;
5. 抽象不能掩盖生命周期:无论 Python 还是 C++,状态、资源、异常和所有权都是工程代码真正需要管理的部分。
当你能准确回答“对象何时创建、代码何时执行、状态保存在何处、异常向哪里传播、资源何时释放”时,就不再只是会使用迭代器、生成器和装饰器,而是真正理解了它们。