yield理解

这是 StackOverflow 上的一个很热的帖子, 这里是投票最高的一个答案

原文: http://stackoverflow.com/questions/231767/the-python-yield-keyword-explained

提问者的问题

Python 关键字 yield 的作用是什么? 用来干什么的?

比如, 我正在试图理解下面的代码 :

1
2
3
4
5
def node._get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild

下面的是调用 :

1
2
3
4
5
6
7
8
result, candidates = list(), [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

当调用 _get_child_candidates 的时候发生了什么? 返回了一个列表? 返回了一个元素? 被重复调用了么? 什么时候这个调用结束呢?

回答部分

为了理解什么是 yield ,你必须理解什么是生成器. 在理解生成器之前, 让我们先走近迭代.

可迭代对象

当你建立了一个列表, 你可以逐项地读取这个列表, 这叫做一个可迭代对象 :

1
2
3
mylist = [1, 2, 3]
for i in mylist:
print(i)
1
2
3

mylist 是一个可迭代对象. 当你使用一个列表生成式来建立一个列表的时候, 就建立了一个可迭代对象 :

1
2
3
mylist = [x*x for x in range(3)]
for i in mylist:
print(i)
0
1
4

所有你可以使用 for .. in .. 语法的叫做一个迭代器 : 列表, 字符串, 文件 … 你经常使用它们是因为你可以如你所愿的读取其中的元素, 但是你把所有的值都存储到了内存中, 如果你有大量数据的话, 这个方式并不是你想要的.

生成器

生成器是可以迭代的, 但是你 只可以读取它一次 ,因为它并不把所有的值放在内存中, 它是实时地生成数据 :

1
2
3
mygenerator = (x*x for x in range(3))
for i in mygenerator:
print(i)
0
1
4

看起来除了把 [] 换成 () 外没什么不同. 但是, 你不可以再次使用 for i in mygenerator , 因为生成器只能被迭代一次 : 先计算出 0, 然后继续计算 1, 然后计算 4, 一个接一个的 …

yield 关键字

yield 是一个类似 rerun 的关键字, 只是这个函数返回的是一个生成器.

1
2
3
4
5
6
7
8
9
def createGenerator():
mylist = range(3)
for i in mylist:
yield i*i

mygenerator = createGenerator()
print(mygenerator)
for i in mygenerator:
print(i)
<generator object createGenerator at 0x00C189C0>
0
1
4

这个例子没什么用途, 但是它让你知道, 这个函数会返回一大批你只需要读一次的值.

为了精通 yield ,你必须要理解 : 当你调用这个函数的时候, 函数内部的代码并不立马执行 , 这个函数只是返回一个生成器对象, 这有点蹊跷不是吗.

那么, 函数的代码什么时候执行呢? 当你使用 for 进行迭代的时候.

现在到了关键点了 !

第一次迭代中你的函数会执行, 从开始到达 yield 关键字, 然后返回 yield 后的值作为第一次迭代的返回值, 然后, 每次执行这个函数都会继续执行你在函数内部定义的那个循环的下一次, 再返回那个值, 直到没有可以返回的.

如果生成器内部没有定义 yield 关键字, 那么这个生成器被认为空的. 这种情况可能因为是循环进行没了, 或者是没有满足 if/else 条件.

回到你的代码

生成器 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建 node 对象的一个方法, 该方法返回一个生成器
def node._get_child_candidates(self, distance, min_dist, max_dist):
# 如果 node 对象的左边仍然有 child
# 并且距离合适, 则返回下一个 child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild

# 如果 node 对象的右边仍然有 child
# 并且距离合适, 则返回下一个 child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild

# 如果函数执行到这了, 则认为生成器已经空了
# 不再有这两个值: leftchild, rightchild

调用者 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建一个空列表, 和带有当前对象引用的列表
result, candidates = list(), [self]

# candidates 的循环 (一开始他们只包含一个元素 --> self)
while candidates:

# 获取最后一位 candidate , 并把它从列表中移除
node = candidates.pop()

# 获取 obj 和 candidate 之间的 distance
distance = node._get_dist(obj)

# 如果距离合适, 加入result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)

# 在 candidates 列表中添加 candidate 的 children
# 这样, 循环就能持续执行, 直到它查遍 candidate 的所有 children
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

这个代码包含了几个小部分 :

  • 我们对一个列表进行迭代, 但是迭代中列表还在不断的扩展. 它是一个迭代这些嵌套的数据的简洁方式, 即使这样有点危险, 因为可能导致无限迭代.
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
    穷尽了生成器的所有制, 但 while 不断地在产生新的生成器, 他们会产生和上一次不一样的值, 但没有作用到同一个节点上.
  • extend() 是一个迭代器方法, 作用于迭代器, 并把参数追加到迭代器的后面.

通常我们传给它一个列表参数 :

1
2
3
4
a = [1, 2]
b = [3, 4]
a.extend(b)
print(a)
[1, 2, 3, 4]

但是在你的代码中是一个生成器, 这是不错的, 因为 :

  • 你不必读两次所有的值.
  • 你可以有很多子对象, 但不必叫他们都储存在内存里面.

并且这很奏效, 因为 Python 不关心一个方法的参数是不是个列表. Python 只希望它是一个可以迭代的, 所以这个参数可以是列表, 元祖, 字符串, 生成器 … 这叫做 duck typing, 这也是为何 Python 如此棒的原因之一, 但这已经是另外一个问题了 …

你可以在这里停下, 来看看生成器的一些高级用法 :

控制生成器的穷尽
1
2
3
4
5
class Bank():   # 让我们创建一个银行, 生成 ATM
crisis = False
def create_atm(self):
while not self.crisis:
yield '$100'
1
2
hsbc = Bank()
corner_street_atm = hsbc.create_atm()
1
print(corner_street_atm.__next__())
$100
1
print(corner_street_atm.__next__())
$100
1
print([corner_street_atm.__next__() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
1
hsbc.crisis = True # 经济危机来了, 没钱啦!
1
print(corner_street_atm.__next__())
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-21-0291977c370d> in <module>()
----> 1 print(corner_street_atm.__next__())


StopIteration: 
1
wall_street_atm = hsbc.create_atm() # 甚至新的 ATM 也有危机
1
print(wall_street_atm.__next__())
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-32-b42bcf449bd2> in <module>()
----> 1 print(wall_street_atm.__next__())


StopIteration: 

糟糕的是, 危机走了, 但是 ATM 却依然没钱 …

1
hsbc.crisis = False
1
print(corner_street_atm.__next__())
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-36-0291977c370d> in <module>()
----> 1 print(corner_street_atm.__next__())


StopIteration: 
1
brand_new_atm = hsbc.create_atm()
1
2
3
4
5
6
7
i = 7
for cash in brand_new_atm:
i -= 1
if i > 0:
print(cash)
else:
break
$100
$100
$100
$100
$100
$100

对于控制一些资源的访问来说, 这很有用.

Itertools, 你最好的朋友

itertools 包含了很多特殊的迭代方法. 是不是曾想过复制一个迭代器? 串联两个迭代器? 把嵌套的列表分组? 不用创造一个新的列表的 zip/map ?

只要 import itertools

需要个例子? 让我们看看比赛中 4 匹马可能到达终点的先后顺序的可能情况 :

1
2
3
4
5
import itertools

horses = [1, 2, 3, 4]
races = itertools.permutations(horses)
print(races)
<itertools.permutations object at 0x012B9DB0>
1
print(list(races))
[(1, 2, 3, 4), (1, 2, 4, 3), (1, 3, 2, 4), (1, 3, 4, 2), (1, 4, 2, 3), (1, 4, 3, 2), (2, 1, 3, 4), (2, 1, 4, 3), (2, 3, 1, 4), (2, 3, 4, 1), (2, 4, 1, 3), (2, 4, 3, 1), (3, 1, 2, 4), (3, 1, 4, 2), (3, 2, 1, 4), (3, 2, 4, 1), (3, 4, 1, 2), (3, 4, 2, 1), (4, 1, 2, 3), (4, 1, 3, 2), (4, 2, 1, 3), (4, 2, 3, 1), (4, 3, 1, 2), (4, 3, 2, 1)]
了解迭代器的内部机理

迭代是一个实现可迭代对象(实现的是 __iter__() )和迭代器(实现的是 __next__() )的过程. 可迭代对象是你可以从其获取到一个迭代器的任一对象. 迭代器是那些允许你迭代可迭代对象的对象.

更多见这个文章 http://effbot.org/zone/python-for-statement.htm