用法
一个 常用的 yield 应用场景是使用它生成斐波那契数列,代码如下:
def fib():
a, b = 0, 1
while True:
yield b
a, b = b, a + b
当我们调用 fib 时并不是直接获得函数的返回值,而是获得一个生成器对象:
def fib():
a, b = 0, 1
while True:
yield b
a, b = b, a + b
print(fib())
#
一种简单的使用生产器对象方式为循环:
def fib(n):
a, b = 0, 1
while True:
yield b
if b >= n:
return
a, b = b, a + b
for x in fib(5):
print(x)
# 1, 1, 2, 3, 5
这里我把迭代器最大的值限制在5,避免程序无休止运行下去。此外,还可以采用 next 方法来获取生成器的值:
g = fib(5)
print(next(g)) # 1
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # 5
print(next(g)) # StopIteration
可以看出第 6 次调用抛出了 StopIteration。如果一个函数已近包含 yield, 那么这个函数在执行 return 语句时即会抛出 StopIteration 异常,且返回值在异常对象的 value 属性中。例如:
def gen():
yield 5
return "end"
g = gen()
next(g)
try:
next(g)
except StopIteration as e:
print(e.value) # end
当然,以下这种情况依然会抛出 StopIteration,只是最终的返回值为 None 而已:
def gen():
yield 5
g = gen()
next(g)
try:
next(g)
except StopIteration as e:
print(e.value) # None
为什么需要 yield
在了解 yield 的基本用法后,还需要知道为什么需要使用 yield。如果没有 yield,我们当然可以使用 list 来保存数列的值:
def fiblist(n):
res = []
a, b = 0, 1
while b <= n:
res.append(b)
a, b = b, a + b
return res
print(fiblist(5)) # [1, 1, 2, 3, 5]
可以看出,使用 list 同样可以计算数列。但是这时存在一个问题,如果我需要生成大量的数列这时就存在两个问题:
- 在生成数列的同时我们需要进行一次循环,而从 list 里面取出数列时我们同样还要循环一次。增加了循环的成本。
- 所有生成的数都保存在内存中,如果后续依旧采用这种方式会急速的使得内存浪费。
其实者涉及到代码设计中的一种使用资源的方式: 延迟加载。即资源并不在它声明的时候马上读取到内存中,而是当真正需要使用的时候菜才进行加载。
而当采用 yield 生产数列的时候则是符合这一观点的,即如果你不使用循环或者 next 去读取生成器,则它永远只是一个生成器对象,并不会发生真正的计算。
为什么yield能行
许多解析 yield 的文章都止步于前两个内容,而并未分析为什么 yield 能行。yield 最关键的思想在于当我们 "调用" 一次含有 yield 的函数之后,函数的上下文依然存在。即 yield 的真正作用为在函数在切换到其他上下文,依然会保存自己的上下文。例如对于 fib 函数,当我们调用一次 next 后,a, b 的值未变为初始值,而 是 yield 之前的值。这有一点类似于 c 语言中的静态函数。
yield 还能做什么
利用上面提到的特点,yield 还可以完成三个基础操作:
- 保存上下文,进行迭代计算
- 保存上下文,缓存资源
- 保存上下文,使得异步更加优雅
其中计算斐波那契数列则是上面的第一点应用。至于第二点应用,还需要提到学习一下生成器的其他用法。
send
生成器除了使用 next 函数取值以外,还可以使用 send 函数向生成器传值并获得一个返回值,例如:
def gen():
b = yield "start"
while True:
c = yield b
if c == None:
return "end"
b = c
g = gen()
a = g.send(None)
print(a) # start
print(g.send(1)) # 1
print(g.send(2)) # 2
print(g.send(3)) # 3
print(g.send(None)) # StopIteration value = end
python 规定,第一次向生成器发送的值必需为 None,起作用为启动生成器。上面函数的运行流程为:
- 调用 a = g.send(None)。gen 运行到 yield "start",gen 返回 "start"。
- 调用 a.send(1)。gen 函数继续运行,此使获取到 send 发送的值为 1,则 b = 1。gen函数继续运行到 yield b, 此使gen 函数返回 b,即1。
- 调用a.send(2)。gen 函数继续运行,此时获取到 c 的值为 2,继续运行,b = c = 2,遇到下一个 yield ,返回2。
其实 a = yield b 这条语句可以看作两个部分: 第一步为 yield 即函数先返回 b。此时函数停止运行,等待send。当下一个 send 调用产生后,函数继续运行并将接收的值传递给 a。
有了这个知识后。考虑这样一个需求: 接收一个正则表达式,并判定某个字符串是否被该表达式匹配。一般来说可以这样写:
class Re:
def __init__(self, reg_expr):
self._reg_expr = re.compile(reg_expr)
def test(self, string):
return bool(self._reg_expr.match(string))
r = Re("hello")
print(r.test("hello world")) # True
print(r.test("python")) # False
但是有了 yield 之后,我们则可以这样写:
def re_test(re_expr):
cached_re = re.compile(re_expr)
sentence = yield None
while True:
sentence = yield bool(cached_re.match(sentence))
test = re_test("hello")
test.send(None)
print(test.send("hello world")) # True
print(test.send("python")) # False
虽然这相比使用类实现的方式并没有节省代码,但可以更清楚的理解 yield 的保存上下文的功能。
此外,第三个作用将在下一篇文章中介绍。
yield from
具有 yield 的函数虽然被当成生成器对象对待。但在开发中依然希望具有 yield 的函数能像普通函数一样工作。 对于一个普通函数,进行函数嵌套是最基础的操作,非常简单也非常好理解。例如在函数 a 中调用 函数 b:
def a():
b()
但对于一个生成器对象来说,并不如此简单。例如一个生成器对象为 a:
def a():
for i in range(10):
yield i
另一个生成器 b 想调用生成器 a,并在结束后做点其他事情。这时我们并不能:
def b():
a()
yield "ok"
因为 a 是一个生成器对象,因此我们需要:
def b():
for i in a():
yield i
yield "ok"
这就与普通的函数调用存在差异了。这时即可以使用 yield from,上面的代码等价于:
def b():
yield from a()
yield "ok"
是不是更加优雅了?此外,如果 a 生成器还需要接收数据,例如:
def a():
b = yield None
while True:
print(b)
b = yield
if b == None:
return "end"
那如果不使用 yield from, b 函数将变得异常复杂:
def b():
g = a()
g.send(None)
c = yield None
while True:
try:
g.send(c)
except StopIteration as e:
print(e)
c = yield
x = b()
x.send(None)
x.send(1) # 1
x.send(2) # 2
x.send(None) # end
实际上,如果使用 yield from,我们只需要:
def b():
yield from a()
x = b()
x.send(None)
x.send(1) # 1
x.send(2) # 2
可以看出,yield from 使得生成器的嵌套更加的优雅。此外,yield from 有更多的优点,这里不再赘述。