Kodewick
第 29 课,共 31 课目录

迭代器和生成器

第 29 课,共 31 课 · 第 6 部分 · 进阶 · 约 20 分钟

列表、字符串、字典、range、文件,你都用 for 循环遍历过。它们都能遍历,是因为都守同一个很简单的约定:「给我下一个」。这一课先看清这个约定,再学一个关键字 yield,用它写出自己的一串值。

可迭代对象和迭代器

for 循环能遍历的东西,都叫可迭代对象:列表、字符串、字典、range。把可迭代对象交给 iter(),得到一个迭代器:它一次给出一个东西。每调用一次 next(),就拿到下一个。

main.py
1lamps = ["北门", "磨坊", "桥头"]2it = iter(lamps)3print(next(it))4print(next(it))5print(next(it))
输出
北门
磨坊
桥头

迭代器记得自己走到了哪儿,而且只往前走。没有东西了,next() 就抛出 StopIteration:

main.py
1it = iter("ab")2print(next(it))3print(next(it))4print(next(it))
输出
a
b
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    print(next(it))
StopIteration

发生了什么: 程序在这里停下来了。

可迭代对象迭代器
例子列表、字符串、字典、rangeiter() 返回的东西;map、filter 对象
做什么能被遍历,遍历几次都行用 next() 一次给出一个
能从头再来吗能,每次循环都从头开始不能,用完了就一直是空的

for 循环背后做了什么

for 循环就是把 iter() 和 next() 藏了起来。它先要一个迭代器,每一轮之前调用一次 next(),碰到 StopIteration 就悄悄结束。自己动手写出来,同一个循环是这样的:

main.py
1lamps = ["北门", "磨坊", "桥头"]2it = iter(lamps)3while True:4    try:5        lamp = next(it)6    except StopIteration:7        break8    print("点亮", lamp)
输出
点亮 北门
点亮 磨坊
点亮 桥头

平时不用这么写,for 循环都替你做了。但这能说明为什么迭代器只能用一遍:第二个循环找同一个迭代器要东西,已经一个都不剩了。

先猜一猜

最后一行显示什么?

1it = iter([1, 2, 3])2for n in it:3    print(n)4print(list(it))

生成器和 yield

自己写迭代器挺麻烦。生成器函数替你把这件事做了:它就是一个普通的 def,里面有 yield。调用它并不运行函数体,而是返回一个生成器,生成器就是一种迭代器。每次 next(),函数体运行到下一个 yield,把那个值交出去,然后就停在那里。

main.py
1def countdown(n):2    print("开始")3    while n > 0:4        yield n5        n -= 16    print("结束")7 8gen = countdown(3)9print(next(gen))10print(next(gen))11print("回到主程序")12print(next(gen))
输出
开始
3
2
回到主程序
1

点单步看一看:每次 next(),当前行都跳回 countdown 里,到 yield 又跳出来。生成器停着的时候,局部变量 n 的值一直保留着。「结束」没有显示:要第四次 next(),函数体才会走到那一行。

平时一般直接遍历生成器,或者交给 list()、sum() 这些接受可迭代对象的函数:

main.py
1def countdown(n):2    while n > 0:3        yield n4        n -= 15 6for n in countdown(3):7    print(n)8print(list(countdown(5)))9print(sum(countdown(4)))
输出
3
2
1
[5, 4, 3, 2, 1]
10

直接打印生成器本身,只会看到 <generator object countdown at 0x...> 这样的东西,后面的内存地址每次都不一样。想看里面的值,就遍历它,或者套上 list()。

先猜一猜

三行按什么顺序显示?

1def lamps():2    yield "磨坊"3    print("中间")4    yield "桥头"5 6for name in lamps():7    print(name)

为什么用生成器

列表一下子把所有东西都放在内存里。生成器一个都不存:有人要了才算出一个值,交出去就忘了。要把十万个数的平方加起来,用不着先做一个有十万个平方的列表:

main.py
1def squares(limit):2    for n in range(limit):3        yield n * n4 5print(sum(squares(100_000)))
输出
333328333350000

sum() 要一个平方,加上,再要下一个。任何时候内存里都只有一个平方。换成列表,就是十万个数,还没开始加就得全部存好。遇到很大的文件或者没完没了的数据,这个差别决定了程序能不能跑起来。

没有尽头的生成器

生成器有人要才给,所以它不一定要停。下面这个永远在发灯的编号。这没问题,只要遍历它的那一方会停下来:

main.py
1def lamp_ids():2    n = 13    while True:4        yield f"L{n:03}"5        n += 16 7for lamp_id in lamp_ids():8    if lamp_id == "L004":9        break10    print(lamp_id)
输出
L001
L002
L003

千万别写 list(lamp_ids()):list() 会一直要下去,程序永远结束不了。只想要前几个,就用 itertools 模块(Python 自带)里的 islice,它是给迭代器用的切片。itertools.count 是现成的、没有尽头的计数器:

main.py
1import itertools2 3def lamp_ids():4    n = 15    while True:6        yield f"L{n:03}"7        n += 18 9print(list(itertools.islice(lamp_ids(), 3)))10print(list(itertools.islice(itertools.count(10, 5), 4)))
输出
['L001', 'L002', 'L003']
[10, 15, 20, 25]

生成器表达式

把列表推导式的方括号换成括号,就是生成器表达式。写法一样,但它不造列表,而是像生成器函数那样一个一个地给出值。

main.py
1loads = [3, 8, 2, 6, 11]2print(sum(n * 2 for n in loads))3print(max(len(word) for word in ["矿石", "灯", "小镇仓库"]))4print(any(n > 10 for n in loads))
输出
60
4
True

any() 只要有一项为真就给出 True,所以最后一行问的是有没有哪一车超过 10。生成器表达式是唯一的实参时,它的括号可以和函数的括号共用:sum(n * 2 for n in loads)。和所有迭代器一样,生成器表达式只能用一遍:

main.py
1doubled = (n * 2 for n in [1, 2, 3])2print(sum(doubled))3print(sum(doubled))
输出
12
0

要的是列表本身,比如要打印、要用下标、要遍历两遍,就用方括号。值直接交给 sum()、max()、any() 或者一个循环,就用括号。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

一次一个

用 iter() 和 next() 显示 ores 的前两个,一个一行。不要用循环。

要显示
铁
金
exercise1.py
1ores = ["铁", "金", "铜"]2# 在这里写代码
输出
点「运行」或按 Ctrl+Enter
练习 2

偶数

把生成器 evens 写完,让它交出从 0 到 limit(包括 limit)的偶数。最后一行就会显示下面的列表。

要显示
[0, 2, 4, 6, 8, 10]
exercise2.py
1def evens(limit):2    pass3 4print(list(evens(10)))
输出
点「运行」或按 Ctrl+Enter
练习 3

重的那几车

显示大于 5 的那些载重加起来是多少。用 sum() 加生成器表达式,不要用列表。

要显示
25
exercise3.py
1loads = [3, 8, 2, 6, 11]2print(sum(loads))
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 可迭代对象能被遍历;iter() 把它变成迭代器,next() 一次拿一个。
  • 迭代器用完了,next() 抛出 StopIteration;for 循环接住它,然后结束。
  • 有 yield 的函数是生成器函数:每次 next() 让它运行到下一个 yield,停在那里。
  • 生成器有人要才算出值,所以省内存,甚至可以没有尽头;用 break 或 itertools.islice 让它停下。
  • (x for x in ...) 是生成器表达式;和所有迭代器一样,只能用一遍。