协慌网

登录 贡献 社区

“yield” 关键字有什么作用?

Python 中yield关键字的用途是什么?它有什么作用?

例如,我正在尝试理解这段代码1

def _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 = [], [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时会发生什么?列表是否返回?单个元素?它又被召唤了吗?后续通话何时停止?


1. 代码来自 Jochen Schulz(jrschulz),他为度量空间创建了一个很棒的 Python 库。这是完整源代码的链接: 模块 mspace

答案

要了解yield作用,您必须了解发电机是什么。而在此之前的发电机来iterables。

Iterables

创建列表时,您可以逐个阅读其项目。逐个读取它的项称为迭代:

>>> 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... ” 的所有内容都是可迭代的; listsstrings ,文件......

这些迭代很方便,因为您可以根据需要读取它们,但是您将所有值存储在内存中,当您拥有大量值时,这并不总是您想要的。

发电机

生成器是迭代器,是一种只能迭代一次的迭代器。生成器不会将所有值存储在内存中, 它们会动态生成值

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

除了你使用()而不是[]之外,它是一样的。但是,你不能 for i in mygenerator执行for i in mygenerator因为生成器只能使用一次:它们计算 0,然后忘记它并计算 1,然后逐个计算 4。

产量

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"


你的代码解释了

发电机:

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

呼叫者:

# 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))耗尽了生成器的所有值,但while不断创建新的生成器对象,这些对象将生成与之前的值不同的值,因为它不应用于相同的值节点。

  • extend()方法是一个列表对象方法,它需要一个 iterable 并将其值添加到列表中。

通常我们将列表传递给它:

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

但是在你的代码中它得到了一个生成器,这很好,因为:

  1. 您不需要两次读取值。
  2. 您可能有很多孩子,并且您不希望它们都存储在内存中。

它的工作原理是因为 Python 不关心方法的参数是否是列表。 Python 期望 iterables 所以它将适用于字符串,列表,元组和生成器!这叫做鸭子打字,这也是 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 # 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
...

注意:对于 Python 3,使用print(corner_street_atm.__next__())print(next(corner_street_atm))

它可以用于控制对资源的访问等各种事情。

Itertools,你最好的朋友

itertools 模块包含操作 iterables 的特殊函数。曾经希望复制发电机吗?链两个发电机?使用单行分组嵌套列表中的值? 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__()方法)的过程。 Iterables 是可以从中获取迭代器的任何对象。迭代器是允许您迭代迭代的对象。

在这篇文章中有关于for循环如何工作的更多信息

Grokking yield捷径

当你看到一个带有yield语句的函数时,应用这个简单的技巧来理解会发生什么:

  1. 在函数的开头插入行result = []
  2. result.append(expr)替换每个yield expr
  3. 在函数底部插入一行return result
  4. 耶 - 没有更多的yield声明!阅读并找出代码。
  5. 比较功能与原始定义。

这个技巧可以让你了解函数背后的逻辑,但是实际上与yield与基于列表的方法中发生的情况有很大不同。在许多情况下,yield 方法将更高效,更快。在其他情况下,即使原始函数工作得很好,这个技巧也会让你陷入无限循环。请继续阅读以了解更多信息...

不要混淆你的 Iterables,Iterators 和 Generators

首先, 迭代器协议 - 当你写

for x in mylist:
    ...loop body...

Python 执行以下两个步骤:

  1. 获取mylist的迭代器:

    调用iter(mylist) - > 这将返回一个带有next()方法的对象(或 Python 3 中的__next__() )。

    [这是大多数人忘记告诉你的步骤]

  2. 使用迭代器循环遍历项目:

    保持调用next()方法在迭代器从第 1 步从返回的返回值next()被分配给x ,并执行循环体。如果从next()引发异常StopIteration ,则意味着迭代器中没有更多值,并且退出循环。

事实上,Python 在任何时候想要循环对象的内容时执行上述两个步骤 - 所以它可能是 for 循环,但它也可能是像otherlist.extend(mylist)代码( otherlist列表是 Python 列表) 。

这里mylist是一个可迭代的,因为它实现了迭代器协议。在用户定义的类中,您可以实现__iter__()方法以使类的实例可迭代。此方法应返回迭代器 。迭代器是一个带有next()方法的对象。可以在同一个类上实现__iter__()next() ,并使__iter__()返回self 。这适用于简单的情况,但是当您希望两个迭代器同时循环遍历同一个对象时。

所以这是迭代器协议,许多对象实现了这个协议:

  1. 内置列表,词典,元组,集,文件。
  2. 用户定义的实现__iter__()
  3. 发电机。

请注意, for循环不知道它正在处理什么类型的对象 - 它只是遵循迭代器协议,并且很乐意在它调用next()获得项目。内置列表逐个返回它们的项目,字典逐个返回 ,文件一个接一个地返回等。然后生成器返回...... 那就是yield来源:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

如果你在f123()有三个return语句而不是yield语句,那么只会执行第一个语句,并且函数将退出。但是f123()不是普通的功能。当f123() ,它返回 yield 语句中的任何值!它返回一个生成器对象。此外,该功能并没有真正退出 - 它进入暂停状态。当for循环试图遍历生成器对象时,该函数在之前返回的yield之后的下一行从其挂起状态恢复,执行下一行代码,在本例中为yield语句,并将其返回为下一个项目。这种情况一直发生,直到函数退出,此时生成器引发StopIteration ,循环退出。

所以生成器对象有点像适配器 - 它的一端展示了迭代器协议,通过暴露__iter__()next()方法来保持for循环的快乐。然而,在另一端,它运行该功能足以从中获取下一个值,并将其重新置于挂起模式。

为什么要使用发电机?

通常,您可以编写不使用生成器但实现相同逻辑的代码。一种选择是使用我之前提到的临时列表 '技巧'。这在所有情况下都不起作用,例如,如果你有无限循环,或者当你有一个很长的列表时,它可能会使内存的使用效率低下。另一种方法是实现一个新的可迭代类SomethingIter ,它将状态保存在实例成员中,并在它的next() (或 Python 3 中的__next__() )方法中执行下一个逻辑步骤。根据逻辑, next()方法中的代码可能看起来非常复杂并且容易出错。这里的发电机提供了一个简洁的解决方案

想一想:

对于具有 next()方法的对象,迭代器只是一个奇特的声音术语。因此,屈服函数最终会像这样:

原始版本:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

这基本上是 Python 解释器对上面代码的作用:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

为了更深入地了解幕后发生的事情,可以将for循环重写为:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

这更有意义还是只是让你感到困惑? :)

我要指出,这为了说明的目的过于简单化。 :)