Python 中的 yield关键字有什么作用

126 阅读5分钟

要明白yield在做什么,你要先理解什么是生成器(generator)。然后在理解生成之前,你又要搞明白什么是可迭代对象(iterable)。

Iterables

当你创建了一个列表,那你就可以一个接一个地访问它的元素,这种一个接一个访问叫做迭代(iteration):

>>> 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 ...迭代操作:lists, strings, files...

上面这些可迭代对象很简单,你可以随心所欲地访问它们。但是如果这个对象有很多的值的时候,把它们全部都存储在内存里就不合适了。

Generators

生成器是迭代器,一种只能迭代一次的可迭代对象。生成器不会把所有的值都存储在内存里,而是在运行时即时生成值:

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

生成器和列表推导式相比就是把[]换成了()。但是你不能再次运行for i in mygenerantor,因为生成器只能迭代一次:它先计算出0,然后抛弃它,在计算出1,这样一个接着一个到计算出4,然后结束。

Yield

yield关键字的用法就像return一样,不同的是函数返回的是一个生成器:

>>> def create_generator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

这是一个没什么用的例子,但是当你想要你的函数返回一大堆并且只会被访问一次的值时,使用生成器就是非常方便的了。

要掌握yield,你必须明白,当你调用函数时,你写在函数体中的代码是不会运行的。这个函数只是返回一个生成器对象。

然后,您的代码将从每次停止的地方继续运行。

重点来了:

一开始使用for遍历你的函数创建的生成器时,它会从头开始运行你函数中的代码,直到它碰到了yield,然后它会返回循环的第一个值。再然后,每个后续调用都将运行函数中的另一次迭代并返回下一个值。这样运行会一直持续到生成器为空,就是当循环结束或者跳出了if/else结构,函数内部碰不到yield时,迭代就结束了。

问题中的代码解释

Generator:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the 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 the 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 candidate's 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))会消耗掉生成器的值,但是while语句在不断地创建新的生成器,这些生成器会产生不同的值,因为它们应用在不同的节点上。

  • extend()是一个列表方法,用来把一个可迭代对象的值填充到列表中,通常传入一个列表:

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

但是在你的代码中它传入一个生成器,这是很棒的写法,因为:

  1. 你只会访问那些值一次。
  2. 你可能会有很多的子节点,把它们都放到内存里是不合适的。

它能够工作是因为Python并不关心参数是不是一个列表。它希望的是一个可迭代对象,因此所以你给它无论是stringtuple,还是generator都行。这叫做鸭子类型(duck typing),这也是Python为什么这么cool的一个原因。但这就是另一个故事了...

你可以在这里停下来,然后了解下生成器的高级用法:

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 # The 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
...

注意:在Python3中你要这么写:print(corner_street_atm.__next__())或者print(next(corner_street_atm))

这可以用在控制对资源的访问等各种情况。

Itertools, your best friend

Python中的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)]