前言
大家好,我是倔强青铜三。欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!
欢迎来到 苦练Python第 49 天!
如果你已经跟着我们的节奏一路肝到第 49 天,那么恭喜你——你已经从青铜熬到黄金,是时候整点高级货了。
今天我们要聊的是 Python 里三位“懒人三宝”:
- 迭代器(Iterator)
- 生成器(Generator)
- 生成器表达式(Generator Expression)
别被名字吓到,它们本质上只做一件事:让数据“懒”起来,用的时候才生成,不用就不占内存。
我们用“类”写迭代器,用“函数”写生成器,用“表达式”写一行魔法,一口气吃透它们!
🧠 迭代器到底是什么?
Python 里,任何实现了 __iter__() 和 __next__() 的对象,都叫迭代器。
官方文档说:
迭代器协议 =__iter__()返回自身 +__next__()返回下一个值,直到抛StopIteration。
1. 用类手写一个反向迭代器
class Reverse:
"""对任意序列实现逆序迭代"""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index -= 1
return self.data[self.index]
使用姿势:
>>> for ch in Reverse("SPAM"):
... print(ch, end="")
MAPS
点评:
- 类写迭代器最直观,适合“需要状态”的复杂逻辑。
- 缺点:样板代码多,容易写错
StopIteration。
⚡️ 生成器:函数版的迭代器
生成器 = 用
yield偷懒写的迭代器。
2. 把上面的 Reverse 用生成器重写
def reverse(data):
for i in range(len(data) - 1, -1, -1):
yield data[i]
使用姿势完全一致:
>>> for ch in reverse("SPAM"):
... print(ch, end="")
MAPS
点评:
- 函数体里只要一个
yield,Python 自动帮你生成__iter__+__next__+ 状态保存。 - 代码量瞬间砍半,调试也舒服。
3. 生成器的高级玩法:无限流 & 惰性过滤
斐波那契无限流:
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
取前 10 项:
from itertools import islice
list(islice(fib(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
惰性偶数过滤:
even_fib = (n for n in fib() if n % 2 == 0)
list(islice(even_fib, 5))
# [0, 2, 8, 34, 144]
🔮 生成器表达式:一行代码的极致优雅
生成器表达式 = 列表推导式的圆括号版本,省内存利器。
4. 一行实现反向迭代
>>> data = "SPAM"
>>> (data[i] for i in range(len(data)-1, -1, -1))
<generator object <genexpr> at 0x...>
>>> ''.join(_)
'MAPS'
5. 实战:亿级日志的内存友好处理
假设你有一个 5 GB 的日志文件,只想找出包含 "ERROR" 的行并打印前 10 条。
import os
def grep_error(path):
with open(path) as f:
return (line for line in f if "ERROR" in line)
for err in islice(grep_error("huge.log"), 10):
print(err, end="")
- 传统写法:一次性读文件 → 内存爆炸。
- 生成器写法:读一行 yield 一行,内存稳稳的 50 MB 以内。
🛠️ 速查表:三兄弟对比
| 特性 | 类迭代器 | 生成器函数 | 生成器表达式 |
|---|---|---|---|
| 定义方式 | 手写类 | def + yield | 圆括号推导式 |
| 状态保存 | 手动属性 | 自动 | 自动 |
| 代码量 | 多 | 中 | 极少 |
| 可读性 | 复杂逻辑清晰 | 线性逻辑清晰 | 简单逻辑一行 |
| 适用场景 | 复杂状态/多方法 | 中等复杂度 | 简单惰性计算 |
🧪 实战项目:可迭代的“扑克牌发牌器”
用类迭代器 + 生成器表达式,写一个无限发牌器:
import itertools
from collections import namedtuple
Card = namedtuple("Card", "rank suit")
class Deck:
ranks = [str(n) for n in range(2, 11)] + list("JQKA")
suits = "♠ ♥ ♦ ♣".split()
def __iter__(self):
# 生成器表达式,一行生成 52 张牌
return (Card(r, s) for r, s in itertools.product(self.ranks, self.suits))
# 无限发牌
deck_cycle = itertools.cycle(Deck())
for i, card in enumerate(deck_cycle, 1):
print(card, end=" ")
if i % 13 == 0:
print()
if i == 52 * 3:
break
输出:
Card(rank='2', suit='♠') Card(rank='3', suit='♠') ... Card(rank='A', suit='♣')
点评:
Deck用生成器表达式实现__iter__,一行搞定 52 张牌。itertools.cycle让它无限循环,发牌永动机。
🧩 常见坑 & 调试技巧
| 坑 | 解释 | 解决 |
|---|---|---|
StopIteration 被吞 | 手动迭代器忘记抛异常 | 用生成器让 Python 帮你抛 |
| yield 之后不能 return value | 语法限制 | 用 return 只能结束,不能带值 |
| 生成器只能遍历一次 | 状态耗尽不可逆 | 重新实例化或转列表 |
| 调试看不到中间值 | 惰性求值 | 先用 list() 展开看 |
✅ 一句话总结
- 迭代器是协议,
__iter__+__next__。 - 生成器是语法糖,
yield一行顶十行。 - 生成器表达式是语法糖中的糖,圆括号就能 lazy。
掌握这三宝,写代码从此又懒又快又省内存!
🚀 互动时间
你最常用哪种方式写迭代?
A. 类迭代器
B. 生成器函数
C. 生成器表达式
评论区告诉我你的答案 + 使用场景。
最后感谢阅读!欢迎关注我,微信公众号:
倔强青铜三。
一键三连(点赞、收藏、关注),我们下一天见!