Python: 迭代器与生成器
你已经在用
for x in list:遍历数据了,但有没有想过in后面可以跟哪些东西?range(1000000)为什么不占内存?迭代器和生成器就是答案。它们让你能处理"无限"的数据流——按需生成,不占内存。
1. 什么是迭代器协议
Python 中,任何可以被 for 循环遍历的对象都可迭代(iterable)。它们背后实现了迭代器协议:
▶ 示例:迭代器协议基础
# 所有容器都是可迭代的
print(hasattr([1, 2, 3], "__iter__")) # True
print(hasattr("abc", "__iter__")) # True
print(hasattr(100, "__iter__")) # False——数字不可迭代
# 手动执行迭代过程
numbers = [1, 2, 3]
iterator = iter(numbers) # 获取迭代器
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
# print(next(iterator)) # StopIteration —— 迭代结束
输出:
# 执行成功
for 循环本质上就是:
▶ 示例:手动模拟 for 循环
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
value = next(it)
print(value)
except StopIteration:
break
输出:
# 执行成功
2. 自定义迭代器
实现 __iter__ 和 __next__ 方法,让你的类能用 for 遍历:
▶ 示例:倒计时迭代器
class Countdown:
"""倒计时迭代器"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self # 迭代器返回自身
def __next__(self):
if self.current <= 0:
raise StopIteration # 停止迭代
value = self.current
self.current -= 1
return value
# 使用
for i in Countdown(5):
print(i, end=" ") # 5 4 3 2 1
输出:
# 函数定义成功
▶ 示例:斐波那契迭代器(难度⭐⭐)
class Fibonacci:
"""斐波那契数列迭代器——生成前 N 个"""
def __init__(self, count):
self.count = count
self.index = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.index >= self.count:
raise StopIteration
self.index += 1
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
for num in Fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
输出:
# 函数定义成功
3. 生成器:yield
写一个类实现 __iter__ 和 __next__ 挺麻烦的。生成器函数用 yield 就能搞定同样的事:
▶ 示例:倒计时生成器
def countdown(start):
"""倒计时生成器"""
while start > 0:
yield start
start -= 1
# 使用
for i in countdown(5):
print(i, end=" ") # 5 4 3 2 1
输出:
# 函数定义成功
yield 和 return 不同——return 结束函数,而 yield 暂停函数,记住当前状态,下次调用时从暂停处继续。
▶ 示例:斐波那契生成器
# 同样的斐波那契——用 yield 实现,比迭代器类简洁得多
def fibonacci(count):
a, b = 0, 1
for _ in range(count):
yield a
a, b = b, a + b
for num in fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
输出:
# 函数定义成功
4. 生成器的懒加载特性
生成器最大的优势是——不一次性生成所有数据,按需生产:
▶ 示例:生成器懒加载
def generate_numbers():
"""模拟逐个生成数字"""
print("生成 1")
yield 1
print("生成 2")
yield 2
print("生成 3")
yield 3
gen = generate_numbers()
print("创建了生成器,但还没执行")
print(next(gen)) # 生成 1 \n 1
print(next(gen)) # 生成 2 \n 2
print(next(gen)) # 生成 3 \n 3
输出:
生成 1
生成 2
生成 3
创建了生成器,但还没执行
看到执行过程了吧?每次 next() 才执行到下一个 yield,然后停住。这个特性在处理大数据时非常有用:
▶ 示例:逐行读取大文件
# 处理大文件的"懒加载"方式
def read_large_file(filename):
"""逐行读取大文件,不一次性加载到内存"""
with open(filename, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# 用生成器处理——每次只处理一行
for line in read_large_file("huge_log.txt"):
if "ERROR" in line:
print(line) # 处理速度只受磁盘读取速度限制
5. 生成器表达式
和列表推导式很像,但用圆括号——生成器表达式是惰性求值的:
▶ 示例:生成器表达式
# 列表推导式——一次性生成所有数据
squares_list = [x ** 2 for x in range(1000000)]
print(f"列表大小:{len(squares_list)}") # 1000000(占内存)
# 生成器表达式——按需生成
squares_gen = (x ** 2 for x in range(1000000))
print(f"生成器:{squares_gen}") # generator object(几乎不占内存)
# 用法一样——for 遍历
for i, val in enumerate(squares_gen):
if i >= 5:
break
print(val, end=" ") # 0 1 4 9 16
输出:
# 执行成功
| 对比 | 列表推导式 [] |
生成器表达式 () |
|---|---|---|
| 内存 | 所有数据在内存中 | 按需生成,几乎不占 |
| 速度 | 生成快,后续访问快 | 生成惰性,每次计算 |
| 可复用 | 可以反复遍历 | 只能遍历一次 |
| 适用场景 | 数据量小、需要反复使用 | 数据量大、只需遍历一次 |
6. 常见应用场景
- 大数据处理:逐行读取日志、CSV 文件,避免 OOM。
- 无限序列:生成器可以表示"无限"的数据流——如斐波那契数列、传感器数据。
- 管道处理:多个生成器串联,形成数据处理流水线。
- 反向遍历:
reversed()对某些类型返回迭代器。 - 惰性求值:函数返回生成器,调用方决定取多少个数据。
❓ 常见问题
return 一次性返回结果,函数结束。生成器用 yield 逐个产生值,每次暂停保存状态,下次从暂停处继续。生成器函数每次调用返回一个新的生成器对象。list(gen) 把生成器转成列表。但如果数据量太大,转列表会耗尽内存。折中方案:用 itertools.tee() 复制生成器,或者重新调用生成器函数生成一个新的。yield 和 return 可以共存吗?return 在生成器中相当于"停止迭代"并返回 StopIteration 的值。不过很少这么用。实践中如果你发现一个函数既要 yield 又要 return,最好想想设计是否合理。📖 小节
- 可迭代对象支持
for遍历,背后是__iter__和__next__协议 - 手动迭代:
iter()获取迭代器,next()逐个取值,StopIteration结束 - 生成器函数用
yield逐个产生值,状态自动保存 - 生成器是懒加载的——用多少产多少,不占内存
- 生成器表达式
(x for x in seq)类似列表推导式,但惰性求值 yield from可以委托另一个生成器(高级用法,后面用到再学)
📝 作业
-
基础题(难度⭐):用生成器写一个
even_numbers(max_n)函数,生成从 0 到max_n之间的所有偶数。 -
进阶题(难度⭐⭐):用生成器写一个
repeat_list(items, times)函数,把列表中的每个元素重复times次。例如list(repeat_list([1, 2, 3], 2))返回[1, 1, 2, 2, 3, 3]。 -
挑战题(难度⭐⭐⭐):写一个生成器
deep_flatten(nested),可以展开任意深度的嵌套列表。例如list(deep_flatten([1, [2, [3, 4]], 5]))返回[1, 2, 3, 4, 5]。提示:用for遍历元素,如果元素是列表则递归调用yield from。