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

What does the "yield" keyword do in Python?

提问人:Alex. S. 提问时间:10/24/2008 最后编辑:LeoAlex. S. 更新时间:8/10/2023 访问量:3269893


答:

456赞 Douglas Mayle 10/24/2008 #1

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

就代码而言,该函数的作用类似于迭代器,因此当您扩展列表时,它会一次向新列表添加一个元素。get_child_candidates

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

评论

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

它返回了一个生成器。我对 Python 不是特别熟悉,但我相信它与 C# 的迭代器块是一样的,如果你熟悉它们的话。

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

评论

0赞 tzot 5/29/2023
实际上,该方法暂停;保存局部变量和“程序计数器”的状态机是调用帧对象本身,因为它位于堆中,而不是堆栈中。
762赞 Jason Baker 10/24/2008 #3

可以这样想:

迭代器只是一个听起来很花哨的术语,用于表示具有方法的对象。因此,一个 yield-ed 函数最终是这样的: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

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

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

评论

1赞 jfs 10/25/2008
__getitem__可以定义而不是 .例如:,它将打印:0,10,20,...,90__iter__class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)
37赞 Peter 5/6/2017
我在 Python 3.6 中尝试了这个例子,如果我创建,该变量不再调用函数,而只有一个函数。我以为我会提到它。iterator = some_function()iteratornext()__next__()
0赞 SystematicDisintegration 5/12/2020
您编写的循环实现在哪里调用 的方法 ,实例化实例 ?for__iter__iteratorit
2赞 gioxc88 10/15/2020
不幸的是,这个答案根本不是真的。这不是 python 解释器对生成器所做的。它不是从生成器函数开始创建一个类,并实现 和 。这篇文章解释了它在引擎盖下的实际作用 stackoverflow.com/questions/45723893/...。引用 Hettinger @Raymond,“生成器不是在内部实现的,如纯 python 类所示。相反,它们与常规函数共享大部分相同的逻辑。__iter____next__
17888赞 Bite code 10/24/2008 #4

要了解什么是生成器,您必须了解什么是生成器。在理解生成器之前,必须了解可迭代对象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...listsstrings

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

发电机

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

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

它只是一样的,只是你用了而不是 .但是,您不能执行第二次,因为生成器只能使用一次:它们计算 0,然后忘记它并计算 1,并在计算 4 后结束,一个接一个。()[]for i in mygenerator

屈服

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

现在最困难的部分:

第一次调用从函数创建的生成器对象时,它将从头开始运行函数中的代码,直到命中,然后它将返回循环的第一个值。然后,每个后续调用将运行您在函数中编写的循环的另一次迭代,并返回下一个值。这将一直持续到生成器被认为是空的,当函数运行而不点击时就会发生这种情况。这可能是因为循环已经结束,或者因为您不再满足 .foryieldyield"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,你最好的朋友

该模块包含用于操作可迭代对象的特殊函数。有没有想过复制发电机? 串联两台发电机?使用单行代码对嵌套列表中的值进行分组? 没有创建另一个列表?itertoolsMap / 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循环如何工作的信息。

评论

744赞 Matthias Fripp 5/24/2017
yield并不像这个答案所暗示的那么神奇。当您在任意位置调用包含语句的函数时,您将获得一个生成器对象,但不会运行任何代码。然后,每次从生成器中提取对象时,Python 都会在函数中执行代码,直到它出现语句,然后暂停并交付对象。当您提取另一个对象时,Python 会在 之后继续,并继续直到到达另一个对象(通常是相同的对象,但一次迭代后)。这种情况一直持续到函数结束,此时生成器被视为已耗尽。yieldyieldyieldyield
86赞 picmate 涅 2/16/2018
“这些可迭代对象非常方便......但是你把所有的值都存储在内存中,这并不总是你想要的“,要么是错误的,要么是令人困惑的。迭代对象在对可迭代对象调用 iter() 时返回一个迭代器,迭代器并不总是必须将其值存储在内存中,具体取决于 iter 方法的实现,它也可以按需按顺序生成值。
34赞 WoJ 5/7/2020
最好在这个伟大的答案中添加为什么它是一样的,只是你用了 () 而不是 [],特别是是什么(可能会混淆元组)。()
46赞 alani 6/6/2020
@MatthiasFripp “这一直持续到函数结束为止”——或者遇到语句。(在包含 的函数中是允许的,前提是它不指定返回值。returnreturnyield
19赞 Jacob Ward 12/3/2020
yield 语句暂停函数的执行并将值发送回调用方,但保留足够的状态以使函数能够从中断的位置恢复。恢复后,该函数在最后一次产量运行后立即继续执行。这允许其代码随着时间的推移生成一系列值,而不是一次计算它们并像列表一样将它们发送回去。
218赞 tzot 10/24/2008 #5

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

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

  • 我打电话给你,告诉你我想要一个以特定方式计算的数字序列,我让你知道算法是什么。
    此步骤对应于定义生成器函数,即包含产量的函数。
  • 过了一会儿,我告诉你,“好吧,准备好告诉我数字的顺序”。
    此步骤对应于调用返回生成器对象的生成器函数。请注意,你还没有告诉我任何数字;你只需拿起你的纸和铅笔。
  • 我问你,“告诉我下一个数字”,你告诉我第一个数字;在那之后,你等着我问你下一个号码。你的工作是记住你在哪里,你已经说过什么数字,以及下一个数字是什么。我不在乎细节。
    此步骤对应于在生成器对象上调用 next(generator)。
    (在 Python 2 中,是 generator 对象的一个方法;在 Python 3 中,它被命名为 ,但调用它的正确方法是使用内置函数,就像 和
    .next.__next__next()len().__len__)
  • ...重复上一步,直到...
  • 最终,你可能会走到尽头。你不告诉我一个数字;你只是喊道:“抓住你的马!大功告成!没有更多的数字了!
    此步骤对应于生成器对象结束其作业,并引发 StopIteration 异常。
    生成器函数不需要引发异常。当函数结束或发出 .
    return

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

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

for item in sequence:

不管是列表、字符串、字典还是如上所述的生成器对象;结果是一样的:你一个接一个地从序列中读取项目。sequence

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

有关更准确的信息,请阅读 Python 文档中的迭代器类型yield 语句生成器

317赞 Claudiu 10/24/2008 #6

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

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

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

2526赞 user28409 10/26/2008 #7

理解的捷径yield

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

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

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

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

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

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

Python 执行以下两个步骤:

  1. 获取 的迭代器:mylist

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

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

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

    继续在步骤 1 返回的迭代器上调用该方法。返回值 from 被赋值给并执行循环体。如果从内部引发异常,则表示迭代器中没有更多值,并且循环退出。next()next()xStopIterationnext()

事实是,每当 Python 想要遍历对象的内容时,它都会执行上述两个步骤 - 因此它可以是 for 循环,但它也可以是类似(其中是 Python 列表)的代码。otherlist.extend(mylist)otherlist

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

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

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

请注意,循环不知道它正在处理什么样的对象 - 它只是遵循迭代器协议,并且很乐意在调用 .内置列表逐个返回其项目,字典逐个返回,文件逐个返回,等等。发电机回来了......嗯,这就是用武之地:fornext()yield

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

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

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

为什么要使用发电机?

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

评论

43赞 DanielSank 6/18/2017
“当你看到一个带有 yield 语句的函数时,应用这个简单的技巧来理解会发生什么”这难道不是完全忽略了一个事实,即你可以进入发电机,这是发电机的很大一部分?send
18赞 Pedro 9/14/2017
“它可能是一个 for 循环,但它也可能是这样的代码” -> 这是不正确的。 就地修改列表,并且不返回可迭代对象。尝试循环将失败,并返回 ,因为隐式返回 ,并且您无法循环。otherlist.extend(mylist)extend()otherlist.extend(mylist)TypeErrorextend()NoneNone
16赞 today 12/27/2017
@pedro 你误解了这句话。这意味着 python 在执行时执行 on (not on ) 上提到的两个步骤。mylistotherlistotherlist.extend(mylist)
654赞 ninjagecko 6/19/2011 #8

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

  1. 如果编译器在函数内的任何位置检测到关键字,则该函数将不再通过语句返回。相反,它会立即返回一个称为生成器的惰性“待处理列表”对象yieldreturn
  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

基本上,每当遇到语句时,函数都会暂停并保存其状态,然后根据 python 迭代器协议发出“'list' 中的下一个返回值”(对于某些语法结构,例如重复调用和捕获异常的 for 循环等)。您可能遇到过带有生成器表达式的生成器;生成器函数更强大,因为您可以将参数传递回暂停的生成器函数,使用它们来实现协程。稍后会详细介绍。yieldnext()StopIteration


基本示例 ('list')

让我们定义一个类似于 Python 的 .调用 RETURNS 一个生成器:makeRangerangemakeRange(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]

为了更好地感受发电机,您可以尝试使用该模块(请务必使用而不是在必要时使用)。例如,您甚至可以使用生成器来实现无限长的惰性列表,例如 .您可以实现自己的 ,或者在 while 循环中使用关键字来实现。itertoolschain.from_iterablechainitertools.count()def enumerate(iterable): zip(count(), iterable)yield

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


幕后花絮

这就是“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

内置函数只调用 objects 函数,该函数是“迭代协议”的一部分,可以在所有迭代器中找到。你可以手动使用这个函数(以及迭代协议的其他部分)来实现一些花哨的东西,通常是以牺牲可读性为代价的,所以尽量避免这样做......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)

协程(通常通过关键字接受输入的生成器,例如,作为双向通信的一种形式)基本上是一种允许暂停自身并请求输入(例如,下一步应该做什么)的计算。当协程自身暂停时(当正在运行的协程最终命中关键字时),计算将暂停,控制权被反转(产生)回“调用”函数(请求计算值的帧)。暂停的生成器/协程保持暂停状态,直到另一个调用函数(可能是不同的函数/上下文)请求下一个值来取消暂停它(通常传递输入数据以将暂停的逻辑内部定向到协程的代码)。yieldnextInput = yield nextOutputyieldnext

您可以将 Python 协程视为惰性增量挂起列表,其中下一个元素不仅取决于先前的计算,还取决于您可能选择在生成过程中注入的输入。


细节

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

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

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

因此,万一您未能做这样的事情......

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

...然后记住生成器是一个迭代器;也就是说,它是一次性使用的。如果你想重用它,你应该再次调用。如果需要使用结果两次,请将结果转换为列表并将其存储在变量中。那些绝对需要克隆生成器的人(例如,那些正在做可怕的黑客元编程的人)可以在绝对必要的情况下使用 itertools.tee在 Python 3 中仍然有效),因为可复制迭代器 Python PEP 标准提案已被推迟。myRange(...)x = list(myRange(5))

132赞 Dustin Getz 10/4/2012 #9

以下是一些 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)
246赞 RBansal 1/16/2013 #10

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

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

在第二种情况下,只给你一个生成器。生成器是一个可迭代的,这意味着你可以在循环中使用它,但每个值只能访问一次。所有值也不会同时存储在内存中;生成器对象“记住”你上次调用它时它在循环中的位置——这样,如果你使用一个可迭代对象来(比如)数到 500 亿,你就不必一次数到 500 亿,并存储 500 亿个数字来计数。barfor

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

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

评论

4赞 It'sNotALie. 3/22/2019
请注意 - 在 Python 3 中,还返回一个生成器而不是列表,因此您也会看到类似的想法,除了 / 被覆盖以显示更好的结果,在这种情况下。range__repr____str__range(1, 10, 2)
308赞 Daniel 1/19/2013 #11

对于那些喜欢最小工作示例的人,请冥想这个交互式 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
121赞 johnzachary 1/28/2013 #12

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

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

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

217赞 aestrivex 4/4/2013 #13

在描述如何使用生成器的众多优秀答案中,有一种类型的答案我还没有给出。以下是编程语言理论的答案:

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

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

以这种更一般的形式,可以通过两种方式实现延续。这样,程序的堆栈实际上被保存,然后在调用延续时,堆栈被恢复。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)

重申一下:延续可以保存程序堆栈的状态,生成器可以保存迭代的状态。这意味着延续比生成器更强大,而且生成器也更容易。它们对语言设计者来说更容易实现,对程序员来说也更容易使用(如果你有时间燃烧,试着阅读和理解这个关于延续和调用/抄送的页面)。

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

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

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

88赞 Evgeni Sergeev 6/15/2013 #14

这是做什么的心理形象。yield

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

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

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

所以它是一种冻结函数,生成器挂在上面。

随后调用时,它会将函数的归属检索到堆栈中并重新激活它。该函数继续从它中断的地方开始计算,忘记了它刚刚在冷存储中度过了永恒的事实。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_framedir(..)

139赞 alinsoar 8/22/2013 #15

从编程的角度来看,迭代器是作为 thunk 实现的。

语法的背后必须有一个语义。了解语法背后的语义概念非常重要。例如,如果要创建闭包,则有 和 。发电机只是一个闭合器吗?生成器会保存堆栈吗?可以肯定的是,它不会像那样保存堆栈,因为 python 执行模型不是一棵树......可以肯定的是,生成器不是一个简单的闭包,否则函数和生成器将是同一个概念。因此,我试图通过从更基本的闭包概念构建生成器来弄清楚生成器的概念......yieldlambdadefcall-with-current-continuation

为了实现迭代器、生成器和线程池等并发执行等,可以使用发送到闭包对象的消息,该对象具有调度程序,调度程序应答“消息”。

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

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

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

Welcome to 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) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->
103赞 Engin OZTURK 12/20/2013 #16

下面是一个简单的示例:

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 "writing result {}".format(n)

输出:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
writing result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
writing result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
writing 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

评论

0赞 Engin OZTURK 7/2/2018
你是对的。但是,看到“屈服”的行为对流量有什么影响呢?我可以以数学学的名义更改算法。对“收益率”进行不同的评估会有所帮助吗?
169赞 Mike McKerns 2/4/2014 #17

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

为了帮助理解 a 在以下代码中的作用,您可以使用手指跟踪任何具有 .每次手指敲击 时,都必须等待 a 或 a 输入。调用 a 时,跟踪代码,直到命中 ...右侧的代码被评估并返回给调用方...然后你等着。再次调用时,将对代码执行另一个循环。但是,您会注意到,在协程中,也可以与...这会将一个值从调用方发送到 yielding 函数。如果给出 a,则接收发送的值,并将其从左侧吐出......然后,通过代码的跟踪继续进行,直到再次点击(在末尾返回值,就像被调用一样)。yieldyieldyieldnextsendnextyieldyieldnextyieldsendsendyieldyieldnext

例如:

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

评论

3赞 00prometheus 12/5/2015
可爱!蹦床(在Lisp意义上)。不经常看到这些!
165赞 Sławomir Lenart 7/25/2014 #18

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

yield from <expr>

来自 PEP 380 -- 委托给子生成器的语法

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

当一个生成器重新生成另一个生成器生成的值时,新语法还为优化提供了一些机会。

此外,这将引入(从 Python 3.5 开始):

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

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

55赞 Will 5/20/2015 #19

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

563赞 Russia Must Remove Putin 6/25/2015 #20

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

答案 大纲/摘要

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

发电机:

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

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

yield提供 实现迭代器协议的简单方法,由以下两种方法定义:和 。这两种方法 使对象成为可以使用抽象库进行类型检查的迭代器 模块中的类。__iter____next__Iteratorcollections

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)

首先,我们必须使用内置函数对生成器进行排队,接下来。它将 调用相应的 OR 方法,具体取决于 您正在使用的 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

现在,回想一下它在 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

现在模拟向账户再增加 1,000 加上账户的回报率 (60.0):

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

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

其他方法:关闭和投掷

该方法在函数的点上引发 处决被冻结。这也将被 so you 调用 可以将任何清理代码放在您处理的地方:closeGeneratorExit__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

结论

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

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

事实证明,这做了很多。我相信我可以添加更多 详尽的例子。如果您想要更多或有一些建设性的批评,请发表评论告诉我 下面。yield


附录:

对顶级/公认答案的批评**

  • 它对什么是可迭代的感到困惑,只是以列表为例。请参阅我上面的参考资料,但总而言之:可迭代对象有一个返回迭代器的方法。迭代器还提供了一个方法,该方法由循环隐式调用,直到它升起,一旦它升起,它将继续这样做。__iter__.__next__forStopIterationStopIteration
  • 然后,它使用生成器表达式来描述生成器是什么。由于生成器表达式只是创建迭代器的便捷方法,它只会混淆问题,我们还没有进入该部分。yield
  • 控制生成器耗尽中,他调用了该方法(仅适用于 Python 2),而他应该使用内置函数 .调用将是一个适当的间接层,因为他的代码在 Python 3 中不起作用。.nextnextnext(obj)
  • 迭代工具?这与做什么完全无关。yield
  • 没有讨论 Python 3 中提供的方法以及新功能。yieldyield 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 中保持原样 不好,恕我直言。

我的投票是它是一个语法错误,因为你没有得到你所期望的 语法。

我同意这对我们来说是一个明智的地方,就像任何代码一样 依靠当前的行为真的太聪明了 维护。

就实现目标而言,我们可能需要:

  • 语法3.7 中的 Warning 或 DeprecationWarning
  • 2.7.x 中的 Py3k 警告
  • 语法3.8 中的错误

干杯,尼克。

-- 尼克·科格伦 |Ncoghlan 在 gmail.com |澳大利亚布里斯班

此外,还有一个悬而未决的问题(10544),它似乎指向这绝不是一个好主意的方向(PyPy,一个用Python编写的Python实现,已经提出了语法警告。

总而言之,直到 CPython 的开发人员告诉我们:不要在生成器表达式或理解中输入收益

生成器中的语句return

Python 3 中:

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

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

脚注

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

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

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

76赞 Mangu Singh Rajpurohit 7/29/2015 #21

就像每个答案都建议的那样,用于创建序列生成器。它用于动态生成一些序列。例如,在网络上逐行读取文件时,可以按如下方式使用该函数:yieldyield

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

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

for line in getNextLines():
    doSomeThing(line)

执行控制转移陷阱

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

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

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"
69赞 Kaleem Ullah 9/1/2015 #22

屈服是一个对象

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

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

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

就像 CUDA 语言中的 barrier 一样,它不会转移控制权,直到它得到 完成。

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

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

57赞 Bahtiyar Özdere 11/19/2015 #23

关键字只是收集返回结果。想想喜欢yieldyieldreturn +=

46赞 Dimitris Fasarakis Hilliard 2/21/2016 #24

下面是一种基于简单的方法,用于计算斐波那契数列,解释道:yield

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

当你在REPL中输入它,然后尝试调用它时,你会得到一个神秘的结果:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

这是因为存在向 Python 发出的信号,表明您要创建一个生成器,即按需生成值的对象。yield

那么,如何生成这些值呢?这可以通过使用内置函数直接完成,也可以通过将其提供给使用值的构造来间接完成。next

使用内置函数,您可以直接调用 /,强制生成器生成一个值:next().next__next__

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

间接地,如果你向循环、初始值设定项、初始值设定项或其他任何需要生成/生成值的对象提供,你将“消耗”生成器,直到它不能再生成值(并且它返回):fibforlisttuple

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

同样,使用初始值设定项:tuple

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

生成器与函数的不同之处在于它是惰性的。它通过维护其本地状态并允许您在需要时恢复来实现这一点。

首次通过调用它进行调用时:fib

f = fib()

Python 编译函数,遇到关键字,然后简单地返回一个生成器对象。似乎不是很有帮助。yield

当您随后请求它直接或间接生成第一个值时,它会执行它找到的所有语句,直到遇到 ,然后它会返回您提供给的值并暂停。为了更好地演示这一点,让我们使用一些调用(在 Python 2 上替换为 if):yieldyieldprintprint "text"

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

现在,在 REPL 中输入:

>>> gen = yielder("Hello, yield!")

您现在有一个 Generator 对象正在等待命令,以便它生成一个值。使用并查看打印的内容:next

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

未加引号的结果是打印的结果。引用的结果是从 返回的结果。现在再次致电:yieldnext

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

生成器记得它暂停并从那里恢复。打印下一条消息,并再次执行对语句暂停的搜索(由于循环)。yield valueyieldwhile

74赞 smwikipedia 3/25/2016 #25

(我下面的回答只是从使用 Python 生成器的角度来讲,而不是生成器机制的底层实现,这涉及到一些堆栈和堆操作的技巧。

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

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

据我了解,当我们要处理一堆数据时,我们通常先将数据存储在某个地方,然后逐个处理。但这种幼稚的做法是有问题的。如果数据量很大,事先将它们作为一个整体存储是很昂贵的。因此,与其直接存储数据本身,不如间接存储某种元数据,即计算数据的逻辑

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

  1. OO方法,我们包装元数据。这就是所谓的谁实现了迭代器协议(即 和 方法)。这也是常见的迭代器设计模式as a classiterator__next__()__iter__()
  2. 函数式方法,我们包装元数据。这是 所谓的.但在引擎盖下,返回的迭代器仍然是迭代器,因为它也实现了迭代器协议。as a functiongenerator functiongenerator objectIS-A

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

289赞 Bob Stein 3/25/2016 #26

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

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


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 的函数时,它实际上根本不会执行。它返回一个迭代器对象,该对象会记住它离开的位置。每次调用迭代器(这发生在 for 循环中)时,执行会向前推进到下一个产量。 引发 StopIteration 并结束序列(这是 for 循环的自然结束)。next()return

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

>>> 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 执行,直到迭代器前进。

评论

1赞 D-odu 2/16/2023
我喜欢像你这样的人。清楚地解释事情,没有不必要的行话
0赞 Bob Stein 2/16/2023
@D-odu 如果我赢得了你的赞美,真正要感谢的人是那些眼睛盯着听我说话的人。我会因为注意到并试图避免这种情况而受到赞扬。
50赞 Christophe Roussy 6/22/2016 #27

又一个TL;博士

列表中的迭代器:返回列表的下一个元素next()

迭代器生成器:将动态计算下一个元素(执行代码)next()

您可以将 yield/generator 视为一种从外部手动运行控制流的方法(例如继续循环一步),通过调用 ,无论该多么复杂。next

注意:生成器不是正常功能。它像局部变量(堆栈)一样记住以前的状态。有关详细说明,请参阅其他答案或文章。生成器只能迭代一次。你可以没有,但它不会那么好,所以它可以被认为是“非常好”的语言糖。yield

67赞 Tom Fuller 9/10/2016 #28

许多人使用 而不是 ,但在某些情况下可以更高效、更易于使用。returnyieldyield

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

return(在函数中)

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

yield (在函数中)

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

这是代码的结果:

Output

如您所见,这两个函数都做同样的事情。唯一的区别是给出一个列表并给出一个生成器。return_dates()yield_dates()

一个现实生活中的例子是,比如逐行读取文件,或者你只想做一个生成器。

73赞 redbandit 10/13/2016 #29

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

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

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

80赞 Gavriel Cohen 1/2/2017 #30

一个简单的例子来理解它是什么: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

评论

8赞 user9074332 2/5/2020
你确定这个输出吗?如果您使用 ?否则,我相信默认行为会将每个数字放在新行上print(i, end=' ')
3赞 Gavriel Cohen 2/5/2020
@user9074332,你是对的,但它写在一行上以方便理解
39赞 Ahmad Ismail 4/30/2017 #31

yield 与 return 相似。区别在于:

yield 使函数可迭代(在以下示例中,函数变为可迭代)。
它的本质含义是下次调用该函数时,它将从它离开的位置(即 行之后)继续。
primes(n = 1)yield expression

def isprime(n):
    if n == 1:
        return False
    for x in range(2, n):
        if n % x == 0:
            return False
    else:
        return True

def primes(n = 1):
   while(True):
       if isprime(n): yield n
       n += 1 

for n in primes():
    if n > 100: break
    print(n)

在上面的例子中,如果为 true,它将返回质数。在下一次迭代中,它将从下一行继续isprime(n)

n += 1  
22赞 Chen A. 10/3/2017 #32

这里所有的答案都很棒;但其中只有一个(得票最多的一个)与你的代码如何工作有关。其他的则与发电机及其工作原理有关。

所以我不会重复发电机是什么或产量有什么作用;我认为这些都包含在现有的很好的答案中。但是,在花了几个小时试图理解与您的代码类似的代码之后,我将分解它的工作原理。

代码遍历二叉树结构。让我们以这棵树为例:

    5
   / \
  3   6
 / \   \
1   4   8

还有另一个更简单的二进制搜索树遍历实现:

class Node(object):
..
def __iter__(self):
    if self.has_left_child():
        for child in self.left:
            yield child

    yield self.val

    if self.has_right_child():
        for child in self.right:
            yield child

执行代码位于对象上,其实现方式如下:Tree__iter__

def __iter__(self):

    class EmptyIter():
        def next(self):
            raise StopIteration

    if self.root:
        return self.root.__iter__()
    return EmptyIter()

该语句可以替换为 ;Python 将其翻译为while candidatesfor element in tree

it = iter(TreeObj)  # returns iter(self.root) which calls self.root.__iter__()
for element in it: 
    .. process element .. 

因为函数是一个生成器,所以它里面的代码是每次迭代执行的。因此,执行将如下所示:Node.__iter__

  1. root 元素是 first;检查它是否留下了子项并迭代它们(我们称它为 it1,因为它是第一个迭代器对象)for
  2. 它有一个子项,因此被执行。创建一个新的迭代器,该迭代器是 Node 对象本身 (it2)forfor child in self.leftself.left
  3. 与 2 的逻辑相同,并创建一个新的 (it3)iterator
  4. 现在我们到达了树的左端。 没有左子,所以它继续和it3yield self.value
  5. 在下一次调用它时,它会引发并存在,因为它没有正确的子项(它到达函数的末尾而不产生任何结果)next(it3)StopIteration
  6. it1并且仍然活跃 - 它们没有耗尽,调用会产生价值,而不是提高it2next(it2)StopIteration
  7. 现在我们回到上下文,并在停止的地方继续调用:紧接在语句之后。由于它不再有左子,它继续并产生它。it2next(it2)yield childself.val

这里的问题是,每次迭代都会创建子迭代器来遍历树,并保存当前迭代器的状态。一旦到达终点,它就会遍历堆栈,并以正确的顺序返回值(最小值首先产生值)。

您的代码示例在不同的技术中执行了类似的操作:它为每个子元素填充一个元素列表,然后在下一次迭代中弹出它并在当前对象上运行函数代码(因此是 )。self

我希望这对这个传奇话题有所贡献。我花了好几个小时画这个过程来理解它。

153赞 Wizard 11/14/2017 #33

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

我假设你已经学会了这句话。return

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

  1. 尝试使用 获取 num_list。return
def num_list(n):
    for i in range(n):
        return i

运行它:

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

看,你只得到一个数字,而不是它们的列表。 永远不要让你快乐地占上风,只是实施一次就退出。return

  1. 来了yield

替换为 :returnyield

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]

现在,你赢了,得到了所有的数字。

与运行一次和停止的运行时间进行比较,您计划的运行时间。 您可以将 和 解释为 。这称为 。returnyieldreturnreturn one of themyieldreturn all of themiterable

  1. 我们可以用yieldreturn
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

列表输出和对象输出之间的区别在于:returnyield

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

总之,作为 grok 的比喻:

  • return并且是双胞胎yield
  • list并且是双胞胎generator

评论

3赞 Mike S 8/23/2018
这是可以理解的,但一个主要区别是,一个函数/方法可以有多个产量。在这一点上,这个类比完全崩溃了。Yield 会记住它在函数中的位置,因此下次调用 next() 时,函数会继续执行下一个函数。我认为这很重要,应该表达出来。yield
31赞 Andy Jazz 9/9/2018 #34

在Python(一种特殊类型)中,用于生成一系列值,而关键字就像生成器函数的关键字一样。generatorsiteratorsyieldreturn

yield 关键字所做的另一件有趣的事情是保存生成器函数的状态

因此,我们可以在每次产量时将 a 设置为不同的值。numbergenerator

下面是一个实例:

def getPrimes(number):
    while True:
        if isPrime(number):
            number = yield number     # a miracle occurs here
        number += 1

def printSuccessivePrimes(iterations, base=10):
    primeGenerator = getPrimes(base)
    primeGenerator.send(None)
    for power in range(iterations):
        print(primeGenerator.send(base ** power))
30赞 thavan 2/22/2019 #35

yield产生一些东西。这就像有人要求你做 5 个纸杯蛋糕。如果你吃完了至少一个纸杯蛋糕,你可以把它给他们吃,同时你做其他蛋糕。

In [4]: def make_cake(numbers):
   ...:     for i in range(numbers):
   ...:         yield 'Cake {}'.format(i)
   ...:

In [5]: factory = make_cake(5)

这里被称为发电机,它让你做蛋糕。如果调用 ,则会得到一个生成器,而不是运行该函数。这是因为当函数中存在关键字时,它就会成为生成器。factorymake_functionyield

In [7]: next(factory)
Out[7]: 'Cake 0'

In [8]: next(factory)
Out[8]: 'Cake 1'

In [9]: next(factory)
Out[9]: 'Cake 2'

In [10]: next(factory)
Out[10]: 'Cake 3'

In [11]: next(factory)
Out[11]: 'Cake 4'

他们吃光了所有的蛋糕,但他们又要了一个。

In [12]: next(factory)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-12-0f5c45da9774> in <module>
----> 1 next(factory)

StopIteration:

他们被告知不要再问更多了。因此,一旦你消耗了发电机,你就完成了它。如果你想要更多的蛋糕,你需要再次打电话。这就像又下了纸杯蛋糕的订单。make_cake

In [13]: factory = make_cake(3)

In [14]: for cake in factory:
    ...:     print(cake)
    ...:
Cake 0
Cake 1
Cake 2

您还可以将 for 循环与上面的生成器一起使用。

再举一个例子:假设每当您要求时,您都需要一个随机密码。

In [22]: import random

In [23]: import string

In [24]: def random_password_generator():
    ...:     while True:
    ...:         yield ''.join([random.choice(string.ascii_letters) for _ in range(8)])
    ...:

In [25]: rpg = random_password_generator()

In [26]: for i in range(3):
    ...:     print(next(rpg))
    ...:
FXpUBhhH
DdUDHoHn
dvtebEqG

In [27]: next(rpg)
Out[27]: 'mJbYRMNo'

这是一个生成器,它可以生成无限数量的随机密码。因此,我们也可以说,当我们不知道序列的长度时,生成器很有用,这与具有有限数量的元素的列表不同。rpg

84赞 Rafael 3/23/2019 #36

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

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

机器代码:

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

惰性迭代器

更准确地说,这个生成器是一个惰性迭代器!迭代器是一个对象,它帮助我们遍历一系列对象。之所以称为惰性,是因为它不会将序列的所有项目加载到内存中,直到它们被需要。在前面的示例中使用 是从迭代器获取下一项的显式方法。隐式方法是使用 for 循环:next

for barcode in barcode_generator():
    print(barcode)

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

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

实际应用?

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

12赞 Swati Srivastava 1/17/2020 #37

Python 中的 yield 在某种程度上类似于 return 语句,只是有一些区别。如果必须从函数返回多个值,return 语句会以列表形式返回所有值,并且必须将其存储在调用方块的内存中。但是,如果我们不想使用额外的内存怎么办?相反,我们希望在需要时从函数中获取值。这就是产量的用武之地。考虑以下功能:-

def fun():
   yield 1
   yield 2
   yield 3

来电者是:-

def caller():
   print ('First value printing')
   print (fun())
   print ('Second value printing')
   print (fun())
   print ('Third value printing')
   print (fun())

上面的代码段(调用方函数)在调用时输出:-

First value printing
1
Second value printing
2
Third value printing
3

从上面可以看出,yield 向其调用者返回一个值,但是当再次调用该函数时,它不是从第一个语句开始,而是从 yield 之后的语句开始。在上面的示例中,打印了“第一个值打印”并调用了该函数。1 已返回并打印。然后打印“第二值打印”,并再次调用fun()。它没有打印 1(第一个语句),而是返回 2,即在产生 1 之后的语句。进一步重复相同的过程。

评论

0赞 Funny Geeks 5/1/2020
如果尝试运行此代码,则不会打印数字。相反,它打印由 (类似于print(fun())fun()<generator object fun at 0x6fffffe795c8>)
0赞 Swati Srivastava 5/3/2020
@FunnyGeeks我在Jupyter Notebook上运行了相同的代码,它工作正常。另外,这里的重点是解释 yield 关键字的工作原理。该片段仅用于演示目的。
0赞 Funny Geeks 5/4/2020
我在 cygwin 控制台的 python2 和 python3 中尝试过。它没有用。github.com/ImAmARobot/PythonTest
16赞 Aaron_ab 8/22/2020 #38

也可以将数据发送回生成器!

事实上,正如这里的许多答案所解释的那样,使用 .yieldgenerator

您可以使用关键字将数据发送回“实时”生成器yield

例:

假设我们有一个从英语翻译成其他语言的方法。在它开始的时候,它做了一些很重的事情,应该做一次。我们希望这个方法永远运行(不知道为什么.. :)),并接收要翻译的单词。

def translator():
    # load all the words in English language and the translation to 'other lang'
    my_words_dict = {'hello': 'hello in other language', 'dog': 'dog in other language'}

    while True:
        word = (yield)
        yield my_words_dict.get(word, 'Unknown word...')

运行:

my_words_translator = translator()

next(my_words_translator)
print(my_words_translator.send('dog'))

next(my_words_translator)
print(my_words_translator.send('cat'))

将打印:

dog in other language
Unknown word...

总结一下:

使用生成器内部的方法将数据发送回生成器。为了实现这一点,使用了 a。send(yield)

3赞 Siva Sankar 6/14/2021 #39

函数 - 返回。

生成器 - 收益率(包含一个或多个收益率和零或多个回报)。

names = ['Sam', 'Sarah', 'Thomas', 'James']


# Using function
def greet(name) :
    return f'Hi, my name is {name}.'
    
for each_name in names:
    print(greet(each_name))

# Output:   
>>>Hi, my name is Sam.
>>>Hi, my name is Sarah.
>>>Hi, my name is Thomas.
>>>Hi, my name is James.


# using generator
def greetings(names) :
    for each_name in names:
        yield f'Hi, my name is {each_name}.'
 
for greet_name in greetings(names):
    print (greet_name)

# Output:    
>>>Hi, my name is Sam.
>>>Hi, my name is Sarah.
>>>Hi, my name is Thomas.
>>>Hi, my name is James.

生成器看起来像一个函数,但行为就像一个迭代器。

生成器从它被释放(或产生)的地方继续执行。恢复后,该函数在最后一次产量运行后立即继续执行。这允许其代码随着时间的推移生成一系列值,而不是它们一次计算所有值并将它们像列表一样发送回去。

def function():
    yield 1 # return this first
    yield 2 # start continue from here (yield don't execute above code once executed)
    yield 3 # give this at last (yield don't execute above code once executed)

for processed_data in function(): 
    print(processed_data)
    
#Output:

>>>1
>>>2
>>>3

注意: 产量不应该在尝试中......最后构造。

5赞 Mayank Maheshwari 6/15/2021 #40

通常,它用于在函数之外创建迭代器。 将 'yield' 视为函数的 append(),将函数视为数组。 如果满足某些条件,则可以在函数中添加该值以使其成为迭代器。

arr=[]
if 2>0:
   arr.append(2)

def func():
   if 2>0:
      yield 2

两者的输出将相同。

使用 yield 的主要优点是创建迭代器。 迭代器在实例化时不会计算每个项的值。他们只在你要求时才计算它。这称为惰性评估。

评论

0赞 jiten 9/27/2021
+1.感谢您的简单解释,但要求更多,例如如何在您或帖子的代码中轻松评估。看不出你的两个代码是如何以不同的方式工作的。
9赞 ToTamire 10/4/2021 #41

简单的答案

当函数包含至少一个语句时,该函数自动成为生成器函数。调用生成器函数时,python 会在生成器函数中执行代码,直到语句发生。 语句冻结函数的所有内部状态。当您再次调用生成器函数时,python 会从冻结位置继续执行生成器函数中的代码,直到语句一次又一次地出现。生成器函数执行代码,直到生成器函数在没有语句的情况下用完。yieldyieldyieldyieldyield

基准

创建一个列表并返回它:

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

@profile
def function():
    my_sum = 0
    my_values = my_range(1000000)
    for my_value in my_values:
        my_sum += my_value

function()

结果:

Total time: 1.07901 s
Timer unit: 1e-06 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     9                                           @profile
    10                                           def function():
    11         1          1.1      1.1      0.0      my_sum = 0
    12         1     494875.0 494875.0     45.9      my_values = my_range(1000000)
    13   1000001     262842.1      0.3     24.4      for my_value in my_values:
    14   1000000     321289.8      0.3     29.8          my_sum += my_value



Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     9   40.168 MiB   40.168 MiB           1   @profile
    10                                         def function():
    11   40.168 MiB    0.000 MiB           1       my_sum = 0
    12   78.914 MiB   38.746 MiB           1       my_values = my_range(1000000)
    13   78.941 MiB    0.012 MiB     1000001       for my_value in my_values:
    14   78.941 MiB    0.016 MiB     1000000           my_sum += my_value

动态生成值:

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

@profile
def function():
    my_sum = 0
    
    for my_value in my_range(1000000):
        my_sum += my_value

function()

结果:

Total time: 1.24841 s
Timer unit: 1e-06 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     7                                           @profile
     8                                           def function():
     9         1          1.1      1.1      0.0      my_sum = 0
    10
    11   1000001     895617.3      0.9     71.7      for my_value in my_range(1000000):
    12   1000000     352793.7      0.4     28.3          my_sum += my_value



Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     7   40.168 MiB   40.168 MiB           1   @profile
     8                                         def function():
     9   40.168 MiB    0.000 MiB           1       my_sum = 0
    10
    11   40.203 MiB    0.016 MiB     1000001       for my_value in my_range(1000000):
    12   40.203 MiB    0.020 MiB     1000000           my_sum += my_value

总结

生成器函数需要比返回列表的函数多一点时间来执行,但它使用的内存要少得多。

8赞 Ted Shaneyfelt 11/9/2021 #42

一个简单的用例:

>>> def foo():
    yield 100
    yield 20
    yield 3

    
>>> for i in foo(): print(i)

100
20
3
>>> 

工作原理:调用时,该函数会立即返回一个对象。该对象可以传递给 next() 函数。每当调用 next() 函数时,函数都会运行到下一个产量,并为 next() 函数提供返回值。

在后台,for 循环识别该对象是生成器对象,并使用 next() 获取下一个值。

在某些语言(如 ES6 及更高版本)中,它的实现方式略有不同,因此 next 是 generator 对象的成员函数,每次调用方获取下一个值时,都可以从调用方传递值。因此,如果 result 是生成器,那么您可以执行类似 y = result.next(555) 之类的操作,并且生成值的程序可能会说 z = yield 999。y 的值将是 999,下一个从产量中得到,z 的值将是 555,该收益率从下一个得到。Python get 和 send 方法具有类似的效果。

5赞 michalmonday 12/5/2021 #43

生成器允许立即获取单个已处理的项目(无需等待整个集合被处理)。下面的示例对此进行了说明。

import time

def get_gen():
    for i in range(10):
        yield i
        time.sleep(1)

def get_list():
    ret = []
    for i in range(10):
        ret.append(i)
        time.sleep(1)
    return ret


start_time = time.time()
print('get_gen iteration (individual results come immediately)')
for i in get_gen():
    print(f'result arrived after: {time.time() - start_time:.0f} seconds')
print()

start_time = time.time()
print('get_list iteration (results come all at once)') 
for i in get_list():
    print(f'result arrived after: {time.time() - start_time:.0f} seconds')

get_gen iteration (individual results come immediately)
result arrived after: 0 seconds
result arrived after: 1 seconds
result arrived after: 2 seconds
result arrived after: 3 seconds
result arrived after: 4 seconds
result arrived after: 5 seconds
result arrived after: 6 seconds
result arrived after: 7 seconds
result arrived after: 8 seconds
result arrived after: 9 seconds

get_list iteration (results come all at once)
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
result arrived after: 10 seconds
1赞 Conjure.Li 12/29/2021 #44

要了解它的屈服函数,必须了解什么是发电机。此外,在了解生成器之前,您必须了解可迭代对象。 Iterable:可迭代 要创建列表,您自然需要能够逐个读取每个元素。逐个读取其项目的过程称为迭代:

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

所有可用于...在。。。是可迭代的;列表、字符串、文件......

这些可迭代的方法很方便,因为您可以随意读取它们,但您将所有值存储在内存中,当您有很多值时,这并不总是可取的。 发电机:发电机 生成器也是一种迭代器,一种特殊的迭代,只能迭代一次。生成器不会将所有值都存储在内存中,而是动态生成值:

发电机:发电机、发电机、发电机发电但不储存能量;

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

只要使用 () 代替 [],列表推导式就变成了生成器推导式。但是,由于生成器只能使用一次,因此您不能在 mygenerator 中第二次执行 for i:生成器计算 0,然后丢弃它,然后计算 1,最后一次计算 4。典型的黑瞎子掰玉米。

yield 关键字的使用方式与 return 相同,只是该函数将返回生成器。

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() 
>>> print(mygenerator) 
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4 

这个例子本身是无用的,但是当你需要一个函数返回大量值并且只需要读取一次时,使用 yield 就变得很方便了。

要掌握良率,需要明确的一点是,当调用一个函数时,写在函数体中的代码是不会运行的。该函数仅返回生成器对象。初学者可能会对此感到困惑。

其次,要了解每次使用生成器时,代码都会从中断的地方继续。

现在最困难的部分是:

第一次调用从函数创建的生成器对象时,它将从头开始运行函数中的代码,直到达到 yield,然后返回循环的第一个值。然后,每个后续调用都将运行您在函数中编写的循环的下一次迭代,并返回下一个值。这将一直持续到生成器被视为空,当函数运行时没有命中时,就会产生。这可能是因为循环已经结束,或者因为你不再对“if/else”感到满意。

个人理解,希望对您有所帮助!

17赞 yash bhangare 12/30/2021 #45

Python 中的关键字用于在不干扰局部变量状态的情况下退出代码,当再次调用函数时,执行从我们离开代码的最后一点开始。yield

下面的示例演示了 :yield

def counter():
    x=2
    while x < 5:
        yield x
        x += 1
        
print("Initial value of x: ", counter()) 

for y in counter():
    print(y)

上面的代码生成 Below 输出:

Initial value of x:  <generator object counter at 0x7f0263020ac0>
2
3
4

评论

6赞 pippo1980 2/19/2022
x=2 x=x+1 在您的代码中是什么意思?
1赞 Mavaddat Javid 5/5/2022
@pippo1980它只是为了表明在外部作用域中不受更改的影响。xcounter()x
5赞 Vinod Srivastav 1/7/2022 #46

关键字有什么作用?Yield

yield关键字用于创建生成器函数。一种内存效率高的函数,可以像迭代器对象一样使用。

简单来说,yield 关键字会将它给出的任何表达式转换为生成器对象,并将其返回给调用者。因此,如果要获取存储在生成器对象中的值,则必须循环访问生成器对象。

仅靠定义是无法解释的,所以这里有一个非常简单 exampleA.pyyield

# example A
def getNumber():
    for r in range(1,10):
        return r

上述函数将仅返回 1,即使在一个循环中多次调用也是如此。现在,如果我们将关键字替换为 as in exampleB.pyreturnyield

# example B
def getNumber():
    for r in range(1,10):
        yield r

第一次调用时返回 1,再次调用时返回 2,然后返回 3,4然后递增直到 10

尽管示例 B 在概念上是正确的,但要在 python 3 中调用它,我们必须执行以下操作:

# example B
def getNumber():
    for r in range(1,10):
        yield r

g = getNumber() #instance
print(next(g)) #will print 1
print(next(g)) #will print 2
print(next(g)) #will print 3 and so on

3赞 Raymond Hettinger 5/22/2022 #47

要点

  • Python 的语法使用关键字的存在来生成一个返回生成器的函数。yield

  • 生成器是一种迭代器,它是 Python 中循环发生的主要方式。

  • 生成器本质上是一个可恢复的功能。与返回值并结束函数不同,关键字返回值并挂起函数。returnyield

  • 在生成器上调用函数时,该函数会从中断的位置继续执行。next(g)

  • 只有当函数遇到显式或隐含时,它才会真正结束。return

编写和理解生成器的技术

理解和思考生成器的一个简单方法是编写一个常规函数,而不是:print()yield

def f(n):
    for x in range(n):
        print(x)
        print(x * 10)

观察它输出的内容:

>>> f(3)
0
0
1
10
2
2

理解该函数后,将 for 替换为 得到一个产生相同值的生成器:yieldprint

def f(n):
    for x in range(n):
        yield x
        yield x * 10

这给出了:

>>> list(f(3))
[0, 0, 1, 10, 2, 20]

迭代器协议

“yield 的作用”的答案可能简短而简单,但它是一个更大的世界的一部分,即所谓的“迭代器协议”。

在迭代器协议的发送端,有两种相关的对象。可迭代对象是可以循环访问的内容。迭代器是跟踪循环状态的对象。

在迭代器协议的消费者端,我们在可迭代对象上调用 iter() 来获取迭代器。然后我们在迭代器上调用 next() 以从迭代器中检索值。当没有更多数据时,将引发 StopIteration 异常:

>>> s = [10, 20, 30]    # The list is the "iterable"
>>> it = iter(s)        # This is the "iterator"
>>> next(it)            # Gets values out of an iterator
10
>>> next(it)
20
>>> next(it)
30
>>> next(it)
Traceback (most recent call last):
 ...
StopIteration

为了让这一切变得更容易,for-loops 代表我们调用 iter 和 next:

>>> for x in s:
...     print(x)
...   
10
20
30

一个人可以写一本关于这一切的书,但这些是关键点。当我教授 Python 课程时,我发现这是一个最低限度的解释,足以立即构建、理解并开始使用它。特别是,编写函数的技巧,测试它,然后转换为 似乎适用于所有级别的 Python 程序员。printyield

3赞 My Car 10/2/2022 #48

什么是 Python 中的产量?

Python 中的 Yield 关键字类似于 Python 中用于返回值或对象的 return 语句。但是,略有不同。yield 语句将一个生成器对象返回给调用包含 yield 的函数的人,而不是简单地返回一个值。

在程序内部,当您调用具有 yield 语句的函数时,一旦遇到 yield,该函数的执行就会停止,并将生成器的对象返回给函数调用者。简单来说,yield 关键字会将与其一起指定的表达式转换为生成器对象,并将其返回给调用方。因此,如果要获取存储在生成器对象中的值,则需要对其进行迭代。

它不会破坏局部变量的状态。每当调用函数时,执行将从最后一个 yield 表达式开始。请注意,包含 yield 关键字的函数称为生成器函数。

使用具有返回值的函数时,每次调用该函数时,它都会以一组新的变量开头。相反,如果使用生成器函数而不是普通函数,则执行将从上次离开的位置开始。

如果要从函数返回多个值,则可以使用带有 yield 关键字的生成器函数。yield 表达式返回多个值。它们返回一个值,然后等待,保存本地状态,然后再次恢复。

来源:https://www.simplilearn.com/tutorials/python-tutorial/yield-in-python

5赞 Roland 10/13/2022 #49

yield允许您通过将循环部分分解为单独的方法来编写更智能的 -loops,以便于重用。for

假设您需要遍历电子表格的所有非空白行,并对每一行执行某些操作。

for i, row in df.iterrows(): #from the panda package for reading excel 
  if row = blank: # pseudo code, check if row is non-blank...
    continue
  if past_last_row: # pseudo code, check for end of input data
    break
  #### above is boring stuff, below is what we actually want to do with the data ###
  f(row)

如果你需要在类似的循环中调用,你可能会发现自己重复了语句和对有效行的检查,这很无聊、复杂且容易出错。我们不想重蹈覆辙(DRY原则)。g(row)for

您希望将用于检查每条记录的代码与实际处理行的代码分开,例如 和 。f(row)g(row)

您可以创建一个将 f() 作为输入参数的函数,但在执行所有关于检查有效行以准备调用 f() 的方法中使用它要简单得多:yield

def valid_rows():
  for i, row in df.iterrows(): # iterate over each row of spreadsheet
    if row == blank: # pseudo code, check if row is non-blank...
      continue
    if past_last_row: # pseudo code, check for end of input data
      break
    yield i, row

请注意,该方法的每次调用都将返回下一行,但如果读取所有行并完成,则该方法将正常返回。下一个调用将开始一个新的循环。forreturnfor

现在,您可以对数据进行迭代,而不必重复对有效行的无聊检查(现在已分解为自己的方法),例如:

for i, row in valid_rows():
  f(row)

for i, row in valid_rows():
  g(row)

nr_valid_rows = len(list(valid_rows()))

仅此而已。请注意,我没有使用迭代器、生成器、协议、协程等术语。我认为这个简单的例子适用于我们的很多日常编码。

3赞 Super Kai - Kazuya Ito 11/7/2022 #50

产量

  • 可以通过停止函数多次从函数返回值。
  • 可以像它一样使用它。fromyield from
  • 在返回大数据时使用,将大数据分割成小块数据,以防止RAM的大量使用。

例如,下面可以通过停止逐个返回,因此总共停止 3 次,总共返回 3 次:test()'One''Two'['Three', 'Four']test()test()test()

def test():
    yield 'One'                  # Stop, return 'One' and resume 
    yield 'Two'                  # Stop, return 'Two' and resume
    yield from ['Three', 'Four'] # Stop and return ['Three', 'Four'] 

并且,下面这 3 组代码可以调用和打印 、 和 :test()'One''Two''Three''Four'

for x in test():
    print(x)
x = test()
print(next(x))
print(next(x))
print(next(x))
print(next(x))
x = test()
print(x.__next__())
print(x.__next__())
print(x.__next__())
print(x.__next__())

结果如下:

$ python yield_test.py
One
Two
Three
Four

此外,当使用 with 时,无法从中获取值:returnyieldreturn

def test():
    yield 'One' 
    yield 'Two'
    yield from ['Three', 'Four']
    return 'Five' # 'Five' cannot be got

x = test()
print(next(x))
print(next(x))
print(next(x))
print(next(x))
print(next(x)) # Here

因此,尝试获取时出现以下错误:'Five'

$ python yield_test.py 
One
Two
Three
Four
Traceback (most recent call last):
  File "C:\Users\kai\yield_test.py", line 12, in <module>
    print(next(x))
          ^^^^^^^
StopIteration: Five

评论

0赞 juanpa.arrivillaga 1/27/2023
# 'Five' cannot be got好吧,它可以,如果你发现错误并查看StopIteration.args[0]