Python3 yield from 用法详解

yield from是Python3.3以后全新的语言结构,它的作用比yield多得多,因此人们认为继续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用await关键字,这个名称就好多了。当然,后面Python也改成了await,这个我们在结尾说,少废话,先看东西。

1. 替代内层for循环

如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的for循环:

1
2
3
4
5
6
7
8
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

chain 生成器函数把操作依次交给接收到的各个可迭代对象处理。

1
2
3
4
5
6
7
Python3.3之后引入了新语法:
>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
  • yield from 完全代替了内层的 for 循环。
  • yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。
  • 在这个示例中使用 yield from 代码读起来更顺畅,不过感觉更像是语法糖。

上面这个例子看上去比较简单(传统意义上说因为我们只是for循环一次就完事,因为只嵌套了一层),我们再来看几个yield from的例子。
例子1:我们有一个嵌套型的序列,想将它扁平化处理为一列单独的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
yield from flatten(x)
else:
yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]
for x in flatten(items):
print(x)
# output:
1 2 3 4 5 6 7 8
-----------------------------------------------
items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
for x in flatten(items):
print(x)

# output:
Dave
Paula
Thomas
Lewis
  • collections.Iterable是一个抽象基类,我们用isinstance(x, Iterable)检查某个元素是否是可迭代的.如果是的话,那么就用yield from将这个可迭代对象作为一种子例程进行递归。最终返回结果就是一个没有嵌套的单值序列了。
  • 代码中额外的参数ignore types 和检测语句isinstance(x, ignore types)用来将字符
    串和字节排除在可迭代对象外,防止将它们再展开成单个的字符。
  • 如果这里不用yield from的话,那么就需要另外一个for来嵌套,并不是一种优雅的操作

例子2:利用一个Node类来表示树结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Node:
def __init__(self, value):
self._value = value
self._children = []

def __repr__(self):
return 'Node({!r})'.format(self._value)

def add_child(self, node):
self._children.append(node)

def __iter__(self):
return iter(self._children)

def depth_first(self):
yield self
for c in self:
yield from c.depth_first()


if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))
for ch in root.depth_first():
print(ch)
  • __iter__代表一个Pyton的迭代协议,返回一个迭代器对象,就能迭代了
  • depth_frist返回一个生成器,仔细体会其中的yieldyield from用法

上面两个例子无论是树还是嵌套序列,都比较复杂,观察这里yield from跟的是什么,跟的是函数,生成器函数,而且都是在函数内递归。虽然我也不是理解的很透彻 =,= 。但现在应该知道,这是yield from一种常用的方法了(认真体会,手动滑稽)。

2. 打开双通道

如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有可能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
这里有张详细图来说明三者关系:http://flupy.org/resources/yield-from.pdf
例子就不展开了,有兴趣的童鞋可以去 Fluent Python这本书上 查看 示例16-17。并且结合示例图好好体会(我也有待好好体会)

3. 总结

  • yield from伴随着Python3.4新加入的asyncio模块得以发扬光大, asynico + yield from双枪组合,但当时定义协程还是需要@asyncio.coroutine装饰器,之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程

  • 从Python 3.5开始引入了新的语法 asyncawaityield from换成了await(为了不与实现内层for循环的yield from误解?!),@asyncio.coroutine换成了asyncasynico + await成了新的双枪组合,一直到未来… 从Python设计的角度来说,它们让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更Pythonic。

  • 从Python3.5加入以来,asynico官方文档有点混乱,毕竟是新模块,语法也一直在变换中,到了Python 3.7,3.8才趋于稳定,文档也好像重写了,清晰明了,如果要细品asynico,那么不如看最新文档搞起来,能节省不少功夫!。