Python yield深度解析

为了理解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,这是好的,因为:
  1. 你不需要读取它的值两次。
  2. 你可以有许多的儿子并且你不想把他们都存储在内存中。
这能工作是因为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上的一个帖子,地址是: 这里

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值