35. 发电机
35. 发电机
生成器是可恢复的函数。正常函数以一个返回值开始、运行和结束。生成器可以启动、产生一个值、挂起其帧、稍后从同一指令位置恢复、产生另一个值,然后重复直到完成。
生成器函数是包含以下内容的任何函数体yield。python id="j7t4va" def numbers(): yield 1 yield 2 yield 3 调用此函数不会立即运行主体。python id="oyb9ik" g = numbers() 该调用创建一个生成器对象。当发电机恢复时,主体启动:python id="9sgmxp" print(next(g)) print(next(g)) print(next(g)) 输出:text id="wj3i3f" 1 2 3 在最后一个值之后,下一个简历将引发StopIteration。
35.1 生成器函数与生成器对象
生成器函数是用以下定义的可调用函数def。
生成器对象是调用生成器函数时返回的可恢复迭代器。```python id="nckwxg" def gen(): yield 1
print(gen)
print(gen())
从概念上讲:text id="hfh6hz"
gen
function object
gen()
generator object
suspended execution state
code object
frame or frame-like state
这与普通函数不同:python id="wsroz3"
def f():
return 1
f()
```呼唤f立即运行主体并返回1。
呼唤gen返回一个可以稍后运行主体的对象。
35.2yield这yield表达式为生成器的调用者生成一个值并暂停执行。```python id="cwhj6e"
def gen():
x = 10
yield x
x = 20
yield x
执行顺序:text id="jvg4tx"
create generator object
resume generator
x = 10
yield 10
suspend
resume generator x = 20 yield 20 suspend
resume generator
finish function
raise StopIteration
```局部变量x由于生成器保持其执行状态,因此在挂起后仍然存在。
35.3 生成器是迭代器
生成器对象实现迭代器协议。```python id="mtd1nu" g = numbers()
print(iter(g) is g)
print(next(g))
发电机具有:text id="vlpm43"
iter
next
send
throw
close
这`for`循环之所以有效,是因为生成器是迭代器:python id="9z52dp"
for x in numbers():
print(x)
从概念上讲:text id="rdvvmh"
it = iter(numbers())
while True: try: x = next(it) except StopIteration: break print(x)
生成器必须在恢复之间保留执行状态。
该状态包括:```text id="9ta9ii"
code object
instruction position
local variables
value stack
exception state
closure cells
running state
finished state
```例子:```python id="h463cg"
def gen():
a = 1
b = 2
yield a
yield b
```第一个之后`yield`,生成器必须记住:```text id="oj7swc"
a = 1
b = 2
next instruction is after first yield
```这就是发电机与框架紧密连接的原因。
## 35.5 普通函数框架与生成器框架
正常的功能框通常在返回后消失。```python id="lzuq79"
def f():
x = 1
return x
```后`f()`返回后,帧可以被清除。
发电机框架在悬挂时仍然存在。```python id="6zyz25"
def gen():
x = 1
yield x
```后`next(gen())`达到`yield`,该帧无法清除,因为它可能稍后恢复。
|特色|功能正常|发电机|
|---|---|---|
|调用立即运行正文 |是的 |没有 |
|可以暂停|没有 |是的 |
|让步后保留当地人|没有 |是的 |
|返回一个最终结果 |是的 |最终结果变成`StopIteration.value`|
|实现迭代器协议 |没有 |是的 |
|框架寿命|通常通话时长 |直至完成或关闭 |
## 35.6`next()`
`next(g)`恢复发电机。```python id="07krhu"
def gen():
yield "a"
yield "b"
g = gen()
print(next(g))
print(next(g))
```执行:```text id="tuypws"
next(g)
resume at start
yield "a"
return "a" to caller
next(g)
resume after first yield
yield "b"
return "b" to caller
```生成器对象记录调用之间的指令位置。
## 35.7 完成和`StopIteration`当生成器完成时,它会升高`StopIteration`。```python id="suekt2"
def gen():
yield 1
g = gen()
print(next(g))
print(next(g))
```第二个`next(g)`提高`StopIteration`。
生成器可以通过以下方式完成:```text id="6n5ndu"
falling off the end
executing return
raising an exception
being closed
```从终点掉下来就等于回来`None`。```python id="51z1x1"
def gen():
yield 1
```后`yield 1`,函数到达末尾。下一份简历加薪`StopIteration`。
## 35.8`return`在发电机中
发电机可以使用`return value`。```python id="lbrk4s"
def gen():
yield 1
return 99
```返回值变为`value`的属性`StopIteration`。```python id="tyi2fu"
g = gen()
print(next(g))
try:
next(g)
except StopIteration as exc:
print(exc.value)
```输出:```text id="dzo3b2"
1
99
```一个`for`循环忽略最后的`StopIteration.value`。
## 35.9`yield`是一个表达式`yield`可以通过接收值`send`。```python id="vpynq0"
def gen():
x = yield "ready"
yield x
```使用:```python id="5cofsb"
g = gen()
print(next(g))
print(g.send(42))
```执行:```text id="fafbmz"
next(g)
runs until yield "ready"
returns "ready"
g.send(42)
resumes generator
yield expression evaluates to 42
x = 42
yield x
```所以`yield`两者都发送一个值并且可以接收一个值返回。
## 35.10 启动生成器`send`新创建的生成器尚未到达其第一个`yield`。
因此,第一份简历必须使用`next(g)`或者`g.send(None)`。```python id="ltjgnn"
g = gen()
g.send(None)
```发送非`None`刚启动的发电机的值是一个错误,因为没有暂停`yield`表达来接收它。```python id="w4yvh2"
g = gen()
g.send(42)
```这引发了`TypeError`。
## 35.11`throw`
`throw`通过在暂停时引发异常来恢复生成器`yield`。```python id="1i4m5p"
def gen():
try:
yield "ready"
except ValueError:
yield "handled"
g = gen()
print(next(g))
print(g.throw(ValueError))
```执行:```text id="h3sq71"
next(g)
yield "ready"
g.throw(ValueError)
resume at yield by raising ValueError
except ValueError catches it
yield "handled"
throw让调用者将异常注入生成器。
35.12close
close要求生成器终止。```python id="4p6xj9"
def gen():
try:
yield 1
finally:
print("cleanup")
g = gen()
next(g)
g.close()
```关闭注入GeneratorExit进入发电机。这finally块运行。
发电机在关闭时不应产生正常值。如果是这样,CPython 会引发RuntimeError。python id="m1acqp" def bad(): try: yield 1 finally: yield 2 呼唤close()在第一次屈服之后会导致错误,因为生成器在关闭期间产生了屈服。
35.13 发电机状态
生成器可以处于多种状态:text id="spbcno" created running suspended closed 使用inspect:
import inspect
def gen():
yield 1
g = gen()
print(inspect.getgeneratorstate(g))
next(g)
print(inspect.getgeneratorstate(g))
try:
next(g)
except StopIteration:
pass
print(inspect.getgeneratorstate(g))
```典型状态:```text id="51wa06"
GEN_CREATED
GEN_SUSPENDED
GEN_CLOSED
```发电机在运行时无法恢复。
## 35.14 重入保护
发电机无法重新进入。```python id="h0ak06"
def gen():
yield next(g)
g = gen()
next(g)
```这试图恢复`g`尽管`g`已经在运行了。 CPython 引发错误。
生成器有一个运行标志以防止破坏其帧状态。
从概念上讲:```text id="lm6k96"
if generator is already executing:
raise ValueError or RuntimeError depending on context
```这可以保护悬挂的框架和堆栈。
## 35.15 生成器字节码
生成器函数编译为标记为生成器的代码对象。```python id="af0twx"
def gen():
yield 1
```调用该函数会创建一个生成器对象,而不是执行帧以完成。
概念性指令序列:```text id="n6w1a4"
LOAD_CONST 1
YIELD_VALUE
RESUME
LOAD_CONST None
RETURN_VALUE
```确切的字节码因 Python 版本而异。
关键指令是`YIELD_VALUE`,它向调用者发送一个值并暂停执行。
## 35.16`yield from`
`yield from`委托给另一个迭代器或生成器。```python id="uxya55"
def outer():
yield from inner()
```它大致相当于:```python id="37kb2t"
for value in inner():
yield value
```但它也转发:```text id="hq74n9"
send
throw
close
StopIteration.value
```这使得`yield from`比简单的循环更强大。
## 35.17 委托`yield from`例子:```python id="9ui4p5"
def inner():
yield 1
yield 2
return 99
def outer():
result = yield from inner()
yield result
print(list(outer()))
```输出:```text id="4ncupl"
[1, 2, 99]
```返回值`inner`成为的结果`yield from`表达于`outer`。
从概念上讲:```text id="02qghi"
outer delegates to inner
inner yields 1
outer yields 1 to caller
inner yields 2
outer yields 2 to caller
inner returns 99 via StopIteration.value
yield from expression evaluates to 99
outer yields 99
```## 35.18`yield from`和`send`
`yield from`转发调用者发送的值。```python id="nfxupw"
def inner():
x = yield "inner ready"
yield x
def outer():
yield from inner()
g = outer()
print(next(g))
print(g.send(42))
```输出:```text id="9lagzv"
inner ready
42
```这`send(42)`达到暂停的`yield`里面`inner`。
## 35.19`yield from`和例外情况`yield from`也转发异常。```python id="nxzr78"
def inner():
try:
yield "ready"
except ValueError:
yield "handled"
def outer():
yield from inner()
g = outer()
print(next(g))
print(g.throw(ValueError))
```异常被抛出到`inner`,不直接处理`outer`,除非委托结束或内部缺少适当的处理程序。
## 35.20 生成器表达式
生成器表达式创建一个类似生成器的对象。```python id="gkiij5"
squares = (x * x for x in range(10))
```它很懒。值按要求计算。```python id="ey0d2q"
print(next(squares))
print(next(squares))
```生成器表达式有其自己的隐式类似函数的作用域。```python id="wkic1b"
x = 100
g = (x for x in range(3))
print(x)
```外层`x`遗迹`100`。
## 35.21 列表推导式与生成器表达式
列表理解立即构建整个列表。```python id="h6kqv5"
xs = [x * x for x in range(10)]
```生成器表达式延迟生成值。```python id="j8cabg"
g = (x * x for x in range(10))
```|特色|列表理解 |生成器表达式 |
|---|---|---|
|评价|渴望|懒惰|
|结果 |列表 |类似生成器的迭代器 |
|内存|存储所有结果 |存储执行状态 |
|可重复使用|是的,列表可以迭代多次 |不,发电机已消耗 |
|范围 |自己的领悟范围|自有发电机范围 |
## 35.22 一次性迭代
生成器是一次性迭代器。```python id="mj534b"
g = (x for x in range(3))
print(list(g))
print(list(g))
```输出:```text id="qbh3cb"
[0, 1, 2]
[]
```一旦耗尽,发电机就会保持耗尽状态。
这与列表等容器不同:```python id="b4xrxl"
xs = [0, 1, 2]
print(list(xs))
print(list(xs))
```列表每次都会创建一个新的迭代器。生成器是它自己的迭代器。
## 35.23 惰性求值
生成器按需计算值。```python id="c1xawr"
def read_lines(path):
with open(path) as f:
for line in f:
yield line.rstrip("\n")
```这不会将整个文件读入内存。它一次读取并生成一行。
延迟执行对于以下情况很有用:```text id="dhu35c"
large files
streams
pipelines
infinite sequences
expensive computations
early stopping
```例子:```python id="zb1yck"
def count():
n = 0
while True:
yield n
n += 1
```该生成器代表一个无限序列。
## 35.24 管道样式
生成器自然地组合。```python id="j6opqr"
def numbers(path):
with open(path) as f:
for line in f:
yield int(line)
def positive(xs):
for x in xs:
if x > 0:
yield x
def squared(xs):
for x in xs:
yield x * x
```使用:```python id="1rj6pt"
pipeline = squared(positive(numbers("data.txt")))
for x in pipeline:
print(x)
```每个阶段都源自前一个阶段。不需要完整的中间列表。
## 35.25 生成器清理
管理资源的生成器应该使用`try/finally`或上下文管理器。```python id="8m79yy"
def lines(path):
f = open(path)
try:
for line in f:
yield line
finally:
f.close()
```如果发电机在耗尽之前关闭,则`finally`块运行。
更好的形式:```python id="yyr3qh"
def lines(path):
with open(path) as f:
for line in f:
yield line
```这`with`语句被编译成与生成器关闭一起使用的清理逻辑。
## 35.26 生成器和资源泄漏
暂停的生成器可以使资源保持活动状态。```python id="2s3f45"
def gen():
f = open("data.txt")
yield f.readline()
f.close()
```如果调用者在第一个值之后停止并且从未关闭生成器,则文件可能会保持打开状态,直到收集生成器。
使用`with`或显式关闭:```python id="lz764o"
g = gen()
next(g)
g.close()
```资源所有权应该在生成器代码中明确。
## 35.27 中的生成器和异常`finally`如果引发清理代码,则该异常会在生成器关闭或终结期间传播。```python id="xdp983"
def gen():
try:
yield 1
finally:
raise RuntimeError("cleanup failed")
```呼唤`g.close()`启动发电机后升高`RuntimeError`。
如果没有正常的调用者上下文,则终结时异常可能会报告为不可引发。
## 35.28 生成器内存保留
暂停的生成器保持其局部变量处于活动状态。```python id="7evyaw"
def gen():
data = bytearray(100_000_000)
yield 1
return len(data)
g = gen()
next(g)
```第一个之后`yield`, `data`保持活动状态,因为生成器可以恢复并使用它。
保留链:```text id="a05d71"
generator object
suspended frame
local data
```要释放内存,请耗尽或关闭生成器,或者避免在产量之间保留大量局部变量。
## 35.29 清除大型局部变量
如果屈服后不需要大对象,请在屈服之前或长时间暂停之前清除它。```python id="mdphyz"
def gen():
data = bytearray(100_000_000)
result = process(data)
data = None
yield result
```这使得大物体可以在悬挂之前被释放。
生成器框架仍然存在,但不再引用`data`。
## 35.30 发电机和`for`循环
发电机经常出现在`for`环形:```python id="ywz49k"
for value in gen():
use(value)
```循环中反复调用`next()`直到`StopIteration`。
如果循环提前退出`break`,生成器对象可能会变得无法访问,然后又关闭。但依赖立即最终确定是特定于实现的。当清理时间很重要时使用上下文管理器。
## 35.31 基于生成器的上下文管理器
的`contextlib.contextmanager`装饰器将生成器变成上下文管理器。```python id="vtcps7"
from contextlib import contextmanager
@contextmanager
def managed():
print("enter")
try:
yield "value"
finally:
print("exit")
with managed() as value:
print(value)
```生成器只产生一次。
从概念上讲:```text id="2vq7qx"
__enter__
run generator until yield
return yielded value
__exit__
resume generator to run cleanup
```如果 with-body 引发,异常将被抛出到生成器中`yield`。
## 35.32 生成器协议方法
生成器对象支持这些重要的方法:
|方法|意义|
|---|---|
|`__next__()`|恢复并发送`None` |
| `send(value)`|恢复并将值发送到当前`yield` |
| `throw(exc)`|通过在当前引发异常来恢复`yield` |
| `close()`|注入`GeneratorExit`并关闭|
|`__iter__()`|返回自我 |`next(g)`来电`g.__next__()`。`g.__next__()`相当于`g.send(None)`对于悬挂式发电机。
## 35.33 生成器属性
生成器对象公开有用的属性。```python id="yu9p0l"
def gen():
yield 1
g = gen()
print(g.gi_code)
print(g.gi_frame)
print(g.gi_running)
```常见属性包括:
|属性 |意义|
|---|---|
|`gi_code`|代码对象|
|`gi_frame`|框架或`None`关闭时|
|`gi_running`|当前是否正在执行 |
|`gi_yieldfrom`|当前委托迭代器`yield from`,如果有的话 |
这些是 CPython 级别的内省钩子,可能会暴露实现细节。
## 35.34 生成器和回溯
如果生成器引发异常,则回溯包括生成器帧。```python id="u2d2xz"
def gen():
yield 1
1 / 0
g = gen()
next(g)
next(g)
```第二个`next(g)`在发电机内部恢复并引发`ZeroDivisionError`。
回溯指向内部失败的行`gen`。
与任何其他 Python 框架一样,生成器框架是回溯的一部分。
## 35.35 发电机和`StopIteration`转型
在发电机内,意外`StopIteration`很危险。```python id="mmdpxy"
def gen():
next(iter([]))
yield 1
```内部`next(iter([]))`提高`StopIteration`。
现代 Python 将其转换为`RuntimeError`当它逃离发电机本体时。这可以防止意外终止看起来像是正常的生成器完成。
如果需要的话,正确的代码应该明确地捕获它:```python id="tmjsqq"
def gen():
try:
value = next(iter([]))
except StopIteration:
return
yield value
```## 35.36 生成器和异步
生成器与协程和异步生成器相关,但又有所不同。
|建设|关键词 |生产 |
|---|---|---|
|发电机|`def`和`yield`|生成器对象 |
|协程 |`async def`|协程对象 |
|异步生成器 |`async def`和`yield`|异步生成器对象 |
普通发电机使用`next`, `send`, `throw`, 和`close`。
协程使用`await`和事件循环调度。
异步生成器使用`async for`和`anext`。
它们共享可恢复执行的想法,但协议不同。
## 35.37 CPython执行模型
在 CPython 级别,生成器是一个拥有挂起执行状态的堆对象。
从概念上讲:```text id="y0j37u"
PyGenObject
code object
frame or interpreter frame state
name and qualname
exception state
running flag
weakrefs
yield-from target
```确切的结构在不同版本中发生变化,但概念字段仍然存在。
恢复时:```text id="zrin3w"
check generator is not closed
check generator is not already running
mark running
enter evaluation loop with saved frame
run until yield, return, or exception
save frame state if yielded
clear frame state if completed
mark not running
return yielded value or propagate exception
```## 35.38`YIELD_VALUE`这`YIELD_VALUE`指令是字节码操作的关键。
从概念上讲:```text id="bpj8tu"
value = pop stack
save current frame position
return value to generator caller
mark generator suspended
```当恢复时,执行在yield指令之后继续。
产生的值不是函数的最终返回值。它是迭代器协议传递的中间结果。
## 35.39`SEND`和代表团
现代字节码对将值发送到生成器、协程和委托路径有特定的支持。
从概念上讲,发送操作:```text id="z95s95"
resume suspended iterator/coroutine
send value or None
receive yielded value, return value, or exception
yield from和await两者都依赖于发送到另一个可恢复对象。
这就是嵌套可恢复计算的连接方式,无需手动编写完整循环。
35.40 常见误解
| 误会 | 正确型号 |
|---|---|
| 调用生成器函数来运行它 | 它创建一个生成器对象 |
yield是一样的return |
yield暂停;return完成 |
| 发电机耗尽后可重复使用 | 这是一击 |
| 生成器存储所有值 | 它存储执行状态并延迟计算 |
yield from是循环的唯一语法 |
它还转发发送、抛出、关闭和返回值 |
| 生成器在每次产量后释放局部变量 | 当地人在暂停期间仍然活着 |
一个for循环看到StopIteration.value |
它忽略值 |
close()只是删除 |
它注入GeneratorExit并运行清理 |
35.41 阅读策略
从一个小型发电机开始:python id="wby2x2" def gen(): x = 1 yield x x = 2 yield x 检查:```python id="hvaeen"
import dis
import inspect
g = gen()
print(inspect.getgeneratorstate(g)) dis.dis(gen)
print(next(g)) print(inspect.getgeneratorstate(g)) print(g.gi_frame.f_locals)
print(next(g))
print(inspect.getgeneratorstate(g))
print(g.gi_frame.f_locals)
然后学习:text id="tf7v1e"
return value
send
throw
close
yield from
try/finally
generator expressions
contextlib.contextmanager
对于每种情况,跟踪:text id="a4iald"
when the body starts
where execution suspends
which locals remain alive
what resumes execution
what exception or value crosses the boundary
when the frame is cleared
生成器是作为具有保存的执行状态的迭代器对象实现的可恢复函数。生成器函数调用创建一个生成器对象。仅当发电机恢复时主体才运行`next`, `send`, `throw`, 或者`close`。
核心模型是:```text id="4a89xx"
generator function call
↓
create generator object with suspended frame
↓
next/send resumes frame
↓
yield returns value and suspends frame
↓
resume later from same point
↓
return or end raises StopIteration
```生成器连接字节码执行、帧、异常处理、迭代、惰性求值、内存生命周期和清理语义。
它们是 CPython 将执行状态视为对象的最清晰示例之一。