跳到主要内容

可迭代对象、迭代器、生成器与推导式

本节目标

查询 Python 迭代协议、生成器控制、yield from、生成器表达式和推导式。

迭代把“能提供元素”和“正处在读取位置”分成两个角色;生成器把这一读取位置连同暂停的执行状态封装为对象;推导式则用紧凑语法构造容器或惰性生成器。本章以 Python 3.14 表达式参考yield 语句参考为准,并接续Python 数据模型与特殊方法中的容器协议。

可迭代对象与迭代器边界

可迭代对象可交给 iter() 来取得迭代器;迭代器保存当前位置,并可由 next() 一次取得一个元素。容器式对象通常每次 iter() 都创建新的迭代器,因此可重复遍历;迭代器自身通常 iter(iterator) is iterator,表示它继续同一条消费进度。代表脚本中的 Countdown.__iter__ 每次返回新的 range 迭代器,所以两轮都从 3 开始。

values = [3, 2, 1]
assert list(values) == [3, 2, 1]
assert list(values) == [3, 2, 1]
iteration_report.py
class Countdown:
def __init__(self, start: int) -> None:
self.start = start

def __iter__(self):
return iter(range(self.start, 0, -1))


def squares(limit: int):
for value in range(limit):
yield value * value


def delegated_topics():
yield "start"
yield from ["python", "typing"]
yield "end"


countdown = Countdown(3)
first_pass = list(countdown)
second_pass = list(countdown)
iterator = iter(countdown)
first = next(iterator)
rest = list(iterator)
comprehension = {
"even": [value for value in range(1, 5) if value % 2 == 0],
"squares": [value * value for value in range(1, 4)],
}

print(f"reiterable={first_pass}|{second_pass}")
print(f"iterator-first={first}")
print(f"iterator-rest={rest}")
print(f"generator={list(squares(3))}")
print(f"delegated={list(delegated_topics())}")
print(f"comprehension={comprehension}")
reiterable=[3, 2, 1]|[3, 2, 1]
iterator-first=3
iterator-rest=[2, 1]
generator=[0, 1, 4]
delegated=['start', 'python', 'typing', 'end']
comprehension={'even': [2, 4], 'squares': [1, 4, 9]}

能交给 for 只证明对象可迭代,不证明它可重放。判断后续能否重新遍历时,要区分每次能产生新迭代器的来源,与已经携带当前位置的单次迭代器。

iternextStopIteration

iter(value) 取得迭代器,next(iterator) 推进一次并返回当前元素;没有下一个元素时,迭代器通过 StopIteration 表示正常耗尽。for 循环在内部处理这个异常,所以手动推进时才通常需要捕获它。协议转换是“可迭代对象 → iter() → 迭代器 → next() → 值或 StopIteration”。

iterator = iter(["Python"])
assert next(iterator) == "Python"
try:
next(iterator)
except StopIteration:
exhausted = True

StopIteration 是正常终止信号,并非可吞掉后继续重试的业务错误。常规迭代器一旦耗尽就会保持该状态,所以手动推进的循环应在收到信号时结束。

可重复遍历与单次消费

列表、元组和代表脚本的 Countdown 这类可迭代对象可反复调用 iter(),每次得到独立进度。生成器、文件迭代器和大多数由 iter(container) 得到的迭代器则是单次消费:第一次读取已经改变了它们的当前位置。把同一迭代器交给两个消费者时,第二个消费者只能得到第一个留下的部分。

iterator = iter([1, 2, 3])
assert next(iterator) == 1
assert list(iterator) == [2, 3]

两个步骤都需要全部数据时,可以先保存为容器、为每个步骤重新创建迭代器,或者明确提供可重放的来源。直接复用已消耗的生成器,只会让后一步看到剩余部分或空结果。

生成器函数、yield 与暂停状态

函数体含有 yield 时,该函数是生成器函数。调用生成器函数只创建生成器迭代器,函数体在首次推进时才开始执行;每次到达 yield 都返回一个值并暂停,局部变量、指令位置和异常处理状态会保留到下一次推进。next(generator) 恢复执行,直到下一个 yield 或函数结束;正常结束转为 StopIteration

def squares():
yield 0
yield 1

generator = squares()
assert next(generator) == 0

调用生成器函数只创建对象,不会立即执行其中的初始化或副作用。如果调用者从未推进它,函数体以及清理前的工作都不会发生。

sendthrowclose 与清理

生成器首次只能用 next()send(None) 启动;在第一个 yield 暂停前没有表达式可接收非 None 的发送值。后续 send(value) 让暂停的 yield 表达式求值为 valuethrow(exception) 在暂停点注入异常;close() 在暂停点注入 GeneratorExit,使 finally 块有机会清理资源。收到 GeneratorExit 后生成器应结束,不能再产出普通值。

def receiver():
try:
value = yield "ready"
yield value
finally:
cleaned = True

channel = receiver()
assert channel.send(None) == "ready"
channel.close()

首次推进前没有暂停的 yield 表达式可接收值,所以此时调用 send("value") 会抛出 TypeError。需要及时运行 finally 时,应由生成器的拥有者显式调用 close(),而不是等待垃圾回收。

yield from 委托

yield from iterable 把子可迭代对象产生的值直接交给当前生成器的调用者。对子生成器而言,send()throw()close() 会在其支持相应方法时参与委托;子生成器结束时所带 StopIteration.value 成为该 yield from 表达式的结果。代表脚本把中间两个主题委托给列表,因此输出顺序保持 startpythontypingend

def labels():
yield "start"
yield from ["python", "typing"]
yield "end"

assert list(labels()) == ["start", "python", "typing", "end"]

委托会继续消费同一个子迭代器;若该来源是单次迭代器,委托结束后它也已耗尽,重用时必须提供新的可迭代对象或迭代器。yield from 的目标必须是可迭代对象;把普通非可迭代值交给它会在开始委托时失败,它也不会自动并行消费多个来源。

生成器表达式与惰性求值

生成器表达式以圆括号写出,产生生成器迭代器而不是立刻构造列表。生成器表达式只有最左侧 iterable 表达式会立即求值:其结果会立刻被传给 iter();元素表达式、后续 for 和过滤条件都在请求下一个值时才惰性求值。因此左侧来源的名称错误会在定义表达式时出现,而元素计算错误可延后到消费时出现。

source = [1, 2, 3]
squares = (value * value for value in source)
assert next(squares) == 1

惰性求值不会自动冻结来源。来源或其元素在消费前发生变化时,后续求值可能观察到新状态;若需要的是创建时快照,应先构造明确容器。

列表、集合与字典推导式

列表、集合和字典推导式会完成循环与过滤后分别构造新 listsetdict。从左到右嵌套 for/if 子句,只有到达最内层时才计算元素表达式;字典推导式计算键和值并插入字典。列表保留产生顺序,集合会去重且不应用于需要固定显示顺序的输出,字典的重复键以后一次为准。

even = [value for value in range(1, 5) if value % 2 == 0]
labels = {value for value in ["python", "python"]}
squares = {value: value * value for value in range(1, 4)}
assert even == [2, 4]

列表、集合和字典推导式会立即物化结果并立刻完成工作;面对大型或无界输入会消耗过多内存或永远无法结束,应改用按需消费的生成器表达式。集合的直接迭代顺序不能作为确定性展示或测试结果;需要稳定序列时排序,或改用列表、按插入顺序使用字典。

推导式作用域与求值顺序

推导式在单独的隐式嵌套作用域中执行,因此 for 目标不会泄漏覆盖外围的同名绑定。最左侧 for 的 iterable 在外围作用域求值后传入这个隐式作用域;后续 for 与条件可依赖前一层取得的值,只能在嵌套作用域中求值。该边界解释了为何推导式可读取外围名称,却不会把它的迭代变量写回外围。

value = "outer"
items = [value * 2 for value in range(2)]
assert items == [0, 2]
assert value == "outer"

由于隐式推导式作用域禁止使用 yield,不能借它延后工作或安排暂停期间的清理;需要暂停或精确控制清理时机时,应写显式生成器函数。yield from 同样在该隐式作用域中被禁止,不能用来“边构造边返回”。本章只讨论同步迭代,不把异步迭代规则混入此边界。

消费状态、终止条件与资源释放

排查迭代问题时,先标出对象转换和时间线:可迭代对象经 iter() 产生迭代器,next() 推进至值或 StopIteration;生成器调用只创建对象,推进才执行,send/throw/close 改变暂停点;容器推导式立即完成,生成器表达式除最左来源外延后计算。需要自定义对象协议时,也可回看数据模型的容器模拟与下标访问;下一章会把对象组织方式扩展到类、继承、组合与 dataclass

iterator = iter([1])
assert list(iterator) == [1]
assert list(iterator) == []

无限迭代器需要明确的停止条件或限量消费;单次迭代器经过第一次 list() 后已耗尽,第二次只会得到空列表。生成器的正常结束应通过 return 或函数体结束表达;内部迭代耗尽所产生的 StopIteration 需在合适边界处理,若意外逃出生成器函数体,就会被转换为 RuntimeError