今天你 AI 赋能了吗?打开浏览器,在地址栏输入 xuefulu.ai ,按下 Enter 即可访问学府路1号!
2026年10月14日 星期三 10:40:25
常用网址: | | 学府路1号 https://xuefulu.ai | 下载 Python https://xuefulu.ai/Python.html | 下载 TRAE https://xuefulu.ai/Trae.html

一、为什么还需要「生成器」

前面学过列表推导式,它能一行把一串结果全算出来。但「全算出来」这件事本身有个前提:结果装得下。

如果要做的是「一百万个数的平方和」,列表推导式会先把一百万个数字真的造出来放进内存,再求和。而生成器(generator)的想法是:别急着造,用到哪个才算哪个。

先看两个写法。下面这两段代码输出一模一样,但干活的方式完全不同:

Python
# 写法一:先把 1~5 的平方全算出来,装进列表
squares = [n * n for n in range(1, 6)]
for x in squares:
    print(x)

输出:

输出
1
4
9
16
25
Python
# 写法二:生成器——要用一个才算一个
squares = (n * n for n in range(1, 6))
for x in squares:
    print(x)

输出:

输出
1
4
9
16
25

写法一的方括号 [] 造的是一个列表,五个数当场全部算好;写法二的圆括号 () 造的是一个生成器,它只在循环要的时候才算下一个。

数据只有五个时看不出差别。数据是五百万个时,写法一会把内存吃满,写法二依然只占一点点。

一句话记住:列表是「一箱货」,生成器是「一条传送带」。箱子要先装满,传送带是你要一件它才送一件。

本篇要讲两件相互咬合的事:迭代器(能一个个往外吐东西的东西)和生成器(自己动手造迭代器的最省事办法)。第 16 篇《列表》第十二节里那句「后面学『生成器』时,还会回到这套机制上」,说的就是这里。

课程思政|工匠精神与严谨求实 · 用到哪个才算哪个

列表推导式会把结果一次全算出来,生成器却只在你伸手要的时候才产出下一个。该省的地方省下来,不是小气,是对资源的敬畏——内存、算力、电费都是公共的家底。写代码时先问一句「这些结果当真都要用吗」,能按需而作、不图虚功,才谈得上精打细算的那点工匠功夫。

二、可迭代对象:能被 for 循环的,都是

写到现在,被 for 循环直接遍历过的东西已经不少了。凡是能写在 for 后面的,都叫可迭代对象(iterable)。

Python
print('字符串:', end=' ')
for ch in 'abc':
    print(ch, end=' ')
print()
print('列表:', end=' ')
for x in [1, 2, 3]:
    print(x, end=' ')
print()
print('字典:', end=' ')
for k in {'a': 1, 'b': 2}:
    print(k, end=' ')
print()
print('range:', end=' ')
for x in range(3):
    print(x, end=' ')
print()
print('文件对象(这里用一段内存文本代替):', end=' ')
import io
for line in io.StringIO('第一行\n第二行\n'):
    print(line.strip(), end=' ')
print()

输出:

输出
字符串: a b c 
列表: 1 2 3 
字典: a b 
range: 0 1 2 
文件对象(这里用一段内存文本代替): 第一行 第二行 

常用的可迭代对象有以下这些:

可迭代对象长什么样特点
字符串'abc'一个字符一个字符地给
列表[1, 2, 3]按顺序给
元组(1, 2, 3)按顺序给
字典{'a': 1}默认给键,不是值
集合{1, 2, 3}顺序不保证
rangerange(5)边算边给,不占内存
文件对象open(...)一行一行地给
生成器(x for x in ...)要一个才给一个
判断小技巧:凡是能写进 for 后面的、能写进 sum()/max()/list() 括号里的,基本都是可迭代对象。

但要注意:可迭代对象 ≠ 迭代器。这个区别下一节就讲清楚。

三、迭代器:能一个个往外吐

迭代器(iterator)是一种更「纯粹」的东西:它自己就是一条数据通道,你问它要,它给你一个;再问,再给一个。

怎么认出一个迭代器?两种办法:

  1. 它能被 next() 调用,每次吐出一个值;
  2. 它自己也能被 for 遍历(因为迭代器本身就是可迭代对象)。
Python
colors = ['红', '绿', '蓝']
it = iter(colors)          # 把列表「包装」成迭代器
print('第一个:', next(it))
print('第二个:', next(it))
print('第三个:', next(it))

输出:

输出
第一个: 红
第二个: 绿
第三个: 蓝

这里出现了两个新面孔:iter() 和 next()。它们的配合方式是:

函数作用一句话
iter(x)把可迭代对象 x 变出一个迭代器开一条传送带
next(it)从迭代器 it 里取出下一个值从传送带上拿一件

列表和迭代器的区别在这里:列表是「仓库」,迭代器是「取货顺序」。列表同时装着全部数据,你可以反复从头看;迭代器只记得「下一个该给谁」,给过就过去了。

Python
# 列表可以反复遍历,迭代器不行
nums = [1, 2, 3]
print('列表第一次:', list(nums))
print('列表第二次:', list(nums))
it = iter(nums)
print('迭代器第一次:', list(it))
print('迭代器第二次:', list(it))

输出:

输出
列表第一次: [1, 2, 3]
列表第二次: [1, 2, 3]
迭代器第一次: [1, 2, 3]
迭代器第二次: []

第二次是空列表 []——因为迭代器已经走到头了,它不会回头。这是生成器最常被踩的坑,第十节会专门讲。

四、iter() 与 next():手动走一遍 for

写 for x in 某个列表 时,Python 在背后偷偷做了三件事。现在把它拆开看:

  1. 先调用 iter(某个列表),拿到一个迭代器;
  2. 反复调用 next(迭代器),每拿到一个值就把这个值给 x,执行一遍循环体;
  3. 当 next() 说「没有了」时,循环安静地结束,不报错。

所以下面这两段代码是完全等价的:

Python
# 平时这么写
for x in ['a', 'b', 'c']:
    print('拿到', x)

输出:

输出
拿到 a
拿到 b
拿到 c
Python
# 手写一遍它背后做的事
it = iter(['a', 'b', 'c'])
print('第 1 次', next(it))
print('第 2 次', next(it))
print('第 3 次', next(it))

输出:

输出
第 1 次 a
第 2 次 b
第 3 次 c

既然一样,为什么平时还用 for?因为 for 会自动处理「什么时候没了」,你不用自己数还剩几个。

平时写代码根本不用手动 iter() / next(),直接 for 就行。但看懂这一层很有用:出错信息里的「迭代器」「可迭代对象」这些词,说的就是这套机制。

还有一个常用的静态判断:collections.abc 里有两个专门用来「验身份」的东西,这里先混个脸熟。

Python
from collections.abc import Iterable, Iterator
print('str 可迭代?', isinstance('abc', Iterable))
print('list 可迭代?', isinstance([1, 2], Iterable))
print('list 是迭代器?', isinstance([1, 2], Iterator))
print('iter(list) 是迭代器?', isinstance(iter([1, 2]), Iterator))

输出:

Python
str 可迭代? True
list 可迭代? True
list 是迭代器? False
iter(list) 是迭代器? True

结论很清楚:列表是可迭代对象,但它本身不是迭代器。要 iter() 包装一下,才得到真正的迭代器。

课程思政|科技自立自强 · 会用,更要看懂一层

for 循环看着简单,背后其实是 iter() 开传送带、next() 一件件取货。平时你不必手写这两步,但看懂了这一层,报错里蹦出的「迭代器」「可迭代对象」就不再是天书。工具可以先用别人的,原理必须长在自己手上——能把它内部怎么转讲清楚,才谈得上把技术真正学到家。

五、StopIteration:走到头了

迭代器没有「还剩几个」这个属性,它就是被一次次问。问到没有了怎么办?它会抛一个 StopIteration 异常,用这种方式告诉你「下班了」。

Python
it = iter([1])
print(next(it))
print(next(it))   # 只有一个元素,第二次就走到头了

运行后最后一行报错是:

输出
StopIteration

看到这个报错不要慌——这是正常机制,不是程序写坏了。它只在你手动调 next() 次数多了的时候才会冒出来;for 循环内部会接住这个异常,然后安静地结束:

Python
for x in [1]:
    print(x)
print('循环正常结束了,没有报错')

输出:

输出
1
循环正常结束了,没有报错

如果你想「取一个,没了也不要报错」,可以给 next() 一个默认值,写在第二个参数位置:

Python
it = iter([1, 2])
print('第 1 次:', next(it))
print('第 2 次:', next(it))
print('第 3 次:', next(it, '没有了'))   # 第二个参数是默认值

输出:

输出
第 1 次: 1
第 2 次: 2
第 3 次: 没有了
写「手动逐个取值」的代码时,养成给默认值的习惯:next(it, None)。这样取空了会得到 None,而不是把程序打断。
顺带预告:StopIteration 本质上是一个异常——程序出事时发出的「信号」。异常是什么、怎么稳稳接住它,下一篇《25-异常处理》专门讲。

六、生成器函数:把 return 换成 yield

上面说的迭代器,要么是 iter() 包装出来的,要么是 range() 这类内置的。那自己写的函数怎么变成迭代器?答案只有一个字:把 return 换成 yield。

Python
# 普通函数:一次算完,return 一个结果
def get_squares_list(n):
    result = []
    for i in range(1, n + 1):
        result.append(i * i)
    return result

print(get_squares_list(5))

输出:

输出
[1, 4, 9, 16, 25]
Python
# 生成器函数:只把 return 换成 yield,其余照旧
def get_squares(n):
    for i in range(1, n + 1):
        yield i * i

g = get_squares(5)
print('它是什么:', type(g))

输出:

输出
它是什么: <class 'generator'>

注意第二段打印出来的不是 [1, 4, 9, 16, 25],而是一个 generator 对象——因为调用生成器函数时,函数体一行都不会执行,它只是把「会按这个规则产东西」这件事记下来,还给你一个生成器。

如果你直接 print(g),看到的会是 <generator object get_squares at 0x…> 这类东西。那串 0x… 是内存地址,每次运行都不一样,所以生成器对象本身打印不出内容。想看内容只有一条路:把它取出来。

要拿值,就遍历它(或者 next() 它):

Python
def get_squares(n):
    for i in range(1, n + 1):
        yield i * i

for x in get_squares(5):
    print(x, end=' ')

输出:

输出
1 4 9 16 25 

需要时也可以直接变成列表,写法很自然:

Python
def get_squares(n):
    for i in range(1, n + 1):
        yield i * i

print(list(get_squares(5)))
print(sum(get_squares(5)))

输出:

输出
[1, 4, 9, 16, 25]
55
记法:看到函数体里有 yield,它就是生成器函数。调用它得到生成器,遍历生成器才真正执行函数体。

七、yield 会「暂停」:一步一步看清楚

yield 和 return 最容易混的一点是:return 交完值函数就结束了;yield 交完值函数只是暂停,下次要值时从暂停的地方接着往下跑。

看下面这段代码,注意打印顺序:

Python
def demo():
    print('第一段:开始干活')
    yield 1
    print('第二段:接着刚才停的地方')
    yield 2
    print('第三段:函数到底了')

g = demo()
print('--- 创建好了,但什么都没打印 ---')
print('拿到:', next(g))
print('拿到:', next(g))
try:
    next(g)
except StopIteration:
    print('--- 再取就没有了,生成器已经结束 ---')

输出:

输出
--- 创建好了,但什么都没打印 ---
第一段:开始干活
拿到: 1
第二段:接着刚才停的地方
拿到: 2
第三段:函数到底了
--- 再取就没有了,生成器已经结束 ---

把执行顺序读一遍:

阶段发生了什么屏幕上出现什么
g = demo()函数体没执行,只造出生成器什么都不打
第一个 next(g)跑到第一个 yield 就停下第一段… 和 拿到: 1
第二个 next(g)从暂停处继续,跑到第二个 yield第二段… 和 拿到: 2
第三个 next(g)继续跑到底,函数结束第三段…
「暂停」这件事是生成器最特别的地方:函数中途的状态(局部变量、跑到哪一行)全被保留着。这也是它「省内存」的根本原因。

AI 助手小贴士|for 循环和生成器的衔接原理,是这一篇的枢纽问题。

打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:用 for 循环遍历生成器时,for 是怎么知道该停的?请结合 iter、next 和 StopIteration 把完整流程讲一遍

八、惰性求值:省内存的真相

惰性求值是说:结果不是一次全算出来,而是用到才算。生成器就是这样干的。

下面这段代码造一千个数字,用 sys.getsizeof() 量一量两者各占多少内存:

Python
import sys
mylist = [x for x in range(1000)]
mygen = (x for x in range(1000))
print('列表占用:', sys.getsizeof(mylist), '字节')
print('生成器占用:', sys.getsizeof(mygen), '字节')

输出:

输出
列表占用: 8856 字节
生成器占用: 192 字节

差了四十多倍。而且这个差距会随数据量越拉越大——列表的占用随元素个数线性增长,生成器的占用基本不变(它就记着「跑到哪了」)。

把所有数字加起来,两种写法结果一样:

Python
print('列表版:', sum([x for x in range(1000)]))
print('生成器版:', sum(x for x in range(1000)))

输出:

输出
列表版: 499500
生成器版: 499500
用 sum()、max()、min()、any()、all() 这类「把所有元素汇总成一个值」的函数时,不要写方括号。
sum(x for x in ...) 比 sum([x for x in ...]) 少造一个列表,数据大时差别很明显。

课程思政|工匠精神与严谨求实 · 先量一量,再说省不省

有人说生成器省内存,光凭感觉可不行——用 sys.getsizeof() 把两种写法的占用各量一遍,数字立刻说话:四十多倍的差距就摆在那里。先量再改,别凭感觉,这是做工程最朴素的纪律。将来处理经济数据、跑统计模型,也请养成「让事实核对判断」的习惯,而不是「我觉得应该差不多」。

九、生成器表达式:把 [] 换成 ()

列表推导式已经学过,它长这样:[表达式 for 变量 in 可迭代对象]。

把它外面的方括号改成圆括号,就成了生成器表达式,其他一个字都不用动:

Python
nums = [1, 2, 3, 4, 5]
sq_list = [n * n for n in nums]     # 列表推导式
sq_gen = (n * n for n in nums)      # 生成器表达式
print(sq_list)
print(type(sq_gen))
print(list(sq_gen))

输出:

输出
[1, 4, 9, 16, 25]
<class 'generator'>
[1, 4, 9, 16, 25]

想知道手上拿到的是哪一种,用 type() 一看就清楚:

Python
print(type([x for x in range(3)]))
print(type((x for x in range(3))))

输出:

输出
<class 'list'>
<class 'generator'>

两者的对照:

列表推导式 […]生成器表达式 (…)
造出什么一个列表一个生成器
什么时候算立刻全部算好用到一个才算一个
占内存随元素个数增长基本固定
能反复用能只能一次
有 len()有没有
能取下标 g[0]能不能

带条件的写法完全一致:

Python
nums = [1, 2, 3, 4, 5, 6]
evens = (n for n in nums if n % 2 == 0)
print('偶数:', list(evens))
print('平方和:', sum(n * n for n in nums))

输出:

输出
偶数: [2, 4, 6]
平方和: 91
怎么选?结果要反复用、要取下标、要看长度——用列表推导式;只用来「过一遍就算完」(求和、写入、传给下一个环节)——用生成器表达式。

十、生成器只能用一次

这是新手最常踩的一个坑:生成器走完一遍就空了,第二次遍历什么都拿不到,而且不会报错。

Python
gen = (x * 2 for x in range(3))
print('第一次:', list(gen))
print('第二次:', list(gen))   # 空的!

输出:

输出
第一次: [0, 2, 4]
第二次: []

列表就没有这个问题:

Python
lst = [x * 2 for x in range(3)]
print('第一次:', lst)
print('第二次:', lst)

输出:

输出
第一次: [0, 2, 4]
第二次: [0, 2, 4]

所以下面这种「先统计、再打印」的写法是错的——第一遍已经把生成器用光了:

Python
gen = (n for n in [3, 1, 2])
print('个数:', sum(1 for _ in gen))   # 数了一遍,生成器空了
print('内容:', list(gen))             # 什么都拿不到

输出:

输出
个数: 3
内容: []

正确做法二选一:

  1. 要用多次,就先转成列表:data = list(gen),之后都拿 data 用;
  2. 要多次生成,就每次重新调用生成器函数:写 make_gen(),用几次就调几次。
Python
def make_gen():
    for n in [3, 1, 2]:
        yield n

print('个数:', sum(1 for _ in make_gen()))
print('内容:', list(make_gen()))

输出:

输出
个数: 3
内容: [3, 1, 2]
看到「生成器的第二次遍历是空的」,先别怀疑代码逻辑,想一句:它是不是已经被用掉了?

十一、几个实用场景

生成器不是为了炫技,它解决的是几个很实际的麻烦。

11.1 无限序列:想要多少有多少

列表装不下无限长的东西,生成器可以——因为它根本就不预先造。

Python
def fib():
    a, b = 0, 1
    while True:          # 注意:无限循环,但没有死机
        yield a
        a, b = b, a + b

g = fib()
for _ in range(8):
    print(next(g), end=' ')
print()

输出:

输出
0 1 1 2 3 5 8 13 

这段代码里的 while True 永远不会自己退出,但程序不会卡死——因为每次 yield 之后函数就暂停了,控制权回到了 for 这边。

11.2 大数据:一行一行处理,而不是一次全读进来

处理大文件时,一次性 readlines() 会把整个文件塞进内存。逐行处理就没这个问题。

Python
import io
data = '珍珠奶茶 12\n柠檬水 8\n# 这是一行备注\n美式 15\n'

# 生成器:一行一行地交出去,并顺手丢掉空行和备注行
def useful_lines(text):
    for line in text.splitlines():
        line = line.strip()
        if line and not line.startswith('#'):
            yield line

for line in useful_lines(data):
    print(line)

输出:

输出
珍珠奶茶 12
柠檬水 8
美式 15

11.3 管道:把几段处理串起来

每一段都是一个生成器,数据像流水线一样一格一格往下走。

Python
def read_orders(text):
    for line in text.splitlines():
        name, price = line.split()
        yield name, int(price)

def only_pricey(pairs):
    for name, price in pairs:
        if price >= 12:
            yield name, price

data = '珍珠奶茶 12\n柠檬水 8\n美式 15\n'
for name, price in only_pricey(read_orders(data)):
    print(name, price)

输出:

输出
珍珠奶茶 12
美式 15

11.4 拿前几个就停:itertools.islice

面对无限生成器,想「只取前 n 个」用 itertools.islice()——它也能用在普通序列上。

Python
from itertools import islice

def count_from(start):
    n = start
    while True:
        yield n
        n += 1

print(list(islice(count_from(10), 5)))

输出:

输出
[10, 11, 12, 13, 14]
场景为什么用生成器
读大文件 / 大数据不用一次性全装进内存
无限序列(斐波那契、自增编号)列表根本装不下
多步数据加工每步流式处理,写起来像流水线
只要前几个结果算到够了就停,不浪费

课程思政|数据安全与法治素养 · 需要多少,就读多少

处理大文件时,一次性 readlines() 会把整份数据塞进内存,而逐行读取只留下你当前要用的那一行。这背后是一条数据工作的铁律:能用多少取多少,不越界、不多拿。将来面对用户的隐私信息、单位的敏感数据,也请守住「最小必要」这条线——多读一遍、多存一份,都是风险。

十二、十个基础案例

下面十个例子都很短,建议每个都亲手敲一遍、跑一遍。它们覆盖了生成器最常见的用法。

1. 奶茶叫号倒计时

Python
def countdown(n):
    while n > 0:
        yield n
        n -= 1
    yield '请取餐!'

print(list(countdown(3)))

输出:

输出
[3, 2, 1, '请取餐!']

2. 生成 1~n 的平方

Python
def squares(n):
    for i in range(1, n + 1):
        yield i * i

print(list(squares(6)))

输出:

输出
[1, 4, 9, 16, 25, 36]

3. 斐波那契前 n 项

Python
from itertools import islice

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

print(list(islice(fib(), 10)))

输出:

输出
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

4. 只留下大额订单(生成器版筛选)

Python
orders = [12, 25, 30, 5, 88]
big = (n for n in orders if n >= 20)
print(list(big))
print('大单合计:', sum(n for n in orders if n >= 20))

输出:

输出
[25, 30, 88]
大单合计: 143

5. 用 enumerate 给加料编号

Python
def toppings():
    yield '珍珠'
    yield '椰果'
    yield '布丁'

for i, name in enumerate(toppings(), start=1):
    print(i, name)

输出:

输出
1 珍珠
2 椰果
3 布丁

6. 两张加料菜单接力:yield from

Python
def cold():
    yield '珍珠'
    yield '椰果'

def hot():
    yield '芋泥'
    yield '布丁'

def all_toppings():
    yield from cold()     # 把另一个可迭代对象的值一个个转出去
    yield from hot()

print(list(all_toppings()))

输出:

输出
['珍珠', '椰果', '芋泥', '布丁']

7. 算日均销量(不占内存的写法)

Python
def sales():
    for s in [88, 92, 75, 96, 81]:
        yield s

total = sum(sales())
count = sum(1 for _ in sales())
print('总销量:', total, '天数:', count, '日均:', total / count)

输出:

输出
总销量: 432 天数: 5 日均: 86.4

8. 生成器也能用 max() / min()

Python
def squares(n):
    for i in range(1, n + 1):
        yield i * i

print('最大:', max(squares(9)))
print('最小:', min(squares(9)))

输出:

输出
最大: 81
最小: 1

9. 手动 next() 一个个取

Python
g = (ch for ch in '奶茶真好喝')
print(next(g))
print(next(g))
print(next(g))
print('剩下的:', list(g))

输出:

输出
奶
茶
真
剩下的: ['好', '喝']

10. 生成器里带条件提前结束

Python
def paid_orders(amounts):
    for a in amounts:
        if a < 0:           # 负数代表退款,就此停
            return          # 生成器里用 return 就是「提前收工」
        yield a

print(list(paid_orders([3, 8, 5, -1, 9, 2])))

输出:

输出
[3, 8, 5]
第 10 个例子里 return 的作用是「就此结束,后面的不再产出」。注意生成器里的 return 不要带回值——那个值 for 循环拿不到,只会藏在 StopIteration 里,白白让人困惑。

AI 助手小贴士|生成器的省内存优势要用场景题体会。

打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:出 2 道生成器题:①写一个逐行读大文件、只输出含 ERROR 行的生成器;②用生成器表达式求 1~100 里能被 3 整除的数的平方和。等我写完点评

十三、常见坑

坑 1:把生成器当成列表用,第二遍就空了

Python
gen = (x for x in range(3))
print(list(gen))
print(list(gen))   # 空的

输出:

输出
[0, 1, 2]
[]

处理办法:要多次使用就先 data = list(gen);或者写成生成器函数,用一次调一次。

坑 2:函数里忘了写 yield,它就变成普通函数了

Python
def not_a_generator(n):
    for i in range(n):
        i * i            # 忘了 yield,算完就丢了

print(not_a_generator(3))

输出:

输出
None

它返回的是 None,而不是生成器。想看 type() 更容易发现:

Python
def a():
    return [1, 2]

def b():
    yield 1

print(type(a()))
print(type(b()))

输出:

输出
<class 'list'>
<class 'generator'>

坑 3:括号写错,本想省内存结果造了列表

Python
nums = [1, 2, 3]
print(type([n * 2 for n in nums]))    # 方括号:列表
print(type((n * 2 for n in nums)))    # 圆括号:生成器

输出:

输出
<class 'list'>
<class 'generator'>

只有一层表达式时,函数调用的括号可以省掉一层,看起来容易混:

Python
nums = [1, 2, 3]
print(sum([n * 2 for n in nums]))   # 列表 → 再求和
print(sum(n * 2 for n in nums))     # 直接给生成器,更省

输出:

输出
12
12

坑 4:生成器没有 len(),也不能取下标

Python
gen = (x for x in range(3))
print(gen[0])

运行后会报错(逐字照抄解释器):

报错
TypeError: 'generator' object is not subscriptable
Python
gen = (x for x in range(3))
print(len(gen))

运行后会报错(逐字照抄解释器):

报错
TypeError: object of type 'generator' has no len()

处理办法:先转列表 data = list(gen),再 len(data) / data[0]。或者用 sum(1 for _ in gen) 数个数(但数完生成器也空了)。

坑 5:yield 写在函数外面

输出
yield 1

运行后会报错(逐字照抄解释器):

报错
SyntaxError: 'yield' outside function

yield 只能写在函数体里。上面的写法连语法检查都过不了,程序根本不会开始运行。

坑 6:以为调用生成器函数就开始干活了

Python
def task():
    print('开始干活')
    yield 1

t = task()
print('这里还没有任何「开始干活」')

输出:

输出
这里还没有任何「开始干活」

要真正跑起来,必须去取它的值(next() 或 for)。

十四、报错手册

报错原文都是从本机解释器逐字抄下来的,对照着找就行。

报错原文(最后一行)什么情况怎么改
TypeError: 'generator' object is not subscriptable对生成器取下标,如 g[0]先 list(g) 再取下标
TypeError: object of type 'generator' has no len()对生成器调 len()先 list(g),或用 sum(1 for _ in g)
StopIterationnext() 取超了(迭代器已到头)给默认值 next(g, None),或改用 for
SyntaxError: 'yield' outside functionyield 写在了函数外面把 yield 挪进函数体
AttributeError: 'generator' object has no attribute 'append'把生成器当列表,调了 .append()生成器不能追加,先转列表
TypeError: unsupported operand type(s) for +: 'generator' and 'list'把生成器直接和列表相加两边都转成列表再加,或 list(g) + lst

下面这几条是「不报错,但结果不对」的,比报错更值得当心:

现象原因怎么改
第二次遍历生成器,得到空结果生成器只能走一遍,已被用完提前 list(),或每次重新调用生成器函数
调用生成器函数,函数体没执行生成器是「用时才算」用 next() 或 for 去取
函数返回 None,本该有数据函数里漏写了 yield补上 yield,用 type() 确认是生成器
print(gen) 看到一坨看不懂的东西生成器对象不显示内容想看内容就 print(list(gen))

十五、常见问题

Q:生成器到底比列表好在哪?
好在「不占内存」和「能表示无限序列」。数据小的时候两者没区别,数据大的时候列表可能直接内存不够。代价是生成器只能用一次、不能取下标。

Q:什么时候该用生成器?
三种情况最典型:① 数据量大或无限;② 结果只用一遍(求和、写文件、传给下一步);③ 处理流程是「读一点 → 加工一点 → 输出一点」。其余情况用列表更省心。

Q:生成器和迭代器是一回事吗?
不是。迭代器是一种「能被 next() 一个个取值」的东西;生成器是用 yield 或生成器表达式快速造出迭代器的办法。生成器一定是迭代器,反过来不成立。

Q:yield 和 return 能同时用吗?
能,但意义不同。return 在生成器里代表「提前收工」,后面不再产出。别用它带返回值——for 循环拿不到那个值。

Q:生成器能不能在中间「重置」?
不能。走到头就是走到头了,要重来只能重新调用一次生成器函数。

Q:我写的代码里 for 用得好好的,是不是可以不管这一篇?
日常能用 for 解决确实够用。但两点很值得掌握:一个是 sum(x for x in ...) 这种省内存的写法,另一个是「生成器只能用一次」这个坑——不知道它,就会在莫名其妙的地方拿到空结果。

AI 助手小贴士|「生成器到底省在哪」用大文件场景讲最透。让 AI 结合真实需求写示例。

打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:用「逐行读一个 2GB 的日志文件」这个例子,解释生成器相比先把所有行存进列表省在哪;然后写一个只输出包含 ERROR 的行的生成器,加中文注释

十六、随堂自测(12 题)

点击选项即可判分,答错的题会立刻显示解析。

已答 0 / 12 · 正确 0 —

1下面哪一个是生成器表达式?

  • A. (n * n for n in range(5))
  • B. [n * n for n in range(5)]
  • C. {n * n for n in range(5)}
  • D. range(5)
答案:A。圆括号包起来的是生成器表达式,它要一个才算一个。B 是列表推导式(立刻全部算好),C 是集合推导式(会去重且无序),D 是 range 对象,虽然也省内存但不是生成器表达式。

2对一个已经取完的生成器再调 next(),会怎样?

  • A. 返回 None
  • B. 一直等下去,程序卡住
  • C. 抛出 StopIteration
  • D. 自动从头重新开始
答案:C。迭代器到头时用 StopIteration 通知调用方,这是它的正常「下班信号」。for 循环内部会自动接住它,所以用 for 时看不到这个异常。想取空了也不报错,可以写 next(g, None) 给个默认值。

3在生成器函数里,yield 和 return 最根本的区别是?

  • A. yield 必须在循环里用
  • B. yield 只能返回数字,return 什么都能返回
  • C. 两者完全一样,只是写法不同
  • D. yield 交完值函数暂停,return 交完值函数结束
答案:D。暂停是生成器的核心:局部变量和「跑到哪一行」都被保留着,下次 next() 从暂停处接着跑。return 则是彻底结束,函数里的状态就没了。这个区别正是生成器能省内存的原因。

4生成器相对列表,最大的好处是?

  • A. 运行速度一定更快
  • B. 惰性求值,不用把结果全部装进内存
  • C. 能自动去掉重复元素
  • D. 能取下标和求长度
答案:B。生成器用到才算一个,所以内存占用基本固定,还能表示无限序列。它不保证更快(有时反而稍慢),也不会自动去重(那是集合的事),而且不能取下标、也没有 len()。

5下面哪个判断生成器「还剩多少个元素」的写法是对的?

  • A. len(gen)
  • B. gen.count()
  • C. sum(1 for _ in gen)
  • D. gen[-1]
答案:C。生成器没有 len(),调了会报 TypeError: object of type 'generator' has no len()。sum(1 for _ in gen) 能数出来,但要记住——数完生成器也被用光了,之后再用它拿到的是空结果。

6同一个生成器对象可以被完整遍历几次?

  • A. 一次,再遍历就是空的
  • B. 想遍历几次都行
  • C. 两次
  • D. 取决于元素个数
答案:A。生成器只能走一遍,这是它和列表最容易被忽略的差别,而且第二遍不报错、只是空结果。要多次使用就先 data = list(gen),或者把代码写成生成器函数,用一次调一次。

7调用一个含 yield 的函数时,函数体会:

  • A. 立刻从头执行,直到第一个 yield 停下
  • B. 一行都不执行,只返回一个生成器对象
  • C. 全部执行完再返回结果
  • D. 执行一半并返回中间结果
答案:B。调用生成器函数只是「记下生产规则」并返回生成器对象,函数体一行都不跑。只有开始对它取值(next() 或 for)时,才会执行到第一个 yield 然后暂停。

8想把一个生成器的前 3 个值取出来,最合适的是?

  • A. gen[0:3]
  • B. gen.head(3)
  • C. gen.len(3)
  • D. from itertools import islice; list(islice(gen, 3))
答案:D。生成器不支持切片(gen[0:3] 会报 not subscriptable),也没有 len() / head() 这些方法。itertools.islice 就是干这个的,面对无限生成器时尤其有用。

9sum([x * x for x in nums]) 和 sum(x * x for x in nums) 的区别是?

  • A. 完全一样,随便写哪个
  • B. 前者算得更快,后者结果可能不对
  • C. 后者少造一个列表,更省内存
  • D. 后者只能用于数字列表
答案:C。求和、求最值这类「把所有元素汇总成一个值」的场合,不需要先把结果装成列表。去掉方括号就少造一个中间列表,数据大时差别明显。两者结果完全相同。

10yield from other 的作用是?

  • A. 把 other 里的值一个个转交出去
  • B. 把另一个函数的返回值当成一个整体交出去
  • C. 导入另一个模块
  • D. 等于 return other
答案:A。yield from other 相当于对 other 写一个 for 循环再逐个 yield,常用于把一个生成器的内容接进另一个生成器。它不是导入、也不是 return。

11生成器函数的函数体里忘了写 yield,会怎样?

  • A. 报 SyntaxError
  • B. 函数变成普通函数,返回 None
  • C. 返回一个空的生成器
  • D. 程序卡住不动
答案:B。没有 yield 的函数就是普通函数,调用它只是正常执行完并返回 None,不会报错。用 type() 一看就知道:生成器显示 generator,普通函数返回的是它 return 的东西。

12关于「生成器只能用一次」,下面哪个做法是对的?

  • A. 把生成器存成变量,需要时反复遍历它
  • B. 用 len(gen) 判断它还剩没剩
  • C. 遍历前先调 gen.reset() 重置
  • D. 把生成器转成列表再用,或者每次重新调用生成器函数
答案:D。要用多次就先 data = list(gen),或者把逻辑写成生成器函数、每次要用就再调一次。生成器没有 reset(),也不能靠 len() 判断剩余——它压根没有 len()。