没有“迭代器装饰器”标准概念,而是用装饰器包装返回迭代器的函数,在其生成器内部对每次next()调用实现限流与重试;关键在于控制惰性求值过程中的单次取值行为,而非装饰不可调用的迭代器对象本身。

直接说结论:没有“迭代器装饰器”这个标准概念,但你可以用装饰器包装返回迭代器的函数,并在其内部对每次 next() 调用施加限流与重试逻辑。关键不是装饰迭代器对象本身(它不可调用),而是装饰生成迭代器的函数,并在迭代过程中控制行为。
下面分三块讲清楚怎么做、为什么这么设计、以及实际怎么写:
限流 + 重试要作用在“每次取值”上,而不是整个迭代器
迭代器本质是惰性数据流:iter_obj = fetch_items() 返回一个对象,你调用 next(iter_obj) 才真正触发一次数据获取(比如发一次 HTTP 请求、查一次数据库)。所以:
- 限流的目标是控制
next()的调用频率(如每秒最多 5 次); - 重试的目标是某次
next()失败后自动再试几次,而非整个for item in iter_obj:循环重来; - 因此,不能给
iter_obj加装饰器(它不是函数),而应装饰fetch_items这类返回迭代器的工厂函数,并在其返回的迭代器中封装逻辑。
用生成器函数 + 装饰器实现带限流和重试的迭代器
核心思路:装饰器不改原函数签名,而是返回一个新生成器函数,该函数在 yield 前做限流等待、捕获异常并重试。
import time
import functools
from itertools import count
def rate_limited_retry(
max_attempts=3,
delay=1.0,
backoff_factor=2.0,
rate_limit_per_second=5
):
"""装饰器:为返回迭代器的函数添加每次 next() 的限流与重试能力"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 创建原始迭代器(不立即执行)
base_iter = func(*args, **kwargs)
# 记录上次 yield 时间,用于限流
last_yield_time = 0.0
for attempt_count in count(1):
try:
# 限流:计算需等待时间
now = time.time()
elapsed = now - last_yield_time
if elapsed 0:
time.sleep(sleep_time)
last_yield_time = time.time()
# 尝试获取下一个值(可能抛异常)
item = next(base_iter)
yield item
except StopIteration:
break
except Exception as e:
if attempt_count >= max_attempts:
raise e
# 指数退避等待
wait = delay * (backoff_factor ** (attempt_count - 1))
time.sleep(wait)
return wrapper
return decorator
使用示例:
@rate_limited_retry(max_attempts=2, delay=0.5, rate_limit_per_second=2)
def api_item_stream():
for i in range(5):
if i == 2: # 模拟第3次请求失败
raise ConnectionError("Timeout on item 2")
yield f"item-{i}"
# 安全消费
for item in api_item_stream():
print(item)
# 输出:item-0 → item-1 → (重试后)item-2 → item-3 → item-4
关键细节与避坑提醒
-
不要试图装饰迭代器对象:
iter([1,2,3])是个对象,没有__call__,无法被装饰器语法@xxx处理; -
限流单位是“每次
next()”:不是按秒限制总请求数,而是确保任意连续两次next()至少间隔1/rate_limit_per_second秒; -
重试只针对单次
next():失败后重试的是“获取当前这个 item”,成功后继续next()下一个,不会回滚或重复 yield 已成功的项; -
异常类型可细化:上面例子捕获所有
Exception,实际应传入exceptions=(ConnectionError, TimeoutError)参数,避免对ValueError等永久错误重试; -
线程安全需额外处理:如果多线程并发调用同一个迭代器实例,
last_yield_time会冲突——此时建议用threading.local()或改用异步版本(配合async for和asyncio.sleep)。
本质上,这是把「重试」和「限流」从函数级下沉到了迭代器的每次推进动作中,既保持了数据流的惰性,又赋予了每个数据单元独立的容错与节流能力。










