一、为什么还需要「生成器」
前面学过列表推导式,它能一行把一串结果全算出来。但「全算出来」这件事本身有个前提:结果装得下。
如果要做的是「一百万个数的平方和」,列表推导式会先把一百万个数字真的造出来放进内存,再求和。而生成器(generator)的想法是:别急着造,用到哪个才算哪个。
先看两个写法。下面这两段代码输出一模一样,但干活的方式完全不同:
# 写法一:先把 1~5 的平方全算出来,装进列表
squares = [n * n for n in range(1, 6)]
for x in squares:
print(x)
输出:
1
4
9
16
25
# 写法二:生成器——要用一个才算一个
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)。
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} | 顺序不保证 |
| range | range(5) | 边算边给,不占内存 |
| 文件对象 | open(...) | 一行一行地给 |
| 生成器 | (x for x in ...) | 要一个才给一个 |
for 后面的、能写进 sum()/max()/list() 括号里的,基本都是可迭代对象。但要注意:可迭代对象 ≠ 迭代器。这个区别下一节就讲清楚。
三、迭代器:能一个个往外吐
迭代器(iterator)是一种更「纯粹」的东西:它自己就是一条数据通道,你问它要,它给你一个;再问,再给一个。
怎么认出一个迭代器?两种办法:
- 它能被
next()调用,每次吐出一个值; - 它自己也能被
for遍历(因为迭代器本身就是可迭代对象)。
colors = ['红', '绿', '蓝']
it = iter(colors) # 把列表「包装」成迭代器
print('第一个:', next(it))
print('第二个:', next(it))
print('第三个:', next(it))
输出:
第一个: 红
第二个: 绿
第三个: 蓝
这里出现了两个新面孔:iter() 和 next()。它们的配合方式是:
| 函数 | 作用 | 一句话 |
|---|---|---|
iter(x) | 把可迭代对象 x 变出一个迭代器 | 开一条传送带 |
next(it) | 从迭代器 it 里取出下一个值 | 从传送带上拿一件 |
列表和迭代器的区别在这里:列表是「仓库」,迭代器是「取货顺序」。列表同时装着全部数据,你可以反复从头看;迭代器只记得「下一个该给谁」,给过就过去了。
# 列表可以反复遍历,迭代器不行
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 在背后偷偷做了三件事。现在把它拆开看:
- 先调用
iter(某个列表),拿到一个迭代器; - 反复调用
next(迭代器),每拿到一个值就把这个值给x,执行一遍循环体; - 当
next()说「没有了」时,循环安静地结束,不报错。
所以下面这两段代码是完全等价的:
# 平时这么写
for x in ['a', 'b', 'c']:
print('拿到', x)
输出:
拿到 a
拿到 b
拿到 c
# 手写一遍它背后做的事
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 里有两个专门用来「验身份」的东西,这里先混个脸熟。
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))
输出:
str 可迭代? True
list 可迭代? True
list 是迭代器? False
iter(list) 是迭代器? True
结论很清楚:列表是可迭代对象,但它本身不是迭代器。要 iter() 包装一下,才得到真正的迭代器。
课程思政|科技自立自强 · 会用,更要看懂一层
for 循环看着简单,背后其实是 iter() 开传送带、next() 一件件取货。平时你不必手写这两步,但看懂了这一层,报错里蹦出的「迭代器」「可迭代对象」就不再是天书。工具可以先用别人的,原理必须长在自己手上——能把它内部怎么转讲清楚,才谈得上把技术真正学到家。
五、StopIteration:走到头了
迭代器没有「还剩几个」这个属性,它就是被一次次问。问到没有了怎么办?它会抛一个 StopIteration 异常,用这种方式告诉你「下班了」。
it = iter([1])
print(next(it))
print(next(it)) # 只有一个元素,第二次就走到头了
运行后最后一行报错是:
StopIteration
看到这个报错不要慌——这是正常机制,不是程序写坏了。它只在你手动调 next() 次数多了的时候才会冒出来;for 循环内部会接住这个异常,然后安静地结束:
for x in [1]:
print(x)
print('循环正常结束了,没有报错')
输出:
1
循环正常结束了,没有报错
如果你想「取一个,没了也不要报错」,可以给 next() 一个默认值,写在第二个参数位置:
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。
# 普通函数:一次算完,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]
# 生成器函数:只把 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() 它):
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
需要时也可以直接变成列表,写法很自然:
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 交完值函数只是暂停,下次要值时从暂停的地方接着往下跑。
看下面这段代码,注意打印顺序:
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() 量一量两者各占多少内存:
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 字节
差了四十多倍。而且这个差距会随数据量越拉越大——列表的占用随元素个数线性增长,生成器的占用基本不变(它就记着「跑到哪了」)。
把所有数字加起来,两种写法结果一样:
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 可迭代对象]。
把它外面的方括号改成圆括号,就成了生成器表达式,其他一个字都不用动:
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() 一看就清楚:
print(type([x for x in range(3)]))
print(type((x for x in range(3))))
输出:
<class 'list'>
<class 'generator'>
两者的对照:
列表推导式 […] | 生成器表达式 (…) | |
|---|---|---|
| 造出什么 | 一个列表 | 一个生成器 |
| 什么时候算 | 立刻全部算好 | 用到一个才算一个 |
| 占内存 | 随元素个数增长 | 基本固定 |
| 能反复用 | 能 | 只能一次 |
有 len() | 有 | 没有 |
能取下标 g[0] | 能 | 不能 |
带条件的写法完全一致:
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
十、生成器只能用一次
这是新手最常踩的一个坑:生成器走完一遍就空了,第二次遍历什么都拿不到,而且不会报错。
gen = (x * 2 for x in range(3))
print('第一次:', list(gen))
print('第二次:', list(gen)) # 空的!
输出:
第一次: [0, 2, 4]
第二次: []
列表就没有这个问题:
lst = [x * 2 for x in range(3)]
print('第一次:', lst)
print('第二次:', lst)
输出:
第一次: [0, 2, 4]
第二次: [0, 2, 4]
所以下面这种「先统计、再打印」的写法是错的——第一遍已经把生成器用光了:
gen = (n for n in [3, 1, 2])
print('个数:', sum(1 for _ in gen)) # 数了一遍,生成器空了
print('内容:', list(gen)) # 什么都拿不到
输出:
个数: 3
内容: []
正确做法二选一:
- 要用多次,就先转成列表:
data = list(gen),之后都拿data用; - 要多次生成,就每次重新调用生成器函数:写
make_gen(),用几次就调几次。
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 无限序列:想要多少有多少
列表装不下无限长的东西,生成器可以——因为它根本就不预先造。
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() 会把整个文件塞进内存。逐行处理就没这个问题。
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 管道:把几段处理串起来
每一段都是一个生成器,数据像流水线一样一格一格往下走。
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()——它也能用在普通序列上。
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. 奶茶叫号倒计时
def countdown(n):
while n > 0:
yield n
n -= 1
yield '请取餐!'
print(list(countdown(3)))
输出:
[3, 2, 1, '请取餐!']
2. 生成 1~n 的平方
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 项
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. 只留下大额订单(生成器版筛选)
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 给加料编号
def toppings():
yield '珍珠'
yield '椰果'
yield '布丁'
for i, name in enumerate(toppings(), start=1):
print(i, name)
输出:
1 珍珠
2 椰果
3 布丁
6. 两张加料菜单接力:yield from
def cold():
yield '珍珠'
yield '椰果'
def hot():
yield '芋泥'
yield '布丁'
def all_toppings():
yield from cold() # 把另一个可迭代对象的值一个个转出去
yield from hot()
print(list(all_toppings()))
输出:
['珍珠', '椰果', '芋泥', '布丁']
7. 算日均销量(不占内存的写法)
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()
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() 一个个取
g = (ch for ch in '奶茶真好喝')
print(next(g))
print(next(g))
print(next(g))
print('剩下的:', list(g))
输出:
奶
茶
真
剩下的: ['好', '喝']
10. 生成器里带条件提前结束
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]
return 的作用是「就此结束,后面的不再产出」。注意生成器里的 return 不要带回值——那个值 for 循环拿不到,只会藏在 StopIteration 里,白白让人困惑。AI 助手小贴士|生成器的省内存优势要用场景题体会。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:出 2 道生成器题:①写一个逐行读大文件、只输出含 ERROR 行的生成器;②用生成器表达式求 1~100 里能被 3 整除的数的平方和。等我写完点评
十三、常见坑
坑 1:把生成器当成列表用,第二遍就空了
gen = (x for x in range(3))
print(list(gen))
print(list(gen)) # 空的
输出:
[0, 1, 2]
[]
处理办法:要多次使用就先 data = list(gen);或者写成生成器函数,用一次调一次。
坑 2:函数里忘了写 yield,它就变成普通函数了
def not_a_generator(n):
for i in range(n):
i * i # 忘了 yield,算完就丢了
print(not_a_generator(3))
输出:
None
它返回的是 None,而不是生成器。想看 type() 更容易发现:
def a():
return [1, 2]
def b():
yield 1
print(type(a()))
print(type(b()))
输出:
<class 'list'>
<class 'generator'>
坑 3:括号写错,本想省内存结果造了列表
nums = [1, 2, 3]
print(type([n * 2 for n in nums])) # 方括号:列表
print(type((n * 2 for n in nums))) # 圆括号:生成器
输出:
<class 'list'>
<class 'generator'>
只有一层表达式时,函数调用的括号可以省掉一层,看起来容易混:
nums = [1, 2, 3]
print(sum([n * 2 for n in nums])) # 列表 → 再求和
print(sum(n * 2 for n in nums)) # 直接给生成器,更省
输出:
12
12
坑 4:生成器没有 len(),也不能取下标
gen = (x for x in range(3))
print(gen[0])
运行后会报错(逐字照抄解释器):
TypeError: 'generator' object is not subscriptable
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:以为调用生成器函数就开始干活了
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) |
StopIteration | next() 取超了(迭代器已到头) | 给默认值 next(g, None),或改用 for |
SyntaxError: 'yield' outside function | yield 写在了函数外面 | 把 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 题)
点击选项即可判分,答错的题会立刻显示解析。
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)
2对一个已经取完的生成器再调 next(),会怎样?
- A. 返回
None - B. 一直等下去,程序卡住
- C. 抛出
StopIteration - D. 自动从头重新开始
StopIteration 通知调用方,这是它的正常「下班信号」。for 循环内部会自动接住它,所以用 for 时看不到这个异常。想取空了也不报错,可以写 next(g, None) 给个默认值。3在生成器函数里,yield 和 return 最根本的区别是?
- A.
yield必须在循环里用 - B.
yield只能返回数字,return什么都能返回 - C. 两者完全一样,只是写法不同
- D.
yield交完值函数暂停,return交完值函数结束
next() 从暂停处接着跑。return 则是彻底结束,函数里的状态就没了。这个区别正是生成器能省内存的原因。4生成器相对列表,最大的好处是?
- A. 运行速度一定更快
- B. 惰性求值,不用把结果全部装进内存
- C. 能自动去掉重复元素
- D. 能取下标和求长度
len()。5下面哪个判断生成器「还剩多少个元素」的写法是对的?
- A.
len(gen) - B.
gen.count() - C.
sum(1 for _ in gen) - D.
gen[-1]
len(),调了会报 TypeError: object of type 'generator' has no len()。sum(1 for _ in gen) 能数出来,但要记住——数完生成器也被用光了,之后再用它拿到的是空结果。6同一个生成器对象可以被完整遍历几次?
- A. 一次,再遍历就是空的
- B. 想遍历几次都行
- C. 两次
- D. 取决于元素个数
data = list(gen),或者把代码写成生成器函数,用一次调一次。7调用一个含 yield 的函数时,函数体会:
- A. 立刻从头执行,直到第一个
yield停下 - B. 一行都不执行,只返回一个生成器对象
- C. 全部执行完再返回结果
- D. 执行一半并返回中间结果
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))
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. 后者只能用于数字列表
10yield from other 的作用是?
- A. 把
other里的值一个个转交出去 - B. 把另一个函数的返回值当成一个整体交出去
- C. 导入另一个模块
- D. 等于
return other
yield from other 相当于对 other 写一个 for 循环再逐个 yield,常用于把一个生成器的内容接进另一个生成器。它不是导入、也不是 return。11生成器函数的函数体里忘了写 yield,会怎样?
- A. 报
SyntaxError - B. 函数变成普通函数,返回
None - C. 返回一个空的生成器
- D. 程序卡住不动
yield 的函数就是普通函数,调用它只是正常执行完并返回 None,不会报错。用 type() 一看就知道:生成器显示 generator,普通函数返回的是它 return 的东西。12关于「生成器只能用一次」,下面哪个做法是对的?
- A. 把生成器存成变量,需要时反复遍历它
- B. 用
len(gen)判断它还剩没剩 - C. 遍历前先调
gen.reset()重置 - D. 把生成器转成列表再用,或者每次重新调用生成器函数
data = list(gen),或者把逻辑写成生成器函数、每次要用就再调一次。生成器没有 reset(),也不能靠 len() 判断剩余——它压根没有 len()。