Skip to content

Python 生成器:边算边给的"懒人"数据工厂

引言:点菜的两种方式

你去餐厅吃饭,有两种点菜方式:

  • 方式一:让厨师把 100 道菜全部做好,摆满一桌子,你再挑着慢慢吃。桌子再大也摆不下,而且你可能吃两口就饱了,剩下的全浪费;
  • 方式二:你点一道,厨师做一道,你边吃边点。厨房不占地方,你也能随时停。

Python 的**生成器(Generator)**就是方式二——不一次性生成所有数据,而是你要一个,它算一个。这在处理海量数据时,是救命级的技巧。


一、为什么需要生成器?内存的痛点

1.1 列表的局限

上一篇我们学了列表生成式,一行代码就能造出一个列表:

python
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

但如果要生成 100 万个平方数呢?

python
>>> big_list = [x * x for x in range(1000000)]   # 内存瞬间吃掉几十 MB

更惨的是,如果你只需要前 10 个,后面 99 万多个元素占用的内存就全白扔了。

生活化理解:就像你只想喝一杯水,却买了整个水库的水倒进家里——杯子满了,水库空了,你还没喝完。

1.2 生成器的解决方案

如果列表元素能按某种算法推算出来,那我们能不能边循环边计算呢?这样就不必创建完整列表,内存占用始终是一个元素的量级

在 Python 中,这种"一边循环一边计算"的机制,就叫生成器(Generator)

生活化理解:生成器就像一个现做现卖的煎饼摊——你要一个,他摊一个;你不要,他不摊。锅里永远只有一个煎饼,而不是提前摊好 100 个堆在那里。


二、创建生成器:两种方法

2.1 方法一:把 [] 改成 ()

列表生成式用方括号 [],生成器表达式用圆括号 ()

python
>>> L = [x * x for x in range(10)]   # 列表
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> g = (x * x for x in range(10))   # 生成器
>>> g
<generator object <genexpr> at 0x1022ef630>

关键区别

  • L 是一个真实的列表,所有元素已经在内存里;
  • g 是一个生成器对象,里面一个元素都没有,只保存了"怎么算"的算法。

2.2 怎么取出元素?next() 和 for 循环

生成器不能直接用索引 g[0] 取值,因为它根本没有"第 0 个"——得现算。

方式一:手动 next()

python
>>> g = (x * x for x in range(10))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
...   # 一直调用,直到
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
  ...
StopIteration

每次 next(g),生成器就算出下一个值;算完最后一个,再调用就抛 StopIteration 错误。

方式二:for 循环(推荐)

python
>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
...
0
1
4
...
81

生活化理解next() 像你手动按一下出餐铃,服务员上一道菜;for 循环像你开了自动模式,服务员一道接一道上,直到你说"停"。

注意:生成器是一次性的。上面的 for 循环跑完后,g 就空了,再 for 一次不会有任何输出。


三、方法二:用函数 + yield 创建生成器

3.1 什么时候需要函数式生成器

生成器表达式适合简单场景,但如果算法比较复杂(比如斐波那契数列),for 循环写不出来,就得用函数

3.2 斐波那契数列:从普通函数到生成器

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, ...(除前两个数外,每个数都是前两个数之和)

普通函数版本:用 print 打印

python
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

神奇的一行a, b = b, a + b

这行代码同时给 a 和 b 赋新值,不需要临时变量。等价于:

python
t = (b, a + b)   # 先打包成元组
a = t[0]
b = t[1]

生成器版本:把 print(b) 改成 yield b

python
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b          # 唯一改动!
        a, b = b, a + b
        n = n + 1
    return 'done'

调用方式完全不同

python
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

普通函数调用直接返回结果;生成器函数调用返回一个生成器对象

3.3 yield 的魔法:暂停与继续

生成器函数的执行流程和普通函数完全不同

  • 普通函数:从第一行执行到 return 或最后一行,然后结束;
  • 生成器函数:每次 next() 执行到 yield暂停,把值扔出去;下次 next()上次暂停的地方继续执行。

生活化理解:就像看电视剧——普通函数是连续看完整部电影;生成器函数是"看一集暂停,去干别的,下次接着看下一集"。

3.4 逐帧观察:odd() 生成器

python
def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield 3
    print('step 3')
    yield 5
python
>>> o = odd()
>>> next(o)
step 1
1          # 执行到 yield 1,暂停
>>> next(o)
step 2
3          # 从上次暂停处继续,执行到 yield 3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  ...
StopIteration    # 没有更多 yield 了,结束

关键洞察print('step 1') 只在第一次 next() 时执行,后面不再重复——因为函数从 yield 处继续,而不是从头开始。


四、for 循环与 return 值

4.1 for 循环拿不到 return 值

python
>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8

fib(6)return 'done' 哪去了?for 循环拿不到生成器的 return 值

4.2 捕获 return 值:try...except

python
>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

return 的值藏在 StopIteration 异常的 value 属性里。


五、实战应用场景

5.1 读取大文件

python
def read_large_file(file_path):
    """逐行读取大文件,不占用全部内存"""
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            yield line.strip()

# 使用:文件有 100 万行,内存里始终只有 1 行
for line in read_large_file('huge_log.txt'):
    process(line)

5.2 生成无限序列

python
def infinite_counter():
    """生成无限的自然数序列"""
    n = 0
    while True:
        yield n
        n += 1

counter = infinite_counter()
print(next(counter))   # 0
print(next(counter))   # 1
print(next(counter))   # 2
# ... 永远不会内存爆炸,因为每次只算一个

5.3 数据管道:逐层过滤

python
def numbers():
    for i in range(1000000):
        yield i

def even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def square(nums):
    for n in nums:
        yield n * n

# 像流水线一样串联
result = square(even(numbers()))
for x in result:
    if x > 100:
        break
    print(x)

每个环节都是生成器,数据像水流一样逐层通过,内存占用始终极小。


六、经典练习:杨辉三角生成器

题目:用生成器输出杨辉三角的每一行。

python
def triangles():
    row = [1]
    while True:
        yield row
        # 根据当前行计算下一行
        row = [1] + [row[i] + row[i+1] for i in range(len(row)-1)] + [1]

验证

python
n = 0
results = []
for t in triangles():
    results.append(t)
    n = n + 1
    if n == 10:
        break

for t in results:
    print(t)

输出:

text
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
...

思路解析

  1. 第 0 行固定是 [1]
  2. 每一行的首尾永远是 1
  3. 中间的每个数 = 上一行相邻两数之和;
  4. yield 把当前行抛出去,同时准备下一行。

七、常见误区与避坑指南

误区 1:以为生成器可以反复迭代

python
g = (x * x for x in range(3))
print(list(g))   # [0, 1, 4]
print(list(g))   # []  空的!生成器已耗尽

生成器是一次性的,迭代完就空了。如果要重复使用,得重新创建:

python
g = (x * x for x in range(3))   # 重新创建
print(list(g))   # [0, 1, 4]

误区 2:直接 next(odd()) 导致每次都从头开始

python
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1   # 又是 1?!

原因odd() 每次都创建新的生成器对象next() 当然返回第一个值。

正确做法:先保存生成器对象,再反复 next()

python
>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3

误区 3:以为 yield 和 return 一样

python
def f():
    yield 1
    return 2   # 这个 2 不会被 for 循环拿到!
    yield 3    # 永远不会执行
  • yield:暂停,把值抛出去,下次继续;
  • return:彻底结束函数,for 循环直接终止,return 值只能通过捕获 StopIteration 拿到。

误区 4:在生成器表达式里用 return

python
# 错误:生成器表达式里没有 return
g = (x if x > 0 else return for x in [1, -2, 3])   # SyntaxError!

生成器表达式是简洁语法,不能写复杂逻辑。复杂场景请用 yield 函数。

误区 5:混淆生成器函数和普通函数

python
def f1():
    return [1, 2, 3]

def f2():
    yield 1
    yield 2
    yield 3

print(f1())   # [1, 2, 3]        —— 普通函数,直接返回结果
print(f2())   # <generator ...>  —— 生成器函数,返回生成器对象

判断方法:函数体里有没有 yield。有 yield 就是生成器函数,调用返回生成器对象;没有就是普通函数,调用直接返回结果。


八、动手练习

  1. 基础题:写一个生成器 countdown(n),从 n 倒数到 1,每次 yield 一个数。
  2. 进阶题:写一个生成器 fib_gen(),用 yield 生成斐波那契数列的前 10 个数,然后用 for 循环打印。
  3. 挑战题:写一个生成器 read_words(file_path),逐行读取文件,再逐行把每行拆成单词 yield 出去,实现"逐词读取"大文件。
  4. 思考题[x * 2 for x in range(5)](x * 2 for x in range(5)) 有什么区别?分别用在什么场景?

小结

  • 生成器(Generator):边循环边计算,不一次性生成所有元素,极大节省内存。
  • 两种创建方式:生成器表达式 (x * x for x in range(10));生成器函数(含 yield 的函数)。
  • 执行机制:每次 next() 执行到 yield 暂停,下次从暂停处继续;没有更多 yield 时抛 StopIteration
  • 最佳实践:永远用 for 循环迭代生成器,不要手动调 next()
  • 一次性:生成器迭代完就空了,不能重复使用。
  • 核心心法:生成器是"懒人"哲学——只在你真正需要的时候才干活,而且只干一点点。处理大数据、无限序列、数据管道时,它是唯一可行的方案。