跳到主要内容
约 6 分钟5 张卡

Python 迭代器与生成器

迭代器保存遍历进度,每次调用 next() 返回下一个元素。生成器是一种创建迭代器的方式:函数在 yield 处产出值并暂停,下次取值时再继续执行。

它们能让日志和批量数据按需处理,但也带来一次性消费、延后发生的异常和资源持有问题。准备面试时,需要同时说明“什么时候计算”和“谁保存了遍历状态”。

1. 面试核心​

先记住这几条,再读正文
  • 可迭代对象可以交给 iter() 得到迭代器;迭代器可以交给 next() 取值。列表通常每次迭代都创建新的进度,已有迭代器则继续自身进度。卡 · 可迭代对象与迭代器
  • 迭代器通过 StopIteration 表示结束,for 会处理这个结束信号。同一个迭代器耗尽后不会自动重置。卡 · 可迭代对象与迭代器
  • 调用生成器函数得到生成器对象,函数体在首次取值时才开始执行。yield 暂停并保留局部状态,return 结束生成器。卡 · 生成器的暂停与继续
  • 惰性求值可以避免一次性保存全部结果,但不保证所有内存占用都是常数,也不自动缩短完成全部工作所需的时间。卡 · 惰性求值与流水处理
  • 生成器可能在暂停期间持有文件或其他资源。停止消费不等于显式关闭,资源生命周期仍需由 with 或明确关闭来管理。卡 · 资源生命周期

2. 可迭代对象与迭代器​

可迭代对象是能通过 iter(obj) 取得迭代器的对象。列表、字典、集合与字符串都可迭代。迭代器实现 __next__() 提供下一项,且其 __iter__() 返回自身。

values = [10, 20]
first = iter(values)
second = iter(values)

print(next(first)) # 10
print(next(first)) # 20
print(next(second)) # 10
print(iter(first) is first) # True

两个列表迭代器有独立进度,所以 first 已经取完两项时,second 仍从第一项开始。对 first 再调用 iter 不会创建新遍历,它返回同一个迭代器。

for value in values 会先取得迭代器,然后反复取值,直到遇到 StopIteration。这个结束信号由循环处理,不应被当作普通业务失败。

iterator = iter([1])
print(next(iterator)) # 1
print(next(iterator, "end")) # end
print(list(iterator)) # []

第二次 next 指定了缺省值,因此耗尽时返回 "end";没有缺省值就会抛出 StopIteration。list(iterator) 消费的也是剩余部分,不会自动从头恢复。

3. 生成器的暂停与继续​

函数定义中包含 yield,调用它时会得到生成器对象。真正执行函数体要等到首次取值。

def numbers(events):
events.append("started")
yield 1
events.append("continued")
yield 2
events.append("finished")

events = []
generator = numbers(events)
print(events) # []
print(next(generator)) # 1
print(events) # ['started']
print(list(generator)) # [2]
print(events) # ['started', 'continued', 'finished']

第一次 next 执行到 yield 1 后暂停;后面的语句还没有执行。list(generator) 继续消费剩余内容,经过第二次产出后再取值,最终使函数正常结束。

yield 会保留当前局部变量与执行位置。普通函数的 return 则结束本次调用;生成器中的 return 同样终止迭代,不会作为普通元素再产出一次。不要主动 raise StopIteration 来结束生成器,直接 return 或运行到末尾即可。

3.1 生成器表达式​

生成器表达式使用圆括号表达按需生成,列表推导式会立即构造完整列表。

squares = (value * value for value in range(4))
print(next(squares)) # 0
print(list(squares)) # [1, 4, 9]
print(list(squares)) # []

需要重复读取结果时,可以重新创建生成器,或者明确转成列表缓存。转为列表需要保存全部结果,不能同时声称已经实现完整结果缓存且不占结果内存。

生成器表达式也并非把所有操作都延后:最左侧 for 的可迭代表达式会在创建时求值。表达式其余部分通常在消费时执行,因此排查异常时要分清创建与取值两个阶段。

4. 惰性求值与流水处理​

惰性求值指结果在真正需要时才计算。它适合只消费部分结果,或者让各阶段逐条传递数据的任务。

def valid_counts(lines):
for line in lines:
text = line.strip()
if text:
yield int(text)

counts = valid_counts(["1", "", "2"])
print(sum(counts)) # 3

valid_counts 不需要先建立转换后的完整列表,sum 可以边取值边累计。若输入来自文件迭代器,各阶段就能按行处理。

这里仍有边界:示例中的输入列表已经占有内存,生成器还会保存局部状态;如果每行非常大,或处理逻辑把所有记录放入一个字典缓存,整体内存仍会增长。去重、分组与全局排序也可能要求保留额外状态。

4.1 延后的异常​

异常发生在相关语句实际执行时。创建生成器成功,不代表后续消费一定成功。

def parse_values():
yield int("1")
yield int("bad")

values = parse_values()
print(next(values)) # 1
try:
next(values)
except ValueError:
print("parse failed") # parse failed

只在 values = parse_values() 周围捕获异常,处理不到后面取值时的解析失败。应在消费边界决定停止、记录失败还是跳过不合法记录,不能因为错误延后发生就忽略它。

5. 资源生命周期​

生成器暂停时,局部变量仍然存在,已打开的资源也可能仍被持有。提前退出 for 不会自动对任意迭代器执行关闭协议。

资源由调用方持有时,关系更清楚:

import io

def nonempty_lines(stream):
for line in stream:
text = line.strip()
if text:
yield text

with io.StringIO("first\nsecond\n") as stream:
lines = nonempty_lines(stream)
print(next(lines)) # first
print(stream.closed) # True

离开 with 时流已经关闭,即使 lines 还没有消费完。后面不应再继续读取这个生成器,因为它依赖的流已失效。

生成器自身管理资源时,可以用 try/finally 清理,并在不再使用时显式调用 close();生成器关闭会让暂停位置开始收尾。但正常业务仍应优先使用可看见的资源边界,不能依赖解释器何时回收最后一个引用。使用 contextmanager 表达进入与退出的完整协议,见 Python 上下文管理器。

6 · 面试题卡片​

本页题目与参考回答

6.1 可迭代对象、迭代器与生成器有什么关系?​

考察重点:迭代协议、进度保存与生成器的实现角色。

相关内容:可迭代对象与迭代器、生成器的暂停与继续。

参考回答

可迭代对象能通过 iter() 提供迭代器,迭代器保存取值进度并通过 next() 提供下一项;生成器是迭代器的一种。

列表可以多次产生新的迭代器,每个都有独立进度。生成器通过 yield 暂停与继续,自动保存局部状态,无需手动编写完整的迭代器类。

6.2 对同一个生成器连续调用两次 list,为什么第二次通常为空?​

考察重点:消费剩余项、耗尽状态与重建结果。

相关内容:可迭代对象与迭代器、生成器表达式。

参考回答

第一次调用已经把生成器消费完,第二次读取的是同一迭代器的剩余部分。 迭代器不会因为重新遍历就恢复进度。

需要重复读取,可以重新调用生成器函数创建新对象,或第一次就缓存为列表;后者需要为全部结果付出存储成本。

6.3 生成器中的 yield 和 return 有什么区别?​

考察重点:产出值、暂停状态和结束迭代。

相关内容:生成器的暂停与继续。

参考回答

yield 产出一个值并暂停,下一次取值从暂停处继续;return 结束生成器。 调用生成器函数本身只得到生成器对象,首次取值才开始运行函数体。

return 的值不会被普通 for 当作下一个元素消费。结束生成器用 return 或运行到末尾,不主动抛 StopIteration。

6.4 使用生成器是否一定是 O(1) 内存,而且更快?​

考察重点:按需执行、存储边界和完整工作量。

相关内容:惰性求值与流水处理、延后的异常。

参考回答

不能这样保证。 生成器避免一次保存全部输出,但输入、局部变量和去重或分组缓存仍可能占用大量内存。逐个产出并不免除计算每个结果的工作,还会有迭代开销。

只需要前几个结果或希望逐行处理时,惰性执行可以减少不必要的计算和存储;要多次访问或排序全部结果时,需要考虑缓存与整体操作成本。错误也可能推迟到消费阶段才出现。

6.5 提前停止消费持有文件的生成器,文件会自动关闭吗?​

考察重点:暂停状态、资源所有权和关闭时机。

相关内容:资源生命周期。

参考回答

不能只靠停止消费保证及时关闭。 生成器可能仍被引用并暂停在资源打开期间;for 的提前退出不会自动调用任意迭代器的关闭方法。

可以让调用方用 with 管理文件,并限定生成器在该范围内消费;生成器自己管理资源时用 try/finally,在不再需要时显式关闭。不要依赖垃圾回收的时机完成业务资源释放。

7. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明