热门标签:
Q:

"Yield"关键字有什么作用?

Python中yield关键字有什么用? 它能做什么?

例如,我试图理解这段代码1:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

这是来电者:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

当方法_get_child_candidates被调用时会发生什么? 是否返回列表? 单个元素? 又叫了吗? 后续呼叫何时停止?


1. This piece of code was written by Jochen Schulz (jrschulz), who made a great Python library for metric spaces. This is the link to the complete source: Module mspace.
原网址
A:

要了解yield的作用,您必须了解生成器是什么。 在了解生成器之前,您必须了解iterables

迭代器

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

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist是一个可迭代的。 当您使用列表理解时,您创建了一个列表,因此一个可迭代的:

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

您可以使用"for... in..."的所有内容都是可迭代的;listsstrings,文件。..

这些iterables很方便,因为您可以随心所欲地读取它们,但是您将所有值存储在内存中,当您有很多值时,这并不总是您想要的。

发电机

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

>>> 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时。 这可能是因为循环已经结束,或者因为你不再满足an"if/else"


你的代码解释

发生器:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

调用者:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

此代码包含几个智能部分:

  • 循环在列表上迭代,但在循环迭代时列表会扩展。 这是一种简洁的方式来遍历所有这些嵌套数据,即使它有点危险,因为你最终可能会有一个无限循环。 在这种情况下,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))耗尽了生成器的所有值,但是while不断创建新的生成器对象,这些对象将产生与以前的值不同的值,因为它不在同一个节点上应用。

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

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

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

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

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

它的工作原理是因为Python不关心方法的参数是否是列表。 Python需要迭代器,因此它将与字符串、列表、元组和生成器一起工作! 这被称为duck typing,也是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
...

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

它对于控制对资源的访问等各种事情都很有用。

Itertools,你最好的朋友

itertools模块包含用于操作iterables的特殊函数。 曾经想复制一个发电机吗? 链两个发电机? 在一个单行嵌套列表中分组值? Map / Zip不创建另一个列表?

那么就import itertools

一个例子? 让我们来看看四匹马比赛的可能到达顺序:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

理解迭代的内在机制

迭代是一个隐含iterables(实现__iter__()方法)和iterators(实现__next__()方法)的过程。 Iterables是您可以从中获取迭代器的任何对象。 迭代器是允许您迭代可迭代对象的对象。

在这篇文章中有更多关于for循环如何工作的内容。

所有回答

共 29 条

author avatar

理解的捷径yield

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

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

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

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

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

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

Python执行以下两个步骤:

  1. 获取mylist的迭代器:

    Calliter(mylist)->这返回一个带有next()方法(或Python3中的__next__())的对象。

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

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

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

事实是Python在想要循环对象的内容时执行上述两个步骤-因此它可能是for循环,但也可能是像otherlist.extend(mylist)这样的代码(其中otherlist是Python列表)。

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

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

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

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

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

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

所以generator对象有点像一个适配器-在一端它展示了迭代器协议,通过公开__iter__()next()方法来保持for循环的快乐。 然而,在另一端,它运行函数足以从中获取下一个值,并将其放回挂起模式。

为什么要使用发电机?

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

author avatar

这样想吧:

迭代器只是一个具有next()方法的对象的花哨冠冕堂皇的术语。 所以一个yield-ed函数最终是这样的:

原始版本:

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

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

我应该注意到,为了说明目的,这个的过于简单化。 :)

author avatar

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

  1. 如果编译器在函数内的任意位置检测到yield关键字,则该函数不再通过return语句返回。 相反,它立即返回一个懒惰的"挂起列表"对象称为生成器
  2. 生成器是可迭代的。 什么是可迭代? 它类似于listsetrange或dict-view,具有内置协议,用于按特定顺序访问每个元素

简而言之:生成器是一个懒惰的,增量挂起的列表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-loop这样的语法构造,重复调用next()并捕获StopIteration异常等。). 您可能遇到了带有生成器表达式的生成器;生成器函数更强大,因为您可以将参数传回暂停的生成器函数,使用它们来实现协程。 稍后再谈。


基本例子('list')

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

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

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

为了强制生成器立即返回其挂起的值,您可以将其传递到list()(就像您可以任意迭代一样):

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

比较示例与"只返回列表"

上面的例子可以被认为只是创建一个你附加到并返回的列表:

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

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

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


如何使用生成器

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

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

为了更好地感受生成器,您可以使用itertools模块(确保在必要时使用chain.from_iterable而不是chain)。 例如,您甚至可以使用生成器来实现无限长的懒惰列表,如itertools.count()。 您可以实现自己的def enumerate(iterable): zip(count(), iterable),或者在while循环中使用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

内置函数next()只是调用objects.__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)

细枝末节

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

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

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

因此,在不太可能的情况下,你没有做这样的事情。..

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

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

author avatar

yield关键字在Python中做什么?

答案大纲/摘要

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

发电机:

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

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

yield提供了一个 实现迭代器协议的简单方法,由以下两种方法定义: __iter__next(Python2)或__next__(Python3)。 这两种方法 使一个对象成为一个迭代器,您可以使用Iterator抽象基进行类型检查 来自collections模块的类。

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

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

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

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

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

Iterator的一个特点是,一旦耗尽,就不能重用或重置它:

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

如果你想再次使用它的功能,你必须再做一个(见脚注2):

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

例如,可以以编程方式生成数据:

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

上面的简单生成器也等价于下面-截至Python3.3(并且在Python2中不可用),您可以使用yield from:

def func(an_iterable):
    yield from an_iterable

但是,yield from也允许委托给子生成器, 这将在下一节中解释与子协程的合作授权。

协程:

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

下面是一个例子,注意received变量,它将指向发送到生成器的数据:

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


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

首先,我们必须使用内置函数排队生成器,next。 它会的 调用相应的next__next__方法,具体取决于 您正在使用的Python:

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

现在我们可以将数据发送到生成器中。 (发送None是 与调用next相同。):

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

yield from

合作委托给子协程

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


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

您可以在PEP380中阅读更多关于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

结论

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

yield关键字在Python中做什么?

事实证明,yield做了很多。 我相信我还可以再加上更多 彻底的例子。 如果您想要更多或有一些建设性的批评,请通过评论让我知道 下面。


附录:

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

  • 它对什么使可迭代感到困惑,只是使用列表作为示例。 请参阅我上面的引用,但总结一下:一个iterable有一个__iter__方法返回一个迭代器。 一个迭代器提供了一个.next(Python2或.__next__(Python3)方法,它被for循环隐式调用,直到它引发StopIteration,一旦它这样做,它将继续这样做。
  • 然后,它使用生成器表达式来描述生成器是什么。 由于生成器只是创建迭代器的便捷方式,它只会混淆问题,我们还没有进入yield部分。
  • 控制生成器耗尽中,他调用.next方法,而应该使用内建函数next。 这将是一个适当的间接层,因为他的代码在Python3中不起作用。
  • Itertools? 这与yield的作用完全无关。
  • 没有讨论yield提供的方法以及Python3中的新功能yield from顶部/接受的答案是一个非常不完整的答案。

在生成器表达或理解中暗示yield的答案的批判。

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

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

由于yield是一个表达式,因此有人吹捧将其用于理解或生成器表达式是有趣的-尽管没有引用特别好的用例。

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

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

2017年1月29日16:39克雷格*罗德里格斯写道:

无论哪种方法我都可以。 在Python3中留下它们的方式 不好,恕我直言。

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

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

就到达那里而言,我们可能需要:

  • 3.7中的SyntaxWarning或DeprecationWarning
  • Py3k警告在2.7。x
  • 3.8中的SyntaxError

干杯,尼克。

--尼克Coghlan|ncoghlan在gmail.com /澳洲布里斯班

此外,还有一个悬而未决的问题(10544)似乎指向这个never是一个好主意(Pypy,一个用Python编写的Python实现,已经在引发语法警告。)

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

生成器中的return语句

Python2

在生成器函数中,return语句不允许包含expression_list。 在这种情况下,裸return表示生成器完成并将导致StopIteration被提升。

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

Python3

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

脚注

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

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

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

author avatar

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

在你的代码的情况下,函数get_child_candidates就像一个迭代器,所以当你扩展你的列表时,它一次向新列表添加一个元素。

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

author avatar

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

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

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

author avatar

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

TL;DR

而不是这个:

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

这样做:

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

每当你发现自己从头开始构建一个列表时,yield每个部分代替。

这是我第一次屈服的"啊哈"时刻。


yield是一种含糖的说法

建立一系列的东西

同样的行为:

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

不同的行为:

Yield是single-pass:只能遍历一次。 当一个函数中有一个yield时,我们称之为生成器函数。 而一个迭代器就是它返回的。 这些术语很能说明问题。 我们失去了容器的便利性,但获得了根据需要计算的系列的功率,并且任意长。

Yield是懒惰的,它推迟了计算。 当你调用它的时候,一个带有yield的函数实际上根本不执行它。它返回一个迭代器对象,它记住它离开的地方。 每次在迭代器上调用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因为这两个含义适用:

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

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

屈服-让位或放弃(如在政治权力)

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

author avatar

产量给你一个发电机。

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通过调用堆栈向上推送东西,而不是使用某种堆栈变量。 生成器也可以用于专门的树遍历,以及所有其他事情。

author avatar

它正在返回一个发电机。 我对Python不是特别熟悉,但我相信它与C#的迭代器块是一样的东西,如果你熟悉这些。

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

author avatar

有一种类型的答案,我觉得还没有给出,在描述如何使用发电机的许多伟大的答案中。 以下是编程语言理论答案:

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中的生成器。 生成器是延续的特定子类型。 而continuations通常能够保存计算的状态(即程序的调用堆栈),生成器只能保存迭代器的迭代状态。 虽然,这个定义对于生成器的某些用例来说有点误导。 例如:

def f():
  while True:
    yield 4

这显然是一个合理的可迭代器,其行为定义良好-每次生成器迭代它时,它都会返回4(并且永远这样做)。 但在考虑迭代器(即for x in collection: do_something(x))时,它可能不是iterable的原型类型。 这个例子说明了生成器的力量:如果任何东西都是迭代器,则生成器可以保存其迭代的状态。

重申一下:Continuations可以保存程序堆栈的状态,generator可以保存迭代的状态。 这意味着延续比生成器更强大,但生成器也更容易。 它们对于语言设计人员来说更容易实现,并且对于程序员来说更容易使用(如果您有一些时间可以燃烧,请尝试阅读和理解关于continuations的本页并调用/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关键字。

author avatar

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

我想对一个数字序列进行操作,但我不想用该序列的创建来打扰我自己,我只想专注于我想做的操作。 所以,我做以下事情:

  • 我打电话给你,告诉你,我想要一个以特定方式计算的数字序列,我让你知道算法是什么。
    该步骤对应于生成器函数中的yield,即包含ayield的函数。
  • 稍后的某个时候,我告诉你,"好吧,准备好告诉我数字序列"。
    这一步对应调用generator函数,该函数返回一个generator对象。请注意,你还没有告诉我任何数字;你只需抓住你的纸和铅笔。
  • 我问你,"告诉我下一个数字",你告诉我第一个数字;之后,你等着我问你下一个数字。 你的工作是记住你在哪里,你已经说过什么数字,下一个数字是什么。 我不在乎细节。
    此步骤对应于在生成器对象上调用next(generator)
    (在Python2中,.next是生成器对象的一个方法;在Python3中,它被命名为.__next__,但正确的调用方法是使用内建的next()函数,就像len().__len__一样)
  • ...重复上一步,直到…
  • 最终,你可能会走到尽头。 你不告诉我一个数字;你只是喊着,"保持你的马! 我玩完了! 没有更多的数字!"
    此步骤对应于生成器对象结束其作业,并引发StopIteration异常。
    生成器函数不需要引发异常。 当函数结束或发出areturn时,它会自动引发。

这就是生成器所做的事情(一个包含ayield的函数);它开始在第一个next()上执行,每当它执行ayield时暂停,当被要求提供next()值时,它从最后一个点继续执行。 它在设计上完全符合Python的迭代器协议,它描述了如何顺序请求值。

迭代器协议最着名的用户是Python中的for命令。 所以,每当你做一个:

for item in sequence:

如果sequence是一个列表,一个字符串,一个字典或一个生成器对象,就像上面描述的那样无关紧要;结果是一样的:你一个接一个地从序列中读取项目。

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

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

author avatar

虽然很多答案都显示了为什么要使用ayield来创建生成器,但yield有更多用途。 做一个协程很容易,它可以在两个代码块之间传递信息。 我不会重复已经给出的关于使用yield创建生成器的任何精细示例。

为了帮助理解ayield在以下代码中的作用,您可以使用手指在任何具有ayield的代码中跟踪循环。 每次手指碰到yield,都要等待anext或asend输入。 当一个next被调用时,你会跟踪代码,直到你击中yield...yield右边的代码被评估并返回给调用者...然后你等待。 当再次调用next时,您通过代码执行另一个循环。 但是,您会注意到,在协程中,yield也可以与send...一起使用,它将从调用者发送一个值到yielding函数。 如果给出了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()
author avatar

还有另一个yield用法和含义(自Python3.3以来):

yield from <expr>

FromPEP380--Delegating to A Subgenerator的语法:

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

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

而且将引入(自Python3.5以来):

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

为了避免协程与常规生成器混淆(todayyield在两者中都使用)。

author avatar

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

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

作为类比,returnreturn是双胞胎。 return表示'返回并停止',而'yield'表示'返回,但继续'

  1. 尝试获取带有return的num_list。
def num_list(n):
    for i in range(n):
        return i

运行它:

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

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

  1. 来了return

return替换为return

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相比,return运行您计划的时间。 您可以将return解释为return one of them,将return解释为return all of them。 这就是所谓的iterable

  1. 再多一步我们可以用return重写return语句
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]

它是关于return的核心。

listreturn输出和objectreturn输出的区别是:

您将始终从list对象中获取[0,1,2],但只能从'the objectreturnoutput'中检索一次。 因此,它有一个新的名称generator对象,如Out[11]: <generator object num_list at 0x10327c990>所示。

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

  • returnreturn是双胞胎
  • listgenerator是双胞胎
author avatar

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

实现并发执行的迭代器、生成器和线程池等。 作为thunks,人们使用发送到一个closure对象的消息,该对象有一个dispatcher,并且dispatcher回答"消息"

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

有很多方法可以实现这种计算。 我使用了mutation,但是可以在没有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
->
author avatar

以下是一些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)
author avatar

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

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

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

author avatar

下面是一个简单的例子:

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

author avatar

这是一个关于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(..)探索它们,我们可以证实我们上面的心理模型是可信的。

author avatar

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

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

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

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

for line in getNextLines():
    doSomeThing(line)

执行控制转移gotcha

执行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"
author avatar

想象一下,你已经创造了一个非凡的机器,能够每天产生数千个灯泡。 机器在具有唯一序列号的盒子中生成这些灯泡。 您没有足够的空间在同一时间存储所有这些灯泡,所以您想调整它以按需生成灯泡。

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,因为它不会在内存中加载序列的所有项,直到它们被需要。 在前面的示例中使用next是从迭代器中获取下一个项的explicit方式。 隐式方式用于循环:

for barcode in barcode_generator():
    print(barcode)

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

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

真实世界的应用?

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

author avatar

总之,yield语句将您的函数转换为工厂,该工厂生成一个名为agenerator的特殊对象,该对象包装原始函数的主体。 当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语句和yield'n',在下一次调用时,它将执行增量语句,跳回到'while',评估它,如果为true,它将停止并再次产生'n',它将继续

author avatar

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

当在python函数中使用yield而不是return时,该函数被转换为特殊的东西,称为generator function。 该函数将返回generator类型的对象。 yield关键字是通知python编译器专门处理此类函数的标志。正常函数将终止,一旦从它返回一些值。 但是在编译器的帮助下,生成器函数可以被认为是可恢复的。 也就是说,将恢复执行上下文,并从上次运行开始继续执行。 直到你显式调用return,这将引发StopIteration异常(这也是迭代器协议的一部分),或者到达函数的末尾。 我发现了很多关于generator的引用,但是这一个functional programming perspective是最可消化的。

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

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

有两种方法来包装这样的元数据。

  1. OO方法,我们将元数据as a class。 这就是所谓的实现迭代器协议的iterator(即__next__()__iter__()方法)。 这也是常见的迭代器设计模式
  2. 功能方法,我们包装元数据as a function。 这是 所谓generator function。 但在引擎盖下,返回的generator object仍然IS-A迭代器,因为它也实现了迭代器协议。

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

author avatar

产量是一个对象

函数中的

Areturn将返回单个值。

如果您希望一个函数返回一组巨大的值,请使用yield

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

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

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

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

author avatar

许多人使用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使用三行而不是五行,并且少了一个需要担心的变量。

这是代码的结果:

<img alt="输出"src="https://i.stack.imgur.com/iUFNJ.png缧/>

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

一个真实的例子是逐行读取文件,或者如果你只是想制作一个生成器。

author avatar

yield关键字只是收集返回结果。 想想yieldreturn +=

author avatar

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

相似问题