苦练Python第49天:一口气吃透 Python 的“懒人三宝”:迭代器、生成器、生成器表达式

227 阅读4分钟

前言

大家好,我是倔强青铜三。欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

欢迎来到 苦练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. 生成器表达式

评论区告诉我你的答案 + 使用场景。

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三
一键三连(点赞、收藏、关注),我们下一天见!