原题目链接: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 方法被调用时,发生了什么?一个列表被返回了吗?还是一个元素被返回了?它又被再次调用了吗?接下来的调用什么时候停止?
下面是得票最多的答案:来自e-satis
为了理解yield做了什么,你必须知道什么是生成器,而在此之前你需要了解什么是迭代器。
迭代
当你创建了一个列表,你可以一个接一个地读取它的元素。像这样一个接一个读取它元素的行为被称做迭代:
>>>mylist = [1,2,3]
>>>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):
# 每一次你使用生成器对象这里的代码都会被调用:
# 如果在node对象左边仍然有一个child
# 并且 如果distance满足条件, 返回下一个child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# 如果在node对象右边仍然有一个child
# 并且 如果distance满足条件, 返回下一个child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# 当函数执行到这里,生成器可以被认为是空的
# 生成器中不再有两个值:left child和right child
调用:
# 创建一个空列表和一个包含当前对象引用的列表
result, candidates = list(), [self]
# 对candidates的循环 (一开始他们只包含一个元素)
while candidates:
# 获得最后一个candidate并把它从列表中移除
node = candidates.pop()
# 获得obj和candidate之间的distance
distance = node._get_dist(obj)
# 如果distance满足条件, 你就可以拓展result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# 在candidates中添加candidate的children
# 所以循环将一直执行直到其遍历了candidate的children的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()方法是列表对象的方法,其需要一个可迭代对象作为参数并把这个可迭代对象的值加入列表。
一般我们向它传递一个列表:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
但在你的代码里这个方法获得了一个生成器,这很好因为:
1、你不需要读取这些值两次。
2、可能会有很多children对象但你不想把他们都存放在内存中。
它能工作是因为python不关心extend方法的参数是否是一个列表,python期望这个方法的参数是可迭代对象所以字符串、列表、元组和生成器都可以!这叫做填鸭式输入(duck typing),这也是python如此cool的一个原因。但这又是另一个话题了……
你可以就此打住了,也可以再读一点来看看生成器的一个高级用法:
控制生成器的输出
>>> class Bank(): # 我们来建立一家银行并搭一些ATM机
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # 一切正常时,你想要多少,ATM机就给你多少
>>> 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 # 金融危机来了,没钱了!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # 甚至对新的ATM机来说也是这样
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # 问题是,即使危机过去了,ATM机还是空的
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # 重新搭一个来投入商业运营
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
这对很多东西来说都很有用例如控制某个资源的入口。
Itertools,你最好的朋友
itertools模块包含一些用于处理可迭代对象的特殊函数。是不是曾经想过复制一个生成器?连接两个生成器?将嵌套列表里的值提取到一个容器中?Map / Zip
而不用创建一个新的列表?
那就import itertools
。
举个栗子?让我们看看在一场有四匹马的马赛中可能出现的到达终点的顺序:
>>> 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__()
方法)。可迭代对象是你可以从中获得迭代器的任何对象。迭代器是让你可以在可迭代对象上迭代的对象。
更多关于迭代的信息可以参考这篇文章《for循环如何工作》