#Python中yield关键词的作用是什么?
原问题地址:http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python
##问题:
在Python中yield关键字的作用是什么?它是做什么用的?
例如,我正试图理解这段代码:
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
这是调用代码:
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
方法的时候发生了什么呢?返回某一个列表?返回某一个元素?它是不是再次被调用了?后续调用什么时候才会停下来?
注:这里的代码是Jochen Schulz(jrschulz)写的。他建立了一个很棒的度量空间的Python库。点击这个链接就可以看到完整的源代码:Module mspace。
##答案 1:
要了解yield是做什么用的,你就必须了解什么是生成器。在了解生成器之前,先要了解可迭代。
###可迭代
当你创建一个列表后,可以逐个读取它的元素。对一个列表的逐个读取被称为迭代。
>>> for i in mylist:
... print(i)
1
2
3
mylist就是可迭代的。当使用列表解析时,你就创建了一个列表,也就是一个可迭代对象:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
所有可以使用"for... in..."的对象都是可迭代的,如:列表, 字符串, 文件…
这些可迭代对象使用起来很方便,因为你可以随心所欲。但是所有的值都存在了内存中,当有大量数据的时候,这就不是你想要的结果了。
###生成器
生成器是迭代器,但只能遍历一次。这是因为并非所有的值都在内存中,它们被实时生成:
>>> 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是一个关键词,它的用法就像return,只不过它返回一个生成器。
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
这段代码示例没什么用处,但是当你知道函数要返回大量的并且仅需要读一次的数据时,就会觉得它是方便的。
要掌握yield,你就必须明白,当你调用这个函数时,写在函数体里的代码并没有运行。该函数只返回到生成器对象,这可有点棘手。
接下来,每当for使用生成器的时候,你的代码就会被运行。
现在的困难部分是:
第一次用for调用你的函数所创建的生成器对象时,它将会从头开始运行函数中的代码,直到遇到yield,才会返回该循环中的第一个值。然后,每次调用都会再运行一次函数中你所写的循环,并返回下一个值,直到没有什么值可以返回。
一旦函数在运行过程中不再遇到yield,生成器就会被认为是空的。这可能是因为循环已经结束,或者因为你不再满足"if/else"条件了。
###代码说明
生成器:
#这里是你创建node对象的方法,它将返回生成器
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
#如果函数到达这里,生成器将被视为空
#这里只有两个值:左边的子节点、右边的子节点
调用代码:
#创建一个空的列表和一个具有当前对象引用的列表
result, candidates = list(), [self]
#候选项的循环(它们一开始只包含一个元素)
while candidates:
#找到最后的候选项并且从列表中删除它
node = candidates.pop()
#获取obj(当前对象)和候选项之间的距离
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))
返回结果
此代码包含几个巧妙的部分:
-
该循环迭代一个列表,但在迭代过程中列表会扩大。这是运行所有的嵌套数据的一个简洁的方法。只不过这样做有点危险,因为你最终可能会遇到死循环。在这个示例中,
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
会读取这个生成器的所有的值,但while循环不断创造新的生成器对象,它们会从以前的生成器对象中产生出不同的值,因为所应用的节点不同。 -
列表的
extend()
方法,允许可迭代对象并把值添加到列表中。
通常我们给extend()
方法传一个列表:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
但在代码中,会接收生成器,这是好现象,因为:
-
你不需要把这些重复读取。
-
你可以有很多子节点,并且不希望把它们都存在内存中。
它之所以奏效是因为Python不在乎一种方法的参数是不是一个列表。Python允许迭代,因此它可以使用字符串、列表、元组和生成器。这就是所谓的鸭子类型,这也是Python很酷的原因之一。但这是另一个故事了,另一个问题…
你可以停在这里,或者读一点关于生成器的高级使用方法:
###控制生成器的损耗
>>> class Bank(): # let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # when everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # it's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
它可以有利于各种事情,如:控制对于资源的访问。
###迭代工具,你最好的朋友
itertools
模块有一些处理可迭代对象的专用方法。你是否曾经希望复制一个生成器?链接两个生成器?用一行代码把嵌套列表中的值进行分组?在无需创建另一个列表进行Map/Zip
?
那么,import itertools
吧。
一个例子,让我们看看4匹赛马的抵达顺序:
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(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__()
方法)。可迭代对象是可以获得迭代器的任何对象。迭代器允许你对可迭代对象进行迭代。
##答案 2
###深刻理解yield的捷径
(关于Grokking)
当你看到一个用yield语句的函数时,应用这个简单的技巧来了解将会发生的情况:
- 在函数的开始处插入一行
result = []
。 - 把每一处yield语句替换为
result.append(expr)
。 - 在函数的末端插入一行
return result
。 - 耶!再也没有用yield语句了。阅读并理解代码。
- 把这个函数与初始定义进行比较。
这个技巧可能会使你对函数背后的逻辑有所了解,但使用yield和使用列表的函数执行起来是有着显著的不同。在很多情况下,使用yield会使得内存效率更高、更快。在其他情况下,即使原函数运行良好,这个技巧也会使你陷入死循环中。继续阅读以便加深了解…
###不要把你的可迭代对象、迭代器和生成器弄混
首先,迭代器协议。当你书写下面内容的时候
for x in mylist:
...loop body...
Python执行以下两个步骤:
- 获取mylist的迭代器:调用
iter(mylist)
->返回一个带有next()
方法的对象(或者在Python 3中返回__next__()
)。(大多数人忘记告诉你这一步) - 循环遍历迭代器:继续调用在步骤1返回的迭代器的
next()
方法。next()
的返回值被赋给x
,并且循环体也在运行。如果在next()
中抛出StopIteration
异常,就意味着在迭代器中没有更多的值了,循环结束了。
事实上,每当Python需要循环遍历某对象的内容时,它就会执行上述两个步骤。所以它可能是一个for循环,但它也可能是像otherlist.extend(mylist)
这样的代码(其中otherlist是一个Python列表)。
这里的mylist是一个可迭代对象,因为它执行了迭代器协议。在一个自定义的类中,你可以使用__iter__()
方法来让你的实例成为可迭代对象。此方法应该返回到一个迭代器。迭代器是含有next()
方法的对象。可以在同一个类中执行__iter__()
和next()
,并让__iter__()
结果返回self。上述做法只适用于简单的情况。当你想要让两个迭代器在同一时间对同一对象进行循环的时候,这个做法就不能奏效了。
这就是迭代器协议,许多对象都执行这个协议:
- 列表、字典、元组、集合、文件。
- 自定义类中的
__iter__()
。 - 生成器。
注意,for循环不知道它所处理的是什么类型的对象,它只是遵循了迭代器协议,并且只要是调用了next()
,它就会遍历所有内容。列表会一个一个地返回到其元素,字典一个一个地返回键,文件一行一行地返回每行内容,生成器返回…这就是yield起作用的地方:
def f123():
yield 1
yield 2
yield 3
for item in f123():
print item
如果你在f123()
中没有使用yield语句,而是使用了三个return 语句,那么只有第一个会被执行,函数将会退出。但f123()
不是普通的函数。当你调用f123()
的时候,它不会到返回到yield语句中的任何一个值!它返回到一个生成器对象。此外,该函数并没有真正退出,它进入了一个挂起(暂停)状态。当for 循环尝试对生成器对象进行循环时,该函数将从它的挂起状态恢复运行,直到下一个yield语句,并同前运行。这种情况一直持续到函数退出为止,最后,生成器就会抛出StopIteration
异常,循环也随之结束。
因此,生成器对象有点儿像适配器。它的一端展示了迭代器协议,利用__iter__()
和next()
方法保持for循环的畅通。然而,在另一端,它所运行的函数只能够支持它获取下一个值,并把它返回到挂起模式。
###为什么要使用生成器?
通常,你在写代码时,不使用生成器也能实现相同的逻辑。一个选择是使用我之前提到过的临时列表的“把戏”。这种做法并非在所有的情况下都适用。例如,如果你遇到了死循环,或者当你有一个很长的列表时,它可能会导致内存使用效率低下。另一种方法是使用一个新的可迭代的类型。SomethingIter
适用于实例成员,并在它的next()
或者Python 3中的__next__()
方法中执行下一个逻辑步骤。根据逻辑,在next()
方法内部的代码可能看起来很复杂,也容易出现错误。生成器可以为这种情况提供一个干净且容易的解决方案。
打赏帐号:[email protected](支付宝),qiwsir(微信号)