“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)编写的,Jochen Schulz 是一个很好的用于度量空间的 Python 库。这是完整源代码的链接: Module mspace

答案

要了解什么是yield ,您必须了解什么是发电机 。而且,在您了解生成器之前,您必须了解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...上使用 “ 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 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

呼叫者:

# 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 不在乎方法的参数是否为列表。 Python 期望可迭代,因此它将与字符串,列表,元组和生成器一起使用!这就是所谓的鸭子输入,这是 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 模块包含用于操纵可迭代对象的特殊功能。曾经希望复制一个发电机吗?连锁两个发电机?用一个单行将嵌套列表中的值分组? 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循环如何工作的内容

了解yield捷径

当您看到带有yield语句的函数时,请应用以下简单技巧,以了解将发生的情况:

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

这个技巧可能会让您对函数背后的逻辑有所了解,但是yield实际发生的情况与基于列表的方法发生的情况显着不同。在许多情况下,yield 方法也将大大提高内存效率和速度。在其他情况下,即使原始函数运行正常,此技巧也会使您陷入无限循环。请继续阅读以了解更多信息...

不要混淆您的 Iterable,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

这是否更有意义,还是会让您更加困惑? :)

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

yield关键字简化为两个简单事实:

  1. 如果编译器在函数内部的任何位置检测到yield关键字,则该函数不再通过return语句return相反 ,它立即返回一个懒惰的 “待处理列表” 对象,称为生成器
  2. 生成器是可迭代的。什么是可迭代的 ?就像listsetrange或字典视图一样,它具有用于以特定顺序访问每个元素内置协议

简而言之: 生成器是一个懒惰的,增量待定的 list ,并且yield语句允许您使用函数符号来编程生成器应该逐渐吐出的列表值

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

让我们定义一个函数makeRange ,就像 Python 的range 。调用makeRange(n)一个生成器:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

要强制生成器立即返回其待处理的值,可以将其传递到list() (就像您可以进行任何迭代一样):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

将示例与 “仅返回列表” 进行比较

可以将上面的示例视为仅创建一个列表,并将其附加并返回:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

但是,有一个主要区别。请参阅最后一节。


您如何使用发电机

可迭代是列表理解的最后一部分,并且所有生成器都是可迭代的,因此经常像这样使用它们:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

为了更好地了解生成器,可以使用itertools模块(一定要使用chain.from_iterable而不要在需要时使用chain )。例如,您甚至可以使用生成器来实现无限长的惰性列表,例如itertools.count() 。您可以实现自己的def enumerate(iterable): zip(count(), iterable) ,或者在 while 循环中使用yield关键字来实现。

请注意:生成器实际上可以用于更多事情,例如实现协程或不确定性编程或其他优雅的事情。但是,我在这里提出的 “惰性列表” 观点是您会发现的最常见用法。


幕后花絮

这就是 “Python 迭代协议” 的工作方式。也就是说,执行list(makeRange(5)) 。这就是我之前所说的 “懒惰的增量列表”。

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

内置函数next()仅调用对象.next()函数,该函数是 “迭代协议” 的一部分,可以在所有迭代器上找到。您可以手动使用next()函数(以及迭代协议的其他部分)来实现一些奇特的事情,通常是以牺牲可读性为代价的,因此请避免这样做...


细节

通常,大多数人不会关心以下区别,并且可能想在这里停止阅读。

在 Python 发言,可迭代是 “理解的 for 循环的概念” 像列表中的任何对象[1,2,3]以及迭代器是的特定实例所请求的 for 循环等[1,2,3].__iter__()生成器与任何迭代器完全相同,但其编写方式(带有函数语法)除外。

当您从列表中请求迭代器时,它将创建一个新的迭代器。但是,当您从迭代器请求迭代器时(很少这样做),它只会为您提供自身的副本。

因此,在极少数情况下,您可能无法执行此类操作...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... 然后记住生成器是迭代器 ; 即是一次性使用。如果要重用它,则应再次调用myRange(...) 。如果需要两次使用结果,请将结果转换为列表并将其存储在变量x = list(myRange(5)) 。那些绝对需要克隆生成器的人(例如,正在进行骇人的骇人的元编程的人)可以在绝对必要的情况下使用itertools.tee ,因为可复制的迭代器 Python PEP标准建议已被推迟。

yield关键字在 Python 中有什么作用?

答案大纲 / 摘要

  • 具有yield的函数在调用时将返回Generator
  • 生成器是迭代器,因为它们实现了迭代器协议 ,因此您可以对其进行迭代。
  • 也可以生成器发送信息 ,使其在概念上成为协程
  • 在 Python 3 中,您可以使用yield from从一个生成器在两个方向上将其委派给另一生成器。
  • (附录对几个答案进行了评论,包括最上面的一个,并讨论了生成器中return的用法。)

发电机:

yield仅在函数定义内部是合法的,并且yield包含在函数定义中使其返回生成器。

生成器的想法来自具有不同实现方式的其他语言(请参见脚注 1)。在 Python 的 Generators 中,代码的执行会在收益率点冻结 。调用生成器时(下面将讨论方法),恢复执行,然后冻结下一个收益率。

yield提供了一种实现迭代器协议的简便方法, 该协议由以下两种方法定义: __iter__next (Python 2)或__next__ (Python 3)。这两种方法都使对象成为一个迭代器,您可以使用collections模块中的Iterator Abstract Base Class 对其进行类型检查。

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

生成器类型是迭代器的子类型:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

并且如有必要,我们可以像这样进行类型检查:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Iterator一个功能是,一旦耗尽 ,您就无法重用或重置它:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

如果要再次使用其功能,则必须另做一个(请参见脚注 2):

>>> list(func())
['I am', 'a generator!']

一个人可以通过编程方式产生数据,例如:

def func(an_iterable):
    for item in an_iterable:
        yield item

上面的简单生成器也等效于下面的生成器 - 从 Python 3.3 开始(在 Python 2 中不可用),您可以使用yield from

def func(an_iterable):
    yield from an_iterable

但是, yield from还允许委派给子生成器,这将在以下有关与子协程进行合作委派的部分中进行解释。

协程:

yield形成一个表达式,该表达式允许将数据发送到生成器中(请参见脚注 3)

这是一个示例,请注意received变量,该变量将指向发送到生成器的数据:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

首先,我们必须使用内置函数next来使生成器排队。根据您使用的 Python 版本,它将调用适当的next__next__方法:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

现在我们可以将数据发送到生成器中。 (不发送None内容与呼叫next相同 。):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

合作派遣到协程, yield from

现在,回想一下yield from在 Python 3 中可用。这使我们可以将协程委托给子协程:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

现在我们可以将功能委派给子生成器,并且生成器可以像上面一样使用它:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

您可以在PEP 380 中阅读有关yield from的精确语义的更多信息

其他方法:关闭并抛出

close方法将在函数执行被冻结时引发GeneratorExit__del__也将调用它,因此您可以将任何清理代码放在处理GeneratorExit

>>> my_account.close()

您还可以引发一个异常,该异常可以在生成器中处理或传播回用户:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

结论

我相信我已经涵盖了以下问题的各个方面:

yield关键字在 Python 中有什么作用?

事实证明, yield很大。我相信我可以为此添加更详尽的示例。如果您想要更多或有建设性的批评,请在下面评论中告诉我。


附录:

对最佳 / 可接受答案的评论 **

  • 仅以列表为例,它对使可迭代的内容感到困惑。请参阅上面的参考资料,但总而言之:iterable 具有返回iterator__iter__方法。 迭代器提供.next (Python 2 或.__next__ (Python 3))方法,该方法由for循环隐式调用,直到引发StopIteration为止,一旦这样做,它将继续这样做。
  • 然后,它使用生成器表达式来描述什么是生成器。由于生成器只是创建迭代器的一种简便方法,因此它只会使问题感到困惑,而我们仍未达到yield部分。
  • 控制发电机的排气中,他调用.next方法,而应改为使用内置函数next 。这将是一个适当的间接层,因为他的代码在 Python 3 中不起作用。
  • Itertools?这与yield完全无关。
  • 没有讨论yield的方法以及 Python 3 中新功能的yield from最高 / 可接受的答案是非常不完整的答案。

暗示对生成器表达或理解yield的答案的评论。

该语法当前允许列表理解中的任何表达式。

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

由于 yield 是一种表达,因此尽管没有特别好的用例,但有人认为它可以用于理解或生成器表达中。

CPython 核心开发人员正在讨论弃用其津贴 。这是邮件列表中的相关帖子:

2017 年 1 月 30 日 19:05,布雷特 · 坎农写道:

2017 年 1 月 29 日星期日,克雷格 · 罗德里格斯(Craig Rodrigues)在星期日写道:

两种方法我都可以。恕我直言,把事情留在 Python 3 中是不好的。

我的投票是 SyntaxError,因为您没有从语法中得到期望。

我同意这对我们来说是一个明智的选择,因为依赖当前行为的任何代码确实太聪明了,无法维护。

在到达目的地方面,我们可能需要:

  • 3.7 中的语法警告或弃用警告
  • 2.7.x 中的 Py3k 警告
  • 3.8 中的 SyntaxError

干杯,尼克。

-Nick Coghlan | gmail.com 上的 ncoghlan | 澳大利亚布里斯班

此外,还有一个悬而未决的问题(10544) ,似乎正说明这绝不是一个好主意(PyPy,用 Python 编写的 Python 实现,已经在发出语法警告。)

最重要的是,直到 CPython 的开发人员另行告诉我们为止: 不要将yield放在生成器表达式或理解中。

生成器中的return语句

Python 2 中

在生成器函数中, return语句不允许包含expression_list 。在这种情况下,简单的return指示生成器已完成,并将导致StopIteration升高。

expression_list基本上是由逗号分隔的任意数量的表达式 - 本质上,在 Python 2 中,您可以使用return停止生成器,但不能返回值。

Python 3 中

在生成器函数中, return语句指示生成器已完成,并将引起StopIteration升高。返回的值(如果有)用作构造StopIteration的参数,并成为StopIteration.value属性。

脚注

  1. 提案中引用了 CLU,Sather 和 Icon 语言,以将生成器的概念引入 Python。总体思路是,一个函数可以维护内部状态并根据用户的需要产生中间数据点。这有望在性能上优于其他方法,包括 Python 线程 ,该方法甚至在某些系统上不可用。

  2. 例如,这意味着xrange对象(Python 3 中的range )不是Iterator ,尽管它们是可迭代的,因为它们可以重复使用。像列表一样,它们的__iter__方法返回迭代器对象。

  3. yield最初是作为语句引入的,这意味着它只能出现在代码块的一行的开头。现在yield创建一个 yield 表达式。 https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt 提出此更改是为了允许用户将数据发送到生成器中,就像接收数据一样。要发送数据,必须能够将其分配给某物,为此,一条语句就行不通了。

yield就像return一样 - 它返回您告诉的任何内容(作为生成器)。不同之处在于,下次调用生成器时,执行从最后一次调用到yield语句开始。与 return 不同的是,在产生良率时不会清除堆栈帧,但是会将控制权转移回调用者,因此下次调用该函数时,其状态将恢复。

对于您的代码,函数get_child_candidates的行为就像一个迭代器,因此当您扩展列表时,它一次将一个元素添加到新列表中。

list.extend调用迭代器,直到耗尽为止。对于您发布的代码示例,返回一个元组并将其附加到列表中会更加清楚。

还有另外一件事要提及:yield 的函数实际上不必终止。我写了这样的代码:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

然后我可以在其他代码中使用它:

for f in fib():
    if some_condition: break
    coolfuncs(f);

它确实有助于简化某些问题,并使某些事情更易于使用。

对于那些偏爱简单工作示例的人,请在此交互式 Python 会话中进行冥想:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed

TL; DR

代替这个:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

做这个:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

当你发现自己从头开始建立一个列表, yield每片来代替。

这是我第一次屈服。


yield是一种含糖的说法

建立一系列的东西

相同的行为:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

不同的行为:

收益是单次通过 :您只能迭代一次。当一个函数包含一个 yield 时,我们称其为Generator 函数 。而迭代器就是它返回的内容。这些术语在揭示。我们失去了容器的便利性,但获得了按需计算且任意长的序列的功效。

产量懒惰 ,推迟了计算。 当您调用函数时,其中包含 yield 的函数实际上根本不会执行。它返回一个迭代器对象 ,该对象记住它从何处中断。每次您在迭代器上调用next() (这在 for 循环中发生)时,执行都会向前推进到下一个收益。 return引发 StopIteration 并结束序列(这是 for 循环的自然结束)。

产量多才多艺 。数据不必全部存储在一起,可以一次存储一次。它可以是无限的。

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

如果您需要多次通过并且序列不太长,只需在其上调用list()

>>> list(square_yield(4))
[0, 1, 4, 9]

明智地选择yield因为两种含义都适用:

产量 —生产或提供(如在农业中)

... 提供系列中的下一个数据。

屈服 —让步或放弃(如在政治权力中一样)

... 放弃 CPU 执行,直到迭代器前进。

产量可以为您提供发电机。

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

如您所见,在第一种情况下, foo一次将整个列表保存在内存中。对于包含 5 个元素的列表来说,这不是什么大问题,但是如果您想要 500 万个列表,该怎么办?这不仅是一个巨大的内存消耗者,而且在调用该函数时还花费大量时间来构建。

在第二种情况下, bar只是为您提供了一个生成器。生成器是可迭代的 - 这意味着您可以在for循环等中使用它,但是每个值只能被访问一次。所有的值也不会同时存储在存储器中。生成器对象 “记住” 您上次调用它时在循环中的位置 - 这样,如果您使用的是一个迭代的(例如)计数为 500 亿,则不必计数为 500 亿立即存储 500 亿个数字以进行计算。

再次,这是一个非常人为的示例,如果您真的想计数到 500 亿,则可能会使用 itertools。 :)

这是生成器最简单的用例。如您所说,它可以用来编写有效的排列,使用 yield 可以将内容推入调用堆栈,而不是使用某种堆栈变量。生成器还可以用于特殊的树遍历以及所有其他方式。