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

python iterator generator

想要改进这篇文章?提供这个问题的详细答案,包括引文和解释为什么你的答案是正确的。没有足够细节的答案可能会被编辑或删除。

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 库。这是完整源代码的链接:Module mspace。

N
Neuron

要了解 yield 的作用,您必须了解 generators 是什么。在您了解生成器之前,您必须了解 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,因为生成器只能使用一次:它们计算 0,然后忘记它并计算 1,然后一个接一个地计算 4。

屈服

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 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)) 耗尽了生成器的所有值,但同时不断创建新的生成器对象,这些对象将产生与以前的值不同的值,因为它没有应用在同一个对象上节点。

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

通常我们传递一个列表给它:

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

但是在您的代码中,它有一个生成器,这很好,因为:

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

它之所以有效,是因为 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__() 方法)的过程。可迭代对象是您可以从中获取迭代器的任何对象。迭代器是允许您迭代可迭代对象的对象。

在这篇关于 how for loops work 的文章中有更多关于它的信息。

yield 并不像这个答案所暗示的那么神奇。当您在任何地方调用包含 yield 语句的函数时,您将获得一个生成器对象,但没有代码运行。然后,每次从生成器中提取对象时,Python 都会执行函数中的代码,直到遇到 yield 语句,然后暂停并传递对象。当您提取另一个对象时,Python 会在 yield 之后继续并继续,直到它到达另一个 yield(通常是同一个,但稍后迭代)。这一直持续到函数运行结束,此时生成器被视为耗尽。

“这些迭代很方便......但是你将所有值都存储在内存中,这并不总是你想要的”,要么是错误的,要么是令人困惑的。可迭代对象在调用可迭代对象上的 iter() 时返回一个迭代器,并且迭代器并不总是必须将其值存储在内存中,取决于 iter 方法的实现,它还可以按需生成序列中的值。

很高兴添加到这个 great 答案中,为什么 除了您使用 () 而不是 [] 之外,它是一样的,特别是 () 是什么(有可能与元组混淆)。

@MatthiasFripp“这会一直持续到函数运行结束”——或者遇到 return 语句。 (在包含 yield 的函数中允许使用 return,前提是它没有指定返回值。)

yield 语句暂停函数的执行并将一个值发送回调用者,但保留足够的状态以使函数能够从中断的地方恢复。恢复后,函数会在最后一次 yield 运行后立即继续执行。这允许它的代码随着时间的推移产生一系列值,而不是一次计算它们并将它们像列表一样发送回来。

n
nzz

了解产量的捷径

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

在函数的开头插入一行 result = [] 。用 result.append(expr) 替换每个 yield expr。在函数底部插入一行返回结果。是的 - 没有更多的收益声明!阅读并找出代码。将功能与原始定义进行比较。

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

不要混淆你的迭代器、迭代器和生成器

一、迭代器协议——当你写

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

Python 执行以下两个步骤:

获取 mylist 的迭代器:调用 iter(mylist) -> 这将返回一个带有 next() 方法(或 Python 3 中的 __next__() )的对象。 [这是大多数人忘记告诉你的步骤] 使用迭代器循环项目:继续对从步骤 1 返回的迭代器调用 next() 方法。将 next() 的返回值分配给 x 和循环身体被执行。如果在 next() 中引发了异常 StopIteration,则意味着迭代器中没有更多的值并且退出循环。

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

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

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

内置列表、字典、元组、集合、文件。实现 __iter__() 的用户定义类。发电机。

请注意,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 之后的下一行从暂停状态恢复,执行下一行代码,在本例中为 {1 } 语句,并将其作为下一项返回。这种情况一直发生,直到函数退出,此时生成器引发 StopIteration,循环退出。

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

为什么要使用生成器?

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

“当你看到一个带有 yield 语句的函数时,应用这个简单的技巧来理解会发生什么” 这难道不是完全忽略了你可以send进入一个生成器这一事实,这是一个很大的部分发电机的意义?

“它可以是一个 for 循环,但也可以是 otherlist.extend(mylist) 之类的代码”->这是不正确的。 extend() 就地修改列表并且不返回可迭代对象。尝试循环 otherlist.extend(mylist) 将失败并返回 TypeError,因为 extend() 隐式返回 None,并且您不能循环 None

@pedro您误解了那句话。这意味着 python 在执行 otherlist.extend(mylist) 时会在 mylist 上(而不是在 otherlist 上)执行上述两个步骤。

G
Georgy

这样想:

对于具有 next() 方法的对象,迭代器只是一个听起来很花哨的术语。所以一个 yielded 函数最终会是这样的:

原始版本:

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

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

我应该指出,这是为了说明目的而过度简化了。 :)

可以定义 __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

我在 Python 3.6 中尝试了这个示例,如果我创建 iterator = some_function(),则变量 iterator 不再有一个名为 next() 的函数,而只有一个 __next__() 函数。以为我会提到它。

您编写的 for 循环实现在哪里调用 iterator__iter__ 方法,it 的实例化实例?

不幸的是,这个答案根本不正确。这不是 python 解释器对生成器所做的。它不是从生成器函数开始创建类并实现 __iter____next__。这篇文章 stackoverflow.com/questions/45723893/… 解释了它在幕后所做的事情。引用@Raymond Hettinger “生成器没有在内部实现,如纯python类中所示。相反,它们与常规函数共享大部分相同的逻辑”

n
ninjagecko

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

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

简而言之:生成器是一个惰性的、增量挂起的列表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 迭代器协议发出“'list' 中的下一个返回值”(对于某些语法结构,如重复的 for 循环调用 next() 并捕获 StopIteration 异常等)。您可能遇到过带有 generator expressions 的生成器;生成器函数更强大,因为您可以将参数传回暂停的生成器函数,使用它们来实现协程。稍后再谈。

基本示例(“列表”)

让我们定义一个类似于 Python 的 range 的函数 makeRange。调用 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),或者在 while 循环中使用 yield 关键字来实现。

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

在幕后

这就是“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() 函数(和迭代协议的其他部分)来实现花哨的东西,通常以牺牲可读性为代价,所以尽量避免这样做......

协程

Coroutine 示例:

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)

细节

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

在 Python 语言中,iterable 是任何“理解 for 循环概念”的对象,例如列表 [1,2,3],而 iterator 是请求的 for 循环,如 [1,2,3].__iter__()generator 与任何迭代器完全相同,除了它的编写方式(使用函数语法)。

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

因此,万一你没有做这样的事情......

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

...然后请记住,生成器是 iterator;也就是说,它是一次性的。如果您想重复使用它,您应该再次调用 myRange(...)。如果您需要使用两次结果,请将结果转换为列表并将其存储在变量 x = list(myRange(5)) 中。如果绝对需要,那些绝对需要克隆生成器的人(例如,正在做骇人听闻的元编程的人)可以使用 itertools.tee (still works in Python 3),因为 copyable iterator Python PEP standards proposal 已被推迟。

R
Russia Must Remove Putin

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

答案大纲/摘要

带有 yield 的函数在调用时会返回一个生成器。

生成器是迭代器,因为它们实现了迭代器协议,因此您可以迭代它们。

生成器也可以发送信息,使其在概念上成为协程。

在 Python 3 中,您可以使用 yield from 在两个方向上从一个生成器委托给另一个生成器。

(附录批评了几个答案,包括最上面的一个,并讨论了在生成器中使用 return。)

发电机:

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

生成器的想法来自其他具有不同实现的语言(见脚注 1)。在 Python 的生成器中,代码的执行是 frozen 在屈服点。当调用生成器(方法在下面讨论)时,执行恢复,然后在下一个 yield 处冻结。

yield 提供了一种简单的 implementing the iterator protocol 方法,由以下两种方法定义:__iter__next (Python 2) 或 __next__ (Python 3)。这两种方法都使对象成为迭代器,您可以使用 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 (.__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 is that once exhausted 的一项功能,您不能重复使用或重置它:

>>> 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 将生成器排队。它将调用适当的 next__next__ 方法,具体取决于您使用的 Python 版本:

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

现在我们可以将数据发送到生成器中。 (Sending None is the same as calling next.):

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

合作委派到子协程,收益来自

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


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

现在模拟向帐户添加另外 1,000 加上帐户的回报 (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 6, in money_manager
  File "<stdin>", line 2, in <module>
ValueError

结论

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

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

事实证明,yield 做了很多事情。我确信我可以为此添加更详尽的示例。如果您想要更多或有一些建设性的批评,请通过下面的评论告诉我。

附录:

对最佳/接受答案的批评**

仅以列表为例,对什么使可迭代感到困惑。请参阅上面的参考资料,但总而言之:可迭代对象具有返回迭代器的 __iter__ 方法。迭代器提供了一个 .next(Python 2 或 .__next__ (Python 3) 方法,该方法被 for 循环隐式调用,直到它引发 StopIteration,一旦引发,它将继续这样做。

然后它使用生成器表达式来描述生成器是什么。由于生成器只是一种创建迭代器的便捷方式,它只会混淆问题,我们还没有进入 yield 部分。

在控制生成器耗尽中,他调用了 .next 方法,而他应该使用内置函数 next。这将是一个适当的间接层,因为他的代码在 Python 3 中不起作用。

迭代工具?这与 yield 的作用完全无关。

没有讨论 yield 提供的方法以及 Python 3 中的新功能 yield。top/accepted 答案是一个非常不完整的答案。

对建议在生成器表达式或理解中产生的答案的批评。

该语法目前允许列表推导中的任何表达式。

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 核心开发人员是 discussing deprecating its allowance。这是邮件列表中的相关帖子:

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 干杯,Nick。 ——尼克·科格兰 | gmail.com 上的 ncoghlan |澳大利亚布里斯班

此外,还有一个 outstanding issue (10544) 似乎指向 never 的方向是一个好主意(PyPy,一个用 Python 编写的 Python 实现,已经发出语法警告。)

底线,直到 CPython 的开发者告诉我们:不要将 yield 放在生成器表达式或理解中。

生成器中的 return 语句

Python 2 中:

在生成器函数中,return 语句不允许包含 expression_list。在这种情况下,一个简单的返回表示生成器已经完成,并将导致 StopIteration 被引发。

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

Python 3 中:

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

脚注

提案中引用了 CLU、Sather 和 Icon 语言,以将生成器的概念引入 Python。一般的想法是,一个函数可以维护内部状态并根据用户的需要产生中间数据点。这有望在性能上优于其他方法,包括 Python 线程,这在某些系统上甚至不可用。这意味着,例如,范围对象不是迭代器,即使它们是可迭代的,因为它们可以被重用。像列表一样,它们的 __iter__ 方法返回迭代器对象。

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

F
Fang

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

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

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

这很接近,但不正确。每次调用带有 yield 语句的函数时,它都会返回一个全新的生成器对象。只有当您调用该生成器的 .next() 方法时,才会在最后一次 yield 后恢复执行。

C
Claudiu

还有一件事要提一下:产生的函数实际上不必终止。我写过这样的代码:

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);

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

O
Oren

对于那些喜欢最小工作示例的人,请沉思这个交互式 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
B
Bob Stein

TL;博士

而不是这个:

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每一个部分。

这是我的第一个“啊哈”时刻。

yieldsugary 的表达方式

构建一系列东西

相同的行为:

>>> 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 functioniterator 就是它返回的内容。这些条款很有启发性。我们失去了容器的便利性,但获得了根据需要计算且任意长度的序列的能力。

Yield 惰性,它推迟了计算。一个带有 yield 的函数在您调用它时根本不会真正执行。它返回一个 iterator object,它会记住它停止的位置。每次在迭代器上调用 next()(这发生在 for 循环中)时,执行都会向前推进到下一个 yield。 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 的绝妙选择,因为 both meanings 适用:

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

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

yield - 让位或放弃(如在政治权力中)

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

A
Andreas

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 循环等中使用它,但每个值只能访问一次。所有值也不会同时存储在内存中;生成器对象“记住”你上次调用它时它在循环中的位置——这样,如果你使用一个迭代来(比如说)计数到 500 亿,你不必全部计数到 500 亿立即存储 500 亿个数字以供计数。

同样,这是一个非常人为的例子,如果你真的想数到 500 亿,你可能会使用 itertools。 :)

这是生成器最简单的用例。正如您所说,它可用于编写有效的排列,使用 yield 通过调用堆栈向上推,而不是使用某种堆栈变量。生成器也可以用于专门的树遍历,以及其他各种方式。

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

J
Jon Skeet

它正在返回一个生成器。我对 Python 不是特别熟悉,但如果您熟悉的话,我相信它与 C#'s iterator blocks 是一回事。

关键思想是编译器/解释器/任何东西都会做一些诡计,以便就调用者而言,他们可以继续调用 next() 并且它将继续返回值 - 就好像生成器方法被暂停一样。现在显然你不能真正“暂停”一个方法,所以编译器为你构建一个状态机来记住你当前的位置以及局部变量等的样子。这比自己编写迭代器要容易得多。

P
Peter Mortensen

在描述如何使用生成器的许多很好的答案中,有一种我觉得还没有给出的答案。这是编程语言理论的答案:

Python 中的 yield 语句返回一个生成器。 Python 中的生成器是一个返回 continuations 的函数(特别是一种协程,但 continuations 代表更通用的机制来理解正在发生的事情)。

编程语言理论中的延续是一种更为基础的计算,但它们并不经常使用,因为它们极难推理,也很难实现。但是关于什么是延续的想法很简单:它是尚未完成的计算状态。在这种状态下,变量的当前值、尚未执行的操作等都被保存下来。然后在程序稍后的某个时间点,可以调用延续,以便程序的变量重置为该状态并执行保存的操作。

延续,在这种更一般的形式中,可以通过两种方式实现。在 call/cc 方式中,程序的堆栈按字面意思保存,然后当调用延续时,堆栈被恢复。

在延续传递风格 (CPS) 中,延续只是程序员显式管理并传递给子例程的普通函数(仅在函数是第一类的语言中)。在这种风格中,程序状态由闭包(以及恰好编码在其中的变量)表示,而不是驻留在堆栈中某处的变量。管理控制流的函数接受延续作为参数(在 CPS 的某些变体中,函数可以接受多个延续)并通过调用它们来操纵控制流,只需调用它们并随后返回。延续传递样式的一个非常简单的示例如下:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

在这个(非常简单的)示例中,程序员将实际写入文件的操作保存到一个延续中(这可能是一个非常复杂的操作,需要写出许多细节),然后传递该延续(即,作为第一个-类闭包)到另一个执行更多处理的运算符,然后在必要时调用它。 (我在实际的 GUI 编程中经常使用这种设计模式,或者是因为它节省了我的代码行,或者更重要的是,在 GUI 事件触发后管理控制流。)

这篇文章的其余部分将在不失一般性的情况下将延续概念化为 CPS,因为它更容易理解和阅读。

现在让我们谈谈 Python 中的生成器。生成器是延续的特定子类型。虽然延续通常能够保存计算的状态(即程序的调用堆栈),但生成器只能保存迭代器上的迭代状态。虽然,对于生成器的某些用例,这个定义有点误导。例如:

def f():
  while True:
    yield 4

这显然是一个合理的可迭代对象,其行为定义明确——每次生成器迭代它时,它都会返回 4(并且永远如此)。但在考虑迭代器(即for x in collection: do_something(x))时,它可能不是典型的可迭代类型。这个例子说明了生成器的强大功能:如果有任何东西是迭代器,那么生成器可以保存其迭代的状态。

重申一下:延续可以保存程序堆栈的状态,生成器可以保存迭代的状态。这意味着延续比生成器强大得多,而且生成器也容易得多。它们对语言设计者来说更容易实现,对程序员来说也更容易使用(如果您有时间,请尝试阅读并理解 this page about continuations and call/cc)。

但是您可以轻松地将生成器实现(和概念化)作为延续传递样式的简单、特定情况:

每当调用 yield 时,它都会告诉函数返回一个延续。当再次调用该函数时,它会从它停止的地方开始。因此,在伪伪代码(即不是伪代码,但不是代码)中,生成器的 next 方法基本上如下:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

其中 yield 关键字实际上是真正的生成器函数的语法糖,基本上是这样的:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

请记住,这只是伪代码,Python 中生成器的实际实现更为复杂。但是作为一个了解正在发生的事情的练习,请尝试使用连续传递样式来实现生成器对象,而不使用 yield 关键字。

t
tzot

这是一个简单的例子。我将提供高级人类概念与低级 Python 概念之间的对应关系。

我想对一个数字序列进行操作,但我不想为创建该序列而烦恼,我只想专注于我想做的操作。因此,我执行以下操作:

我打电话给你,告诉你我想要一个以特定方式计算的数字序列,我让你知道算法是什么。这一步对应于定义生成器函数,即包含yield的函数。

过了一段时间,我告诉你,“好吧,准备告诉我数字的顺序”。此步骤对应于调用返回生成器对象的生成器函数。请注意,您还没有告诉我任何数字;你只要拿起你的纸和铅笔。

我问你,“告诉我下一个号码”,你告诉我第一个号码;之后,你等我问你下一个号码。你的工作是记住你在哪里,你已经说过什么数字,下一个数字是什么。我不在乎细节。此步骤对应于在生成器对象上调用 next(generator)。 (在 Python 2 中,.next 是生成器对象的一个方法;在 Python 3 中,它被命名为 .__next__,但调用它的正确方法是使用内置的 next() 函数,就像 len() 和 .__len__ 一样)

… 重复上一步,直到…

最终,你可能会走到尽头。你不告诉我一个数字;你只需大喊,“抓住你的马!我完了!没有更多的数字了!”此步骤对应于生成器对象结束其工作并引发 StopIteration 异常。生成器函数不需要引发异常。当函数结束或发出返回时,它会自动引发。

这就是生成器所做的(包含 yield 的函数);它从第一个 next() 开始执行,每次执行 yield 时都会暂停,当被要求输入 next() 值时,它会从最后一个点继续执行。它在设计上与 Python 的迭代器协议完美契合,该协议描述了如何顺序请求值。

迭代器协议最著名的用户是 Python 中的 for 命令。因此,每当您执行以下操作时:

for item in sequence:

sequence 是列表、字符串、字典还是生成器 object 都没有关系,如上所述;结果是一样的:你从一个序列中一个一个地读取项目。

请注意,def创建包含 yield 关键字的函数并不是创建生成器的唯一方法;这只是创建一个的最简单方法。

有关更准确的信息,请阅读 Python 文档中的 iterator typesyield statementgenerators

M
Mike McKerns

虽然很多答案都说明了为什么要使用 yield 创建生成器,但 yield 还有更多用途。制作协程非常容易,它可以在两个代码块之间传递信息。我不会重复已经给出的关于使用 yield 创建生成器的任何优秀示例。

为了帮助理解 yield 在以下代码中的作用,您可以用手指在任何具有 yield 的代码中跟踪循环。每次您的手指点击 yield 时,您都必须等待输入 nextsend。当调用 next 时,您将跟踪代码直到您点击 yield...yield 右侧的代码被评估并返回给调用者...然后您等待。当再次调用 next 时,您将在代码中执行另一个循环。但是,您会注意到,在协程中,yield 也可以与 send... 一起使用,这会将值从调用者发送到 产生函数。如果给出 send,则 yield 接收发送的值,并将其吐出左侧......然后通过代码的跟踪继续进行,直到您再次点击 yield(最后返回值,如如果 next 被调用)。

例如:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

可爱的!一个 trampoline(在 Lisp 意义上)。很少有人看到那些!

C
Community

还有另一个 yield 用途和含义(从 Python 3.3 开始):

yield from <expr>

来自 PEP 380 -- Syntax for Delegating to a Subgenerator

为生成器提出了一种语法,以将其部分操作委托给另一个生成器。这允许将包含“yield”的一段代码分解并放置在另一个生成器中。此外,允许子生成器返回一个值,并且该值可供委托生成器使用。当一个生成器重新生成另一个生成器生成的值时,新语法还为优化提供了一些机会。

此外,this 将介绍(从 Python 3.5 开始):

async def new_coroutine(data):
   ...
   await blocking_action()

避免协程与常规生成器混淆(今天两者都使用了 yield)。

A
AbstProcDo

所有很好的答案,但是对于新手来说有点困难。

我假设您已经学习了 return 语句。

打个比方,returnyield 是双胞胎。 return 表示“返回并停止”,而“yield”表示“返回,但继续”

尝试获得一个返回的 num_list。

def num_list(n):
    for i in range(n):
        return i

运行:

In [5]: num_list(3)
Out[5]: 0

看,你只得到一个数字而不是它们的列表。 return永远不会让你高兴得不得了,只执行一次就退出。

有收获

return 替换为 yield

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

现在,您赢取所有号码。

与运行一次并停止的 return 相比,yield 运行您计划的次数。您可以将 return 解释为 return one of them,将 yield 解释为 return all of them。这称为 iterable

再一步,我们可以用 return 重写 yield 语句

In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

这是关于 yield 的核心。

列表 return 输出和对象 yield 输出之间的区别是:

您将始终从列表对象中获取 [0, 1, 2],但只能从“对象 yield 输出”中检索它们一次。因此,它有一个新名称 generator 对象,如 Out[11]: <generator object num_list at 0x10327c990> 中所示。

总之,作为一个比喻来理解它:

回报和收益是双胞胎

列表和生成器是双胞胎

这是可以理解的,但一个主要区别是您可以在一个函数/方法中拥有多个收益。那个类比在那个时候完全失效了。 Yield 会记住它在函数中的位置,因此下次调用 next() 时,函数会继续执行下一个 yield。我认为这很重要,应该表达出来。

a
alinsoar

从编程的角度来看,迭代器实现为 thunks

要将迭代器、生成器和用于并发执行的线程池等实现为 thunk,可以使用具有调度程序的 messages sent to a closure objectdispatcher answers to "messages"

"next" 是发送到闭包的消息,由“iter”调用创建。

有很多方法可以实现这种计算。我使用了突变,但是可以通过返回当前值和下一个 yielder(使其成为 referential transparent)来进行这种无需突变的计算。 Racket 在某些中间语言中使用了初始程序的一系列转换,其中一种重写使得 yield 运算符可以用更简单的运算符转换为某种语言。

这是一个如何重写 yield 的演示,它使用 R6RS 的结构,但语义与 Python 的相同。它是相同的计算模型,只需要更改语法即可使用 Python 的 yield 重写它。

欢迎来到 Racket v6.5.0.3。 -> (define gen (lambda (l) (define yield (lambda () (if (null?l) 'END (let ((v (car l)))) (set!l (cdr l)) v))) ) (lambda(m) (case m ('yield(yield)) ('init (lambda(data) (set!l data) 'OK)))))) -> (define stream (gen '(1 2 3 ))) -> (stream'yield) 1 -> (stream'yield) 2 -> (stream'yield) 3 -> (stream'yield)'END -> ((stream'init) '(ab))' OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END ->

D
Dustin Getz

以下是一些 Python 示例,说明如何实际实现生成器,就好像 Python 没有为它们提供语法糖一样:

作为 Python 生成器:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

使用词法闭包代替生成器

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

使用对象闭包而不是生成器(因为 ClosuresAndObjectsAreEquivalent

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)
j
johnzachary

我打算发布“阅读 Beazley 的“Python:基本参考”的第 19 页,以快速描述生成器”,但许多其他人已经发布了很好的描述。

另外,请注意 yield 可以在协程中用作它们在生成器函数中的对偶。尽管它与您的代码片段的用途不同,但 (yield) 可以用作函数中的表达式。当调用者使用 send() 方法向方法发送值时,协程将执行,直到遇到下一个 (yield) 语句。

生成器和协程是设置数据流类型应用程序的好方法。我认为了解函数中 yield 语句的其他用途是值得的。

P
Peter Mortensen

这是一个简单的例子:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

输出:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

我不是 Python 开发人员,但在我看来 yield 占据了程序流的位置,下一个循环从“yield”位置开始。似乎它正在那个位置等待,就在此之前,在外面返回一个值,下一次继续工作。

这似乎是一种有趣且不错的能力:D

你是对的。但是看“收益”的行为对流量有什么影响?我可以以数学的名义更改算法。对“产量”进行不同的评估是否会有所帮助?

E
Evgeni Sergeev

这是 yield 所做的工作的心理形象。

我喜欢将线程视为具有堆栈(即使它没有以这种方式实现)。

当调用普通函数时,它会将其局部变量放入堆栈,进行一些计算,然后清除堆栈并返回。其局部变量的值再也见不到了。

对于 yield 函数,当它的代码开始运行时(即在调用函数后,返回一个生成器对象,然后调用其 next() 方法),它同样将其局部变量放入堆栈并计算一段时间.但是,当它遇到 yield 语句时,在清除它的堆栈部分并返回之前,它会对其局部变量进行快照并将它们存储在生成器对象中。它还在其代码中记下它当前所在的位置(即特定的 yield 语句)。

所以这是生成器所依赖的一种冻结函数。

随后调用 next() 时,它会将函数的所有物检索到堆栈中并重新对其进行动画处理。该函数继续从它停止的地方开始计算,忘记了它刚刚在冷库中度过了永恒的事实。

比较以下示例:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

当我们调用第二个函数时,它的行为与第一个函数非常不同。 yield 语句可能无法访问,但如果它出现在任何地方,它就会改变我们正在处理的内容的性质。

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

调用 yielderFunction() 不会运行它的代码,而是从代码中生成一个生成器。 (为了便于阅读,使用 yielder 前缀命名这些东西可能是个好主意。)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

gi_codegi_frame 字段是存储冻结状态的位置。用 dir(..) 探索它们,我们可以确认我们上面的心智模型是可信的。

R
Rafael

想象一下,你创造了一台非凡的机器,它每天能够产生成千上万个灯泡。机器在具有唯一序列号的盒子中生成这些灯泡。您没有足够的空间同时存储所有这些灯泡,因此您希望对其进行调整以按需生成灯泡。

Python 生成器与这个概念没有太大区别。假设您有一个名为 barcode_generator 的函数,它为盒子生成唯一的序列号。显然,您可以通过该函数返回大量此类条形码,但受硬件 (RAM) 限制。更明智且节省空间的选项是按需生成这些序列号。

机器代码:

def barcode_generator():
    serial_number = 10000  # Initial barcode
    while True:
        yield serial_number
        serial_number += 1


barcode = barcode_generator()
while True:
    number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
    barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
    print(barcodes)

    # function_to_create_the_next_batch_of_lightbulbs(barcodes)

    produce_more = input("Produce more? [Y/n]: ")
    if produce_more == "n":
        break

注意 next(barcode) 位。

如您所见,我们有一个独立的“功能”,可以每次生成下一个唯一的序列号。这个函数返回一个生成器!如您所见,我们不是在每次需要新序列号时都调用该函数,而是使用给定生成器的 next() 来获取下一个序列号。

惰性迭代器

更准确地说,这个生成器是一个惰性迭代器!迭代器是帮助我们遍历对象序列的对象。之所以称为 lazy,是因为它在需要时才将序列中的所有项目加载到内存中。上例中使用 nextexplicit 方式从迭代器中获取下一项。 implicit 方式是使用 for 循环:

for barcode in barcode_generator():
    print(barcode)

这将无限打印条形码,但您不会耗尽内存。

换句话说,生成器看起来像一个函数,但行为却像一个迭代器。

现实世界的应用?

最后,现实世界的应用?当您处理大序列时,它们通常很有用。想象一下从磁盘读取一个包含数十亿条记录的巨大文件。在您可以使用其内容之前读取内存中的整个文件可能是不可行的(即,您将耗尽内存)。

Z
ZF007

一个简单的例子来理解它是什么:yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

输出是:

1 2 1 2 1 2 1 2

你确定那个输出吗?如果您使用 print(i, end=' ') 运行该打印语句,那不会只打印在一行上吗?否则,我相信默认行为会将每个数字放在一个新行上

@user9074332,你说得对,不过为了方便理解,写在一行上

P
Peter Mortensen

就像每个答案所暗示的那样,yield 用于创建序列生成器。它用于动态生成一些序列。例如,在网络上逐行读取文件时,您可以使用 yield 函数,如下所示:

def getNextLines():
   while con.isOpen():
       yield con.read()

您可以在代码中使用它,如下所示:

for line in getNextLines():
    doSomeThing(line)

执行控制转移陷阱

执行 yield 时,执行控制将从 getNextLines() 转移到 for 循环。因此,每次调用 getNextLines() 时,都会从上次暂停的点开始执行。

因此,简而言之,具有以下代码的函数

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

将打印

"first time"
"second time"
"third time"
"Now some useful value 12"
s
smwikipedia

(我下面的回答只从使用 Python 生成器的角度说,而不是 underlying implementation of generator mechanism,它涉及一些堆栈和堆操作的技巧。)

当在 python 函数中使用 yield 而不是 return 时,该函数会变成一个特殊的东西,称为 generator function。该函数将返回一个 generator 类型的对象。 yield 关键字是一个标志,用于通知 python 编译器对此类函数进行特殊处理。一旦从它返回某个值,普通函数将终止。但是在编译器的帮助下,生成器函数可以认为是可恢复的。也就是说,将恢复执行上下文,并从上次运行继续执行。直到您显式调用 return,这将引发 StopIteration 异常(这也是迭代器协议的一部分),或者到达函数的末尾。我找到了很多关于 generator 的参考资料,但 functional programming perspective 中的这个 one 是最容易理解的。

(现在我想谈谈generator背后的基本原理,以及基于我自己的理解的iterator。我希望这可以帮助您掌握本质动机迭代器和生成器。这样的概念也出现在其他语言中,例如 C#。)

据我了解,当我们要处理一堆数据时,我们通常会先将数据存储在某个地方,然后逐个处理。但是这种幼稚的方法是有问题的。如果数据量很大,那么预先将它们作为一个整体存储起来会很昂贵。 与其直接存储 data 本身,不如间接存储某种 metadata,即 the logic how the data is computed

有两种方法可以包装此类元数据。

OO 方法,我们将元数据包装为一个类。这就是所谓的迭代器,它实现了迭代器协议(即__next__() 和__iter__() 方法)。这也是常见的迭代器设计模式。函数式方法,我们将元数据包装为函数。这就是所谓的生成器函数。但在底层,返回的生成器对象仍然是 IS-A 迭代器,因为它也实现了迭代器协议。

无论哪种方式,都会创建一个迭代器,即一些可以为您提供所需数据的对象。 OO 方法可能有点复杂。无论如何,使用哪一个取决于您。

P
Peter Mortensen

总之,yield 语句将您的函数转换为一个工厂,该工厂生成一个名为 generator 的特殊对象,该对象环绕您的原始函数的主体。当 generator 被迭代时,它会执行您的函数,直到它到达下一个 yield,然后暂停执行并计算传递给 yield 的值。它在每次迭代中重复这个过程,直到执行路径退出函数。例如,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

简单地输出

one
two
three

力量来自使用带有计算序列的循环的生成器,生成器每次执行循环停止以“产生”计算的下一个结果,这样它就可以动态计算列表,好处是内存为特别大的计算而保存

假设您想创建一个自己的 range 函数来生成可迭代的数字范围,您可以这样做,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

并像这样使用它;

for i in myRangeNaive(10):
    print i

但这是低效的,因为

您创建了一个只使用一次的数组(这会浪费内存)

这段代码实际上在该数组上循环了两次! :(

幸运的是 Guido 和他的团队足够慷慨地开发生成器,所以我们可以这样做;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

现在,在每次迭代时,生成器上的一个名为 next() 的函数都会执行该函数,直到它到达“yield”语句,在该语句中它停止并“生成”值或到达函数的末尾。在这种情况下,在第一次调用时,next() 执行到 yield 语句并产生“n”,在下一次调用时,它将执行增量语句,跳回“while”,评估它,如果为真,它将停止并再次产生'n',它将继续这种方式,直到while条件返回false并且生成器跳转到函数的末尾。

P
Peter Mortensen

产量是一个对象

函数中的 return 将返回单个值。

如果您希望函数返回大量值,请使用 yield

更重要的是,yield 是一个障碍

就像 CUDA 语言中的屏障一样,它在完成之前不会转移控制权。

也就是说,它将从头开始运行您的函数中的代码,直到它到达 yield。然后,它将返回循环的第一个值。

然后,每个其他调用将再次运行您在函数中编写的循环,返回下一个值,直到没有任何值可以返回。

P
Peter Mortensen

许多人使用 return 而不是 yield,但在某些情况下,yield 可能更高效且更易于使用。

这是 yield 绝对最适合的示例:

返回(在函数中)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

产量(在函数中)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

调用函数

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

两个函数做同样的事情,但 yield 使用三行而不是五行,并且少了一个需要担心的变量。

这是代码的结果:

https://i.stack.imgur.com/iUFNJ.png

正如你所看到的,这两个函数都做同样的事情。唯一的区别是 return_dates() 给出了一个列表,而 yield_dates() 给出了一个生成器。

一个现实生活中的例子就像一行一行地读取文件,或者你只是想制作一个生成器。

P
Phillip

yield 关键字只是收集返回的结果。将 yield 视为 return +=

W
Will Dereham

yield 类似于函数的返回元素。不同之处在于,yield 元素将函数转换为生成器。生成器的行为就像一个函数,直到“产生”某些东西。生成器停止直到下一次被调用,并从与它开始的完全相同的点继续。通过调用 list(generator()),您可以将所有“产生”值的序列合二为一。