第 8 章 · 迭代器、生成器与函数式

这一章讲 Python 的「内功」:迭代协议(for 循环的真相)、生成器(惰性求值,内存的救星)、高阶函数(函数式三板斧)。它们是「Pythonic」的分水岭:会用的人代码又短又省内存,不会的人手写一堆临时列表。数据结构课第 2 章的「链表按需访问」、计组课第 7 章(存储层次)的「按需加载」,在这一章找到了 Python 语法级对应。

8.1 迭代协议:for 循环的真相

for 循环不是「数下标」,而是一次次向对象要下一个元素。协议两层:

  • 可迭代对象(Iterable):实现了 __iter__(返回一个迭代器)。列表、字符串、字典、文件、range 全是;
  • 迭代器(Iterator):实现了 __next__(返回下一个,没有则抛 StopIteration)+ __iter__。
it = iter([10, 20])       # 手动走一遍 for 的内部过程
next(it)                  # 10
next(it)                  # 20
next(it)                  # StopIteration → for 在这里优雅收尾

for x in obj 的机器级真相 = iter(obj) 拿迭代器 + 反复 next + StopIteration 收尾。文件对象就是迭代器(第 5 章 for line in f 的原理:逐行惰性读,内存恒定)。理解协议后的红利:任何自定义类实现 __iter__/__next__ 就能被 for 遍历(第 6 章鸭子类型:实现协议即获得能力)。

8.2 生成器:yield 出来的惰性流

生成器(generator)是「会暂停的函数」:yield 处交出一个值并冻结现场,下次调用从冻结处继续:

def countdown(n):
    while n > 0:
        yield n          # 交出一个值,暂停在这
        n -= 1

for x in countdown(3):   # 3, 2, 1
    print(x)

g = countdown(10**9)     # 瞬间创建!一个十亿数的「流」
next(g), next(g)         # 10, 9 —— 不占十亿份内存

对比列表:列表是「全造好放着」,生成器是「要一个造一个」(惰性求值)。两个杀手级场景:大文件处理(逐行生成,内存恒定,第 5 章)、无限序列(斐波那契生成器,永不结束也不爆内存):

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

from itertools import islice
list(islice(fib(), 8))       # [0, 1, 1, 2, 3, 5, 8, 13](取前 8 个)

生成器表达式(第 3 章的括号版推导式)是它的语法糖:sum(n*n for n in range(10**8)) 全程 O(1) 内存(换成列表推导式直接内存爆炸)。「能用生成器就不造列表」是处理大数据的第一戒律。

8.3 管道:生成器的组合威力

生成器的真正威力在串联(Unix 管道思想,第 13 章 Java 网络课的流):

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.strip()

def filter_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def truncate(lines, n=50):
    for line in lines:
        yield line[:n]

# 三段管道:每个生成器都不知道上家是谁,流式流过
for line in truncate(filter_errors(read_lines("big.log"))):
    print(line)

数据像水流过三节管道,内存里同时只存在一行;中间任何一段换成不同的生成器(不同数据源、不同过滤),管道照常工作。这是 Unix 哲学(小工具组合)在语言级的实现,也是第 10 章(处理几个 GB 的 CSV 不爆内存)的技术底座。

8.4 函数式三板斧:map、filter、sorted(key)

Python 的函数式不全纯,但三件套值得会用:

nums = [3, -1, 4, -1, 5]

list(map(abs, nums))              # [3, 1, 4, 1, 5](对每个元素施函数)
list(filter(lambda x: x > 0, nums))   # [3, 4, 5](过滤)
sorted(nums, key=abs)             # [-1, -1, 3, 4, 5](按绝对值排序!)
sorted(users, key=lambda u: u.age, reverse=True)   # 按字段倒序

key= 是 Python 排序的灵魂(不写比较函数,写「取排序依据」的函数;对照 Java 课第 11 章的 Comparator 和计组课第 11 章 qsort 回调,同一思想三代语法)。lambda 是匿名小函数(单表达式,复杂逻辑用 def);functools.reduce(累积累积)、functools.partial(固定部分参数)是进阶工具。风格建议:Python 社区更爱推导式(8.2)多于 map/filter 链,两者都要能读。

8.5 迭代工具箱:itertools 与 enumerate 的亲戚

from itertools import islice, chain, groupby, count, product

islice(fib(), 5, 10)          # 惰性切片(无限序列的窗口)
chain([1,2], [3,4])           # 串接多个可迭代
count(start=10)               # 无限计数器
product("AB", repeat=2)       # 笛卡尔积:(A,A) (A,B) (B,A) (B,B)(离散课第 1 章)

sorted(words, key=len, reverse=True)[:3]     # 最长的三个词
max(scores, key=lambda s: s.value)           # 最大者(不是最大值,是拿最大值的元素!)
any(x > 0 for x in nums), all(x > 0 for x in nums)   # 存在/全称(惰性短路)

max(key=)、any/all + 生成器表达式是高频惯用法:any 找到即停(短路与第 2 章同款),全称判断直接对译离散数学的 ∃/∀(离散课第 3 章)。

常见错误

  • 生成器只能消费一次。g = (x for x in ...); sum(g); sum(g) 第二次得 0(耗尽了);要复用转列表 list(g)(付出内存)。
  • 在生成器里 yield 之前 return 值。生成器函数里 return 只能裸 return(终止),return x 等于抛 StopIteration(x) 细节坑;终止用 return 或 raise。
  • 遍历时改容器(第 3 章重申)。迭代器对着原容器工作;要改就遍历副本或建新容器。
  • map/filter 的结果是惰性的。print(map(f, lst)) 只打印对象地址;要 list() 落地(或 for 消费)。

练习

  1. 动手:写生成器 def read_numbers(path) 逐行产出文件里的整数(跳过空行与非数字行);用 sum(read_numbers("data.txt")) 统计总和;把文件换成 100 万行验证内存恒定(任务管理器看)。
  2. 动手:实现无限素数生成器 primes()(试除法),用 islice 取前 20 个素数;体会「无限流」在列表世界里不可能、在生成器里自然。
  3. 实现:管道重构:把第 5 章的日志分析器(read → filter ERROR → 提取字段 → 汇总)改写为四个生成器函数的管道,对比两个版本的内存占用与可组合性。
  4. 思考:第 5 章「逐行读文件」、本章「生成器惰性」、第 9 章将学的「协程」是一条演化线(暂停-恢复的函数)。对比「回调(Java 课第 11 章 GUI)」「生成器(yield)」「async/await」三种「暂停-恢复」的表达方式,各自把控制权交给了谁?