在Python中,"yield"关键字的作用是什么?

12796

在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时会发生什么? 返回一个列表吗?还是一个单独的元素?它会再次被调用吗?后续的调用何时停止?
这段代码是由Jochen Schulz(jrschulz)编写的,他为度量空间创建了一个很棒的Python库。这是完整源代码的链接:Module mspace

另请参阅:https://www.geeksforgeeks.org/use-yield-keyword-instead-return-keyword-python/amp/ - RadoTheProgrammer
在这里,你可以阅读一个完整的解释。 - Sadegh Moayedizadeh
51个回答

17873
为了理解yield的作用,你必须先理解什么是生成器。而在理解生成器之前,你必须先理解可迭代对象

可迭代对象

当你创建一个列表时,你可以逐个读取其中的元素。逐个读取元素的过程称为迭代:

>>> 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..."的东西都是可迭代的;列表字符串,文件...

这些可迭代对象非常方便,因为你可以随意读取它们,但是它们会将所有的值存储在内存中,当你有大量的值时,这并不总是你想要的。

生成器

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

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

除了使用()而不是[]之外,其他都一样。但是,你不能再次执行for i in mygenerator,因为生成器只能使用一次:它们计算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循环使用生成器时,你的代码将从上次离开的地方继续执行。
现在是困难的部分:
第一次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 are 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 the distance is ok, then you can fill in the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate to the candidate's list
    # so the loop will keep running until it has 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循环中了解更多信息。


740
这个答案中所暗示的 yield 并不像魔法一样。当你调用一个包含 yield 语句的函数时,你会得到一个生成器对象,但是没有代码运行。每次你从生成器中提取一个对象时,Python 执行函数中的代码直到遇到一个 yield 语句,然后暂停并返回该对象。当你提取另一个对象时,Python 在 yield 后面恢复执行,并继续直到达到另一个 yield(通常是相同的 yield,但是在下一次迭代中)。这将持续进行,直到函数运行结束,此时生成器被认为已经耗尽。 - Matthias Fripp
85
“这些可迭代对象很方便,但是将所有值都存储在内存中并不总是你想要的”,这句话可能是错误的或令人困惑的。可迭代对象在调用iter()后返回一个迭代器,而迭代器并不总是必须将其值存储在内存中,这取决于__iter__方法的实现,它也可以根据需要按顺序生成值。 - picmate 涅
33
把这个好的答案补充一下会更好,解释一下为什么仅仅是用了()而不是[],两者其实类似。需要具体说明一下()是什么(可能会与元组混淆)。 - WoJ
45
“这将继续进行,直到该函数运行到末尾”或者遇到一个return语句(如果一个带有yield的函数包含return语句但没有指定返回值,则允许使用)。” - alani
16
yield语句暂停函数的执行并将值返回给调用者,但保留足够的状态以使函数能够恢复到离开的位置继续执行。当恢复时,函数继续执行最后一次yield之后的代码。这允许函数的代码随时间产生一系列值,而不是像列表一样一次性计算并发送它们回来。 - Jacob Ward
显示剩余5条评论

2523

理解yield的捷径

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

  1. 在函数开头插入一行result = []
  2. 将每个yield expr替换为result.append(expr)
  3. 在函数底部插入一行return result
  4. 耶 - 不再有yield语句了!阅读并理解代码。
  5. 将函数与原始定义进行比较。

这个技巧可能让你对函数背后的逻辑有所了解,但实际上,yield的工作方式与基于列表的方法有很大不同。在许多情况下,使用yield的方法在内存效率和速度方面都更优。在其他情况下,这个技巧可能会导致你陷入无限循环,尽管原始函数正常运行。继续阅读以了解更多信息...

不要混淆可迭代对象、迭代器和生成器

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

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

Python进行以下两个步骤:
  1. 获取mylist的迭代器:
  2. 调用iter(mylist) -> 这将返回一个具有next()方法(或者在Python 3中是__next__())的对象。

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

  3. 使用迭代器循环遍历项目:
  4. 保持调用从步骤1返回的迭代器的next()方法。从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语句,那么只会执行第一个return语句,并且函数将退出。但是f123()并不是普通的函数。当调用f123()时,它不会返回yield语句中的任何值!它返回一个生成器对象。此外,该函数并没有真正退出,而是进入了一种暂停状态。当for循环尝试遍历生成器对象时,函数从其暂停状态恢复,在之前yield语句返回的位置的下一行代码处执行下一行代码,即yield语句,并将其作为下一个项返回。这将一直持续到函数退出,此时生成器引发StopIteration,并且循环退出。

因此,生成器对象有点像适配器 - 一端展示迭代器协议,通过公开__iter__()next()方法使for循环正常工作。然而,在另一端,它仅运行函数以获取下一个值并将其放回暂停模式。

为什么要使用生成器?
通常情况下,您可以编写不使用生成器但实现相同逻辑的代码。其中一种选择是使用我之前提到的临时列表“技巧”。但这种方法并不适用于所有情况,例如如果您有无限循环,或者当您有一个非常长的列表时,它可能会对内存使用效率低下。另一种方法是实现一个新的可迭代类SomethingIter,该类将状态保存在实例成员中,并在其next()(或Python 3中的__next__())方法中执行下一个逻辑步骤。根据逻辑的复杂程度,next()方法内部的代码可能会变得非常复杂且容易出错。在这种情况下,生成器提供了一个清晰而简单的解决方案。

43
“当你看到一个带有yield语句的函数时,可以使用这个简单技巧来理解会发生什么。”这是否完全忽略了你可以向生成器中send值的事实,这是生成器的重要部分? - DanielSank
18
它可能是一个for循环,但它也可能是像 otherlist.extend(mylist) 这样的代码。这是不正确的。extend() 是就地修改列表而不返回可迭代对象。尝试循环遍历 otherlist.extend(mylist) 会导致TypeError,因为extend() 隐式地返回 None,你无法循环遍历 None - Pedro
16
@pedro,您误解了那个句子。它的意思是python在执行“otherlist.extend(mylist)”时对“mylist”(而不是“otherlist”)执行了两个提到的步骤。 - today

762

这么想吧:

迭代器只是一个具有 next() 方法的对象的高级术语。 所以一个使用 yield 的函数最终会成为这样:

原始版本:

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

这样理解是否更容易些了,还是更加困惑呢?:)

需要注意的是,这只是为了说明而进行的过度简化。:)


1
__getitem__ 可以代替定义 __iter__。例如:class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i),它会输出:0, 10, 20, ..., 90。 - jfs
36
我在Python 3.6中尝试了这个例子,如果我创建iterator = some_function(),那么变量iterator不再具有名为next()的函数,而只有一个名为__next__()的函数。我觉得需要提一下。 - Peter
你编写的 for 循环实现在哪里调用了 iterator__iter__ 方法,即 it 实例化后的方法? - SystematicDisintegration
2
很不幸,这个答案完全不正确。这不是Python解释器对生成器所做的事情。它并没有从生成器函数开始创建一个类,并实现__iter____next__。它在幕后实际上所做的是在这篇文章中解释的:https://dev59.com/taTja4cB1Zd3GeqPA2X9#Q7OiEYcBWogLw_1baRmq。引用@Raymond Hettinger的话:“生成器并不像您的纯Python类一样在内部实现。相反,它们与常规函数共享大部分相同的逻辑。” - gioxc88

652

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

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

简而言之:通常情况下,生成器是一个懒惰的、逐步挂起的列表,且 yield 语句允许您使用函数表示法来规划生成器应逐步输出的列表值。此外,高级用法还可将生成器用作协程(见下文)。

generator = myYieldingFunction(...)  # basically a list (but lazy)
x = list(generator)  # evaluate every element into a list

   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

基本上,每当遇到yield语句时,函数会暂停并保存其状态,然后根据Python迭代器协议(对于一些语法结构,比如重复调用next()并捕获StopIteration异常的for循环等)发出"在'列表'中的下一个返回值"。你可能已经遇到过具有生成器表达式的生成器;生成器函数更强大,因为你可以将参数传递回暂停的生成器函数,并使用它们来实现协程。关于这个稍后再说。

基本示例('list')

让我们定义一个名为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]

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

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

# return a list                  #  # return a generator
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
    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),或者可以使用 yield 关键字在 while 循环中这样做。

请注意:生成器实际上可以用于许多其他方面,例如 实现协程、非确定性编程和其他优雅的事情。但是,我在这里介绍的"惰性列表"观点是您最常见的用法。


幕后花絮
这就是“Python迭代协议”的工作原理。也就是说,当你执行list(makeRange(5))时发生了什么。这就是我之前描述的“懒惰、逐步增加的列表”。
>>> x=iter(range(5))
>>> next(x)  # calls x.__next__(); x.next() is deprecated
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()函数(以及迭代协议的其他部分)来实现复杂的功能,但通常会牺牲可读性,因此请尽量避免这样做...

协程

协程示例:

def interactiveProcedure():
    userResponse = yield makeQuestionWebpage()
    print('user response:', userResponse)
    yield 'success'

coroutine = interactiveProcedure()
webFormData = next(coroutine)  # same as .send(None)
userResponse = serveWebForm(webFormData)

# ...at some point later on web form submit...

successStatus = coroutine.send(userResponse)

协程(通常通过yield关键字接受输入,例如nextInput = yield nextOutput,作为双向通信的形式)基本上是一种允许自己暂停并请求输入的计算(例如下一步应该做什么)。当协程自己暂停时(当运行的协程最终遇到yield关键字时),计算被暂停,控制被反转(yielded)回到“调用”函数(请求计算的next值的帧)。暂停的生成器/协程保持暂停状态,直到另一个调用函数(可能是不同的函数/上下文)请求下一个值以取消暂停(通常传递输入数据以指导协程代码内部的暂停逻辑)。
你可以将Python协程视为惰性增量挂起列表,其中下一个元素不仅取决于先前的计算,还取决于您在生成过程中选择注入的输入。

细枝末节

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

在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 3中仍然有效),如果绝对必要的话,因为可复制的迭代器Python PEP标准提案已经被推迟。

561

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

答案概要/摘要

  • 具有yield的函数在被调用时,返回一个生成器
  • 生成器是迭代器,因为它们实现了迭代器协议,所以可以迭代它们。
  • 生成器还可以发送信息,因此在概念上它是一个协程
  • 在Python 3中,你可以使用yield from从一个生成器委托给另一个生成器,实现双向传递。
  • (附录批评了一些答案,包括排名第一的答案,并讨论了在生成器中使用return的情况。)

生成器:

yield 只能放在函数定义内部,yield 包含在函数定义中可以使其返回生成器。

生成器的概念来自其他语言(参见脚注1),Python 的生成器执行代码时会 冻结yield 所在的位置。当生成器被调用时(方法将在下面讨论),执行恢复,然后在下一个 yield 处冻结。

yield 提供了一种实现迭代器协议的简便方法,该协议由以下两个方法定义:__iter____next__。这两个方法都可以将对象变成迭代器,并且可以使用 collections 模块的 Iterator 抽象基类进行类型检查。

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__
True                           # implements the iterator protocol.

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

from types import GeneratorType
from collections.abc import Iterator

>>> issubclass(GeneratorType, Iterator)
True

如果必要,我们可以这样进行类型检查:

>>> isinstance(gen, GeneratorType)
True
>>> isinstance(gen, 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及以上版本中,您可以使用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将生成器排队。它将调用适当的next__next__方法,具体取决于您使用的Python版本:

>>> 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进行协作委托给子协程

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


def money_manager(expected_rate):
    # must receive deposited value from .send():
    under_management = yield                   # yield None to start.
    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'''
            raise
        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'''
    # must queue up manager:
    next(manager)      # <- same as manager.send(None)
    # This is where we send the initial deposit to the manager:
    manager.send(deposited)
    try:
        yield from manager
    except GeneratorExit:
        return manager.close()  # delegate?

现在我们可以将功能委托给子生成器,然后它可以像上面那样被生成器使用:
my_manager = money_manager(.06)
my_account = investment_account(1000, my_manager)
first_year_return = next(my_account) # -> 60.0

现在模拟将另外1000元加入账户并计算账户利息(60.0元):

next_year_return = my_account.send(first_year_return + 1000)
next_year_return # 123.6

您可以在PEP 380.中了解有关yield from的精确语义。

其他方法:close和throw

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 6, in money_manager
  File "<stdin>", line 2, in <module>
ValueError

结论

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

在Python中,yield关键字是什么意思?

事实证明,yield有很多用途。我相信我可以添加更详细的例子。如果您想要更多信息或有一些建设性的批评,请在下面评论让我知道。


附录:

对最佳/被接受答案的评价**

  • 有些混淆了什么是可迭代对象,在示例中只使用了列表。详见上文,但简言之,一个可迭代对象具有一个返回迭代器__iter__方法。而一个迭代器则还提供了一个.__next__方法,它会在for循环中被隐式调用,直到触发StopIteration异常,一旦抛出此异常,便将继续抛出。
  • 然后使用生成器表达式来描述生成器是什么。由于生成器表达式只是创建迭代器的方便方式,因此它只会让人更加困惑,我们甚至还没有涉及yield部分。
  • 在控制生成器耗尽的过程中,他调用了.next方法(在Python 2中有效),但他应该使用内置函数next。调用next(obj)是一个适当的间接层,因为他的代码在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,Brett Cannon写道:

在2017年1月29日星期日16:39,Craig Rodrigues写道:

我认为两种方法都可以。按照Python 3中现有的方式不太好。

我的投票是让它成为SyntaxError,因为您无法从语法中得到期望的结果。

我同意这是一个明智的选择,因为任何依赖于当前行为的代码都过于聪明而难以维护。

在实现此目标方面,我们可能需要:

  • 在3.7中使用SyntaxWarning或DeprecationWarning
  • 在2.7.x中使用Py3k警告
  • 在3.8中使用SyntaxError

Cheers, Nick.

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

此外,还存在一个突出问题(10544),似乎表明这永远不是个好主意(PyPy,一种用Python编写的Python实现,已经在引发语法警告了)。

底线是,在CPython的开发人员告诉我们其他方案之前:不要在生成器表达式或推导中放置yield

生成器中的return语句

Python 3中:

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

历史注释,在Python 2中: “在生成器函数中,return语句不允许包含expression_list。在这种情况下,裸露的return表示生成器已完成,并将导致引发StopIteration。” expression_list基本上是由逗号分隔的任意数量的表达式 - 在Python 2中,您可以使用return停止生成器,但无法返回值。

脚注

  1. 在引入生成器概念到Python的提案中,CLU、Sather和Icon等语言被提及。总体思路是函数可以维护内部状态,并按照用户需求产生中间数据点。这承诺比其他方法(包括Python线程)更具性能优势,而有些系统甚至没有Python线程。

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

  3. yield最初作为一个语句引入,意味着它只能出现在代码块的开头。现在yield创建了一个yield表达式。(参见链接1)这个变化是为了允许用户像接收数据一样向生成器发送数据。要发送数据,必须能够将其分配给某个东西,而对于这个目的,语句就行不通了。proposed">(参见链接2)


455

yield 就像 return - 它会返回你告诉它返回的任何东西(作为生成器)。不同之处在于下一次调用生成器时,执行将从上次调用 yield 语句开始。与 return 不同的是,当 yield 发生时,堆栈帧不会被清除,但控制权会传递回调用者,因此其状态将在下次调用函数时恢复。

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

list.extend 调用迭代器直到耗尽。在您发布的代码示例中,更清晰的做法是仅返回元组并将其附加到列表中。


131
这个答案接近正确,但并不完全准确。每次调用带有yield语句的函数时,都会返回一个全新的生成器对象。只有当你调用该生成器的.next()方法时,才会在上一次yield后恢复执行。 - kurosch

316

需要额外提及一点:使用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);

它真正有助于简化一些问题,使一些事情更容易处理。


308

如果您喜欢简明的工作示例,请思考这个交互式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

288

简而言之:

不要这样做:

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 的“噢哈”时刻。

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 是单向迭代:只能迭代一次。当函数中有 yield 时,我们称其为生成器函数。而迭代器就是它返回的内容。这些术语很有启示性。我们失去了容器的便利性,但获得了按需计算和任意长度的序列的强大功能。

yield 是惰性的,它推迟了计算。一个带有 yield 的函数在调用时实际上根本没有执行。它返回一个迭代器对象,记住它离开的位置。每次在迭代器上调用next()(这发生在 for 循环中)时,执行都会前进到下一个 yield。 return 触发 StopIteration 并结束序列(这是 for 循环的自然结束)。

yield 是多才多艺的。数据不必全部存储在一起,可以逐个提供。它可以是无限的。

>>> 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 是非常聪明的,因为两种含义都适用:

yield — 生产或提供(如农业)

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

yield — 让步或放弃(如政治权力)

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


1
我喜欢像您这样的人,能够清晰地解释事物而不使用不必要的术语。 - D-odu
@D-odu 如果我赢得了你的赞美,真正值得感谢的是那些听我讲话时眼睛发直的人。我会为注意到这一点并尝试避免这种情况而自豪。 - Bob Stein

246

Yield会给你一个生成器。

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 循环等语句中使用它,但每个值只能被访问一次。所有的值也不会同时保存在内存中;生成器对象“记住”了上一次调用它时的位置——这样,如果你正在使用可迭代对象(比如)计算到50亿,你不必一次性计算50亿并存储50亿个数字以便遍历。

同样,这是一个相当牵强附会的例子,如果你真的想数到500亿,你可能会使用itertools。:)

这是生成器最简单的用法。正如你所说,可以使用yield通过调用栈将事物推向上面编写高效的排列组合。生成器还可用于专门的树遍历和其他各种任务。


4
请注意,在Python 3中,“range”也返回一个生成器而不是列表,因此您也会看到类似的想法,只是“__repr__” / “__str__”被覆盖以显示更好的结果,在这种情况下是“range(1,10,2)”。 - It'sNotALie.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接