您的位置:首页 > 编程语言 > Python开发

Python yield深度解析

2015-11-26 07:40 671 查看
为了理解yield到底是做什么的,你必须知道什么是generators。在将generators之前,我们先看看什么是iterables.

iterables

当我们创建了一个列表后,你就可以一个接一个地读取元素。这样一个接一个地读取元素就叫做iteration:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3
mylist是一个iterable.但你使用一个列表,你创建了一个列表,也就是一个iterable:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4
任何你可以使用“for....in....”都是一个iterable:列表,字符串,文件等等都是

这些iterable是非常方便的,因为你可以读取你想读取的任意多的数据,但是你把它们的值都存储在了内存中,但有很多只的时候这可能就不是你想要的结果了。

Generators

Generators也是iterables,但是你只可以迭代它们一次。这是因为它并不把所有的值存储在内存中,它们在运行时产生这些值

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4
这和上面的例子是一样,除了这次你用了(),上面用的是[]。但是,你不可以第二次去执行for i in mygenerator,因为generators只能使用一次:它计算了0,然后丢掉它去计算1,直到计算了4,一个接一个地。

Yield

Yield是一个使用上类似于return的关键字,除了这个函数返回的是一个generator.

>>> 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,你必须知道当你调用一个函数,你写在函数体里的代码并没有执行。这个函数仅仅是返回了一个generator对象,这是有一点小技巧的。

然后,你的代码将会在每一次用for使用generator的时候被执行。

现在是比较难的部分:

当for第一次调用从你函数中创建的generator对象时,它会从开始执行函数中的代码直到它遇到yield,然后它会返回这个循环的第一个值。然后,每一次其它的调用将会再一次执行你写在函数中的循环,并且返回下一个值,直到没有值可以返回为止。

generator会被认为是空的一旦你的函数运行的时候没有再遇到yield。这可能是因为循环已经到了终点,或者是你并没有再满足“if/else”.

代码解释

Generator

# 这里你创建了一个node对象的方法,这个方法将返回一个generator
def node._get_child_candidates(self, distance, min_dist, max_dist):

# 这里是每一次你使用generator对象时会调用的代码:

# If there is still a child of the node object on its left
# AND if distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild

# If there is still a child of the node object on its right
# AND if distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild

# If the function arrives here, the generator will be considered empty
# there is no more than two values: the left and the right children


Caller

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

# Get the last candidate and remove it from the list
node = candidates.pop()

# Get the distance between obj and the candidate
distance = node._get_dist(obj)

# If distance is ok, then you can fill the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)

# Add the children of the candidate in the candidates list
# so the loop will keep running until it will have looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result


这些代码包含着一些巧妙的地方:

循环迭代在一个列表上,但是这个列表在循环迭代的时候也在不断膨胀。这是一个简洁的方式去遍历所有这些嵌套的数据,即使这是有一些危险,因为你可能遇到一个无限循环。在这个例子中,candidates.extend(node._get_child_candidates(distance,
min_dist, max_dist))消耗掉所有generator的值,但是while保持着创建新的generator对象,这些对象将从前一个节点中产生不同的值,因为它不会作用在相同的节点上。
extend()方法是一系列对象方法期待着一个iterable并且将整个值加到列表中。

通常我们传入一个列表到它:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
但是你的代码中它取得了一个generator,这是好的,因为:

你不需要读取它的值两次。
你可以有许多的儿子并且你不想把他们都存储在内存中。

这能工作是因为Python并不介意一个函数的参数是不是一个列表。Python期待iterables,所以它可以工作在字符串,列表,元组和generators!这叫做鸭子类型,并且是其中一个为什么Python这么酷的原因。但是这是另外一个故事了,略去不表。

你可以在此停下了,或者继续读一些generator的高级用法:

Controlling a generator exhaustion

>>> 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, your best friend

Itertools模块包含特别的函数来操纵iterables。你可曾想过复制一个generator?连接两个generator?用一个liner分组在一个嵌套的列表中?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)]


Understanding the inner mechanisms of iteration

迭代是一个指明iterables的过程(实现__iter__()方法)和iterators(实现__next__()方法)。Iterables是你能从iterator中取的的任何对象。Iterators是可以让你迭代在iterables上的对象。
关于这篇文章的更多内容请看how does the for loop work

此文翻译自stackoverflow上的一个帖子,地址是:这里
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: