Python 中 “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._leftchildif self._rightchild and distance + max_dist >= self._median:yield self._rightchild

这是调用者:

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

调用方法_get_child_candidates时会发生什么?是否返回列表?单个元素?是否再次调用?后续调用何时停止?


1.这段代码由Jochen Schulz(jrschulz)编写,他为度量空间制作了一个很棒的Python库。这是完整源代码的链接:mspace 模块
3104198 次浏览

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

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

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

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

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

这样想:

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

原文版本:

def some_function():for i in xrange(4):yield i
for i in some_function():print i

这基本上就是Python解释器对上面代码所做的:

class it:def __init__(self):# Start at -1 so that we get 0 when we add 1 below.self.count = -1
# The __iter__ method will be called once by the 'for' loop.# The rest of the magic happens on the object returned by this method.# In this case it is the object itself.def __iter__(self):return self
# The next method will be called repeatedly by the 'for' loop# until it raises StopIteration.def next(self):self.count += 1if self.count < 4:return self.countelse:# A StopIteration exception is raised# to signal that the iterator is done.# This is caught implicitly by the 'for' loop.raise StopIteration
def some_func():return it()
for i in some_func():print i

为了更深入地了解幕后发生的事情,for循环可以重写为:

iterator = some_func()try:while 1:print iterator.next()except StopIteration:pass

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

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

要理解yield的作用,你必须理解generators是什么。在理解生成器之前,你必须理解iterables 可迭代对象

Iterables

当你创建一个列表时,你可以一个接一个地读取它的项目。一个接一个地读取它的项目称为迭代:

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

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

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

你可以使用“for... in...”的一切都是可迭代的;listsstrings,文件…

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

发电机

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

>>> mygenerator = (x*x for x in range(3))>>> for i in mygenerator:...    print(i)014

这是一样的,除了你使用()而不是[]。但是,你不能第二次执行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)014

这是一个无用的例子,但是当你知道你的函数将返回大量的值时,它很方便,你只需要读取一次。

要掌握yield,你必须明白当您调用函数时,您在函数体中编写的代码不会运行。函数只返回生成器对象,这有点棘手。

然后,您的代码将从每次for使用生成器时停止的地方继续。

现在最难的部分:

第一次for调用从你的函数创建的生成器对象时,它将从一开始就运行你函数中的代码,直到它击中yield,然后它将返回循环的第一个值。然后,每个后续调用将运行你在函数中编写的循环的另一次迭代并返回下一个值。这将一直持续到生成器被认为是空的,当函数运行而没有击中yield时就会发生这种情况。这可能是因为循环已经结束,或者因为你不再满足"if/else"


你的代码解释了

发电机:

# Here you create the method of the node object that will return the generatordef _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 childif 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 childif 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 referenceresult, 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 listnode = candidates.pop()# Get the distance between obj and the candidatedistance = node._get_dist(obj)# If the distance is ok, then you can fill in the resultif 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 candidatecandidates.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))

它可以用于各种事情,例如控制对资源的访问。

易特工具,你最好的朋友

迭代工具模块包含操作迭代的特殊函数。曾经想复制生成器吗?链接两个生成器?使用单行代码将嵌套列表中的值分组?Map / Zip而不创建另一个列表?

然后只是import itertools

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

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

理解迭代的内在机制

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

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

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

我想对一系列数字进行操作,但我不想为创建该序列而烦恼,我只想专注于我想做的操作。所以,我做了以下事情:

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

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

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

for item in sequence:

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

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

有关更准确的信息,请阅读Python留档中的迭代器类型yield 语法generator

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

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

然后我可以在其他代码中使用它,如下所示:

for f in fib():if some_condition: breakcoolfuncs(f);

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

快速理解yield

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

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

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

不要混淆你的Iterables、Iterators和Generators

第一,迭代器协议-当你写的时候

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

Python执行以下两个步骤:

  1. 获取mylist的迭代器:

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

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

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

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

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

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

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

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

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

def f123():yield 1yield 2yield 3
for item in f123():print item

而不是yield语句,如果你在f123()中有三个return语句,只有第一个会被执行,函数就会退出。但f123()不是普通的函数。当f123()被调用时,它不要返回的任何值都产生声明!它返回一个生成器对象。此外,函数并没有真正退出——它进入了挂起状态。当for循环尝试循环生成器对象时,函数在它之前返回的yield之后的下一行从挂起状态恢复,执行下一行代码,在这种情况下,是一个yield语句,并将其作为下一项返回。这种情况一直发生,直到函数退出,此时生成器引发StopIteration,循环退出。

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

为什么要使用发电机?

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

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

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

简而言之:最常见的是,生成器是一个懒惰的、增量挂起的列表#0语句允许您使用函数表示法对列表值进行编程生成器应该逐渐吐出。

generator = myYieldingFunction(...)  # basically a list (but lazy)x = list(generator)  # evaluate every element into a list
generatorv[x[0], ..., ???]
generatorv[x[0], x[1], ..., ???]
generatorv[x[0], x[1], x[2], ..., ???]
StopIteration exception[x[0], x[1], x[2]]     done

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


基本示例('list')

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

def makeRange(n):# return 0,1,2,...,n-1i = 0while i < n:yield ii += 1
>>> makeRange(5)<generator object makeRange at 0x19e4aa0>

要强制生成器立即返回其挂起值,您可以将其传递到list()(就像您可以使用任何可迭代对象一样):

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

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

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

# return a list                  #  # return a generatordef makeRange(n):                #  def makeRange(n):"""return [0,1,2,...,n-1]""" #      """return 0,1,2,...,n-1"""TO_RETURN = []               #i = 0                        #      i = 0while i < n:                 #      while i < n:TO_RETURN += [i]         #          yield ii += 1                   #          i += 1return TO_RETURN             #
>>> makeRange(5)[0, 1, 2, 3, 4]

不过,有一个主要的区别;见最后一节。


如何使用生成器

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

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

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

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


在幕后

这就是“Python迭代协议”的工作原理。也就是说,当你做list(makeRange(5))时会发生什么。这就是我之前描述的“懒惰、增量列表”。

>>> x=iter(range(5))>>> next(x)  # calls x.__next__(); x.next() is deprecated0>>> next(x)1>>> next(x)2>>> next(x)3>>> next(x)4>>> next(x)Traceback (most recent call last):File "<stdin>", line 1, in <module>StopIteration

内置函数next()只是调用对象.__next__()函数,它是“迭代协议”的一部分,在所有迭代器上都可以找到。你可以手动使用next()函数(和迭代协议的其他部分)来实现花哨的东西,通常以牺牲易读性为代价,所以尽量避免这样做…


协程

协程示例:

def interactiveProcedure():userResponse = yield makeQuestionWebpage()print('user response:', userResponse)yield 'success'
coroutine = interactiveProcedure()webFormData = next(coroutine)  # same as .send(None)userResponse = serveWebForm(webFormData)
# ...at some point later on web form submit...
successStatus = coroutine.send(userResponse)

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

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


细节

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

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

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

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

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

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

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

作为Python生成器:

from itertools import islice
def fib_gen():a, b = 1, 1while True:yield aa, 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 nonlocaldef _():_.a, _.b = _.b, _.a + _.breturn _.a_.a, _.b = 0, 1return _
assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

使用对象闭包而不是生成器(因为闭包和对象是等价的

class fib_gen3:def __init__(self):self.a, self.b = 1, 1
def __call__(self):r = self.aself.a, self.b = self.b, self.a + self.breturn r
assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

屈服给你一个发电机。

def get_odd_numbers(i):return range(1, i, 2)def yield_odd_numbers(i):for x in range(1, i, 2):yield xfoo = 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()1bar.next()3bar.next()5

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

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

再一次,这是一个非常做作的例子,如果你真的想数到500亿,你可能会使用迭代工具。:)

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

对于那些喜欢最小工作示例的人,请冥想这个交互式Python会话:

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

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

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

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

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

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

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

延续,在这种更一般的形式中,可以通过两种方式实现。在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))。这个例子说明了生成器的力量:如果有任何东西是迭代器,生成器可以保存其迭代的状态。

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

但是你可以很容易地实现(和概念化)生成器作为一个简单的,特定的延续传递样式的情况:

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

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

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

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

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

这是yield所做的心理形象。

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

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

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

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

当随后调用next()时,它会将函数的属性检索到堆栈上并重新动画它。该函数继续从它停止的地方计算,忘记了它刚刚在冷存储中度过了永恒的事实。

比较以下示例:

def normalFunction():returnif False:pass
def yielderFunction():returnif 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(..)探索它们,我们可以确认上面的心理模型是可信的。

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

要实现用于并发执行的迭代器、生成器和线程池等作为thunks,可以使用发送到闭包对象的消息,它有一个调度程序,以及调度程序回答“消息”

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

有很多方法来实现这种计算。我使用了突变,但也可以在没有突变的情况下进行这种计算,方法是返回当前值和下一个yielder(使其为参照透明)。球拍在一些中间语言中使用了初始程序的一系列转换,其中一次重写使得产量运算符在一些具有更简单运算符的语言中被转换。

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

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

这里有一个简单的例子:

def isPrimeNumber(n):print "isPrimeNumber({}) call".format(n)if n==1:return Falsefor x in range(2,n):if n % x == 0:return Falsereturn True
def primes (n=1):while(True):print "loop step ---------------- {}".format(n)if isPrimeNumber(n): yield nn += 1
for n in primes():if n> 10:breakprint "wiriting result {}".format(n)

输出:

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

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

这似乎是一个有趣而美好的能力:D

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

为了帮助理解yield在以下代码中的作用,您可以使用手指跟踪任何具有yield的代码的循环。每次手指点击yield时,您都必须等待输入nextsend。当调用next时,您在代码中跟踪直到您点击yieldyield右侧的代码被评估并返回给调用者…然后您等待。再次调用next时,您在代码中执行另一个循环。但是,您会注意到,在协程中,yield也可以与send…一起使用,它将从调用者yield5向产生函数发送一个值。如果给出了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 None1>>> sequence.send('hello')Received hello2>>> sequence.close()

还有另一个yield的用法和含义(自Python 3.3以来):

yield from <expr>

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

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

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

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

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

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

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

yield关键字在Python中做什么?

答案大纲/总结

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

发电机:

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

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

yield提供了一个实现迭代器协议的简单方法,由以下两个方法定义:__iter____next__。这两种方法使一个对象成为一个迭代器,您可以使用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 iterableTrue>>> hasattr(gen, '__next__')   # and with .__next__True                           # implements the iterator protocol.

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

from types import GeneratorTypefrom 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开始,您可以使用#0

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 * depositedreceived = yield calculated_interestif received:deposited += received

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

首先,我们必须将内置函数#0的生成器排队。它将调用适当的next__next__方法,具体取决于您正在使用的Python:

>>> first_year_interest = next(my_account)>>> first_year_interest50.0

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

>>> next_year_interest = my_account.send(first_year_interest + 1000)>>> next_year_interest102.5

yield from合作委托给子协程

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

def money_manager(expected_rate):# must receive deposited value from .send():under_management = yield                   # yield None to start.while True:try:additional_investment = yield expected_rate * under_managementif additional_investment:under_management += additional_investmentexcept GeneratorExit:'''TODO: write function to send unclaimed funds to state'''raisefinally:'''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 managerexcept GeneratorExit:return manager.close()  # delegate?

现在我们可以将功能委托给子生成器,它可以用于通过上面的生成器:

my_manager = money_manager(.06)my_account = investment_account(1000, my_manager)first_year_return = next(my_account) # -> 60.0

现在模拟向帐户再添加1,000加上帐户的回报(60.0):

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

您可以在PEP 380。中阅读更多关于yield from的精确语义学

其他方法:关闭和抛出

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

my_account.close()

您还可以抛出一个可以在生成器中处理的异常或传播回用户:

import systry:raise ValueErrorexcept:my_manager.throw(*sys.exc_info())

加注:

Traceback (most recent call last):File "<stdin>", line 4, in <module>File "<stdin>", line 6, in money_managerFile "<stdin>", line 2, in <module>ValueError

结论

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

yield关键字在Python中做什么?

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


附录:

顶部的评论/接受的答案**

  • 它混淆了是什么使迭代,只是使用列表作为示例。参见上面我的参考,但总结:迭代有一个__iter__方法返回迭代器迭代器还提供了一个.__next__方法,该方法由for循环隐式调用,直到它引发StopIteration,一旦它确实引发StopIteration,它将继续这样做。
  • 然后它使用生成器表达式来描述生成器是什么。由于生成器表达式只是创建迭代器的一种方便方法,它只会混淆问题,我们还没有到达yield部分。
  • 控制发电机耗尽中,他调用.next方法(仅适用于Python 2),而他应该使用内置函数next。调用next(obj)将是一个合适的间接层,因为他的代码在Python 3中不起作用。
  • 这与yield所做的完全无关。
  • 没有讨论yield提供的方法以及Python 3中的新功能yield from

最高/接受的答案是一个非常不完整的答案。

在生成器表达式或理解中提示yield的答案批评。

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

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

由于产量是一个表达式,它被一些人吹捧为在理解或生成器表达式中使用它很有趣-尽管没有引用特别好的用例。

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

2017年1月30日19:05,Brett Cannon写道:

On Sun,29 Jan 2017 at 16:39克雷格·罗德里格斯写道:

两种方法我都可以。让事情保持Python 3中的样子#36825;不太好,我的意思是

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

我同意这是我们结束的一个明智的地方,就像任何代码一样依靠当前的行为实在是太聪明了可维护

在到达那里方面,我们可能想要:

  • 在3.7中的语法警告或弃用警告
  • Py3k警告
  • 3.8中的语法错误

干杯,尼克。

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

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

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

生成器中的return语句

python3

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

历史笔记,在python2中:"在生成器函数中,return语句不允许包含expression_list。在这种情况下,裸return表示生成器完成并将导致StopIteration被提升。"expression_list基本上是由逗号分隔的任意数量的表达式-本质上,在Python 2中,您可以使用return停止生成器,但不能返回值。

脚注

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

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

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

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

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

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

for line in getNextLines():doSomeThing(line)

执行控制转移问题

执行时,执行控制将从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"

产出是一个对象

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

如果您想要一个返回大量值的函数,请使用yield

更重要的是,yield屏障

就像CUDA语言中的障碍一样,它不会转移控制,直到它得到已完成

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

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

yield关键字只是收集返回的结果。把yield想象成return +=

这里有一个简单的基于yield的方法来计算斐波纳契级数,解释如下:

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

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

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

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

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

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

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

间接地,如果你向for循环、list初始化器、tuple初始化器或任何其他期望生成/产生值的对象提供fib,你将“消耗”生成器,直到它不能产生更多值(并返回):

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

类似地,对于tuple初始化器:

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

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

当你第一次调用fib时:

f = fib()

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

当你然后请求它直接或间接地生成第一个值时,它会执行它找到的所有语句,直到它遇到yield,然后它会返回你提供给yield的值并暂停。为了更好地演示这一点,让我们使用一些print调用(如果在Python 2上替换为print "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 valueprint("Let's go through it again.")

现在,输入REPL:

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

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

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

未引用的结果是打印的结果。引用的结果是从yield返回的。现在再次调用next

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

生成器记住它在yield value处暂停并从那里恢复。打印下一条消息并再次执行搜索yield语句以暂停它(由于while循环)。

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

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

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

据我理解,当我们要处理一堆数据时,我们通常会先将数据存储在某个地方,然后再逐个进行处理。但是这种方法有问题。如果数据量很大,事先将它们作为一个整体存储是很昂贵的。

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

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

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

太长别读

而不是这个:

def square_list(n):the_list = []                         # Replacefor x in range(n):y = x * xthe_list.append(y)                # thesereturn the_list                       # lines

这样做:

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

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

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


yield含糖的表达方式

建立一系列的东西

同样的行为:

>>> for square in square_list(4):...     print(square)...0149>>> for square in square_yield(4):...     print(square)...0149

不同的行为:

产率是单程:你只能迭代一次。当一个函数有产率时,我们称之为发生器函数。而迭代器是它返回的。这些术语很有启发性。我们失去了容器的便利性,但获得了根据需要计算的序列的能力,并且任意长。

产率是懒惰,它推迟了计算。一个包含产率的函数当你调用它时,它实际上根本不执行。它返回一个迭代器对象,它记住它离开的地方。每次你在迭代器上调用next()(这发生在for循环中)时,执行都会向前移动几英寸到下一个产率。return引发StopIteration并结束系列(这是for循环的自然结束)。

产量是通用。数据不必全部存储在一起,它可以一次一个地可用。它可以是无限的。

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

如果你需要多次通过并且系列不是太长,只需调用list()

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

yield这个词的选择很棒,因为两种含义适用于:

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

…提供序列中的下一个数据。

产量-让路或放弃(如在政治权力中)

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

太长别读

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

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

你可以看到产量/生成器是一种从外部手动运行控制流的方法(如继续循环一步),通过调用next,无论流程多么复杂。

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

许多人使用return而不是yield,但在某些情况下,yield可以更有效,更容易使用。

这里有一个例子,yield绝对是最好的:

返回(在函数中)

import random
def return_dates():dates = [] # With 'return' you need to create a list then return itfor 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使用了三行而不是五行,并且少了一个需要担心的变量。

这是代码的结果:

输出

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

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

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

def simple_generator():yield 'one'yield 'two'yield 'three'
for i in simple_generator():print i

简单地输出

onetwothree

功率来自于使用生成器和一个计算序列的循环,生成器每次执行循环停止以“产生”计算的下一个结果,通过这种方式它动态计算列表,好处是为特别大的计算节省内存

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

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

并像这样使用它;

for i in myRangeNaive(10):print i

但这是低效的,因为

  • 创建一个只使用一次的数组(这会浪费内存)
  • 这段代码实际上在该数组上循环了两次!:(

幸运的是,吉多和他的团队慷慨地开发了发电机,这样我们就可以做到这一点;

def myRangeSmart(i):n = 0while n < i:yield nn = n + 1return
for i in myRangeSmart(10):print i

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

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

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

for i in f123():print (i)

输出是:

1 2 1 2 1 2 1 2

产量类似于返回。区别是:

产量使函数可迭代(在下面的示例中,primes(n = 1)函数变得可迭代)。
它本质上意味着下次调用函数时,它将从它离开的地方(在yield expression行之后)继续。

def isprime(n):if n == 1:return Falsefor x in range(2, n):if n % x == 0:return Falseelse:return True
def primes(n = 1):while(True):if isprime(n): yield nn += 1
for n in primes():if n > 100: breakprint(n)

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

n += 1

这里所有的答案都很好;但只有一个(投票最多的一个)与您的代码如何工作有关。其他人一般都与发电机有关,以及它们是如何工作的。

所以我不会重复生成器是什么或收益率做什么;我认为这些已经被很好的现有答案所涵盖。然而,在花了几个小时试图理解与你的代码相似的代码后,我将分解它是如何工作的。

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

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

while candidates语句可以替换为for element in tree;Python将其转换为

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

因为Node.__iter__函数是一个生成器,所以代码里面每次迭代都会执行。所以执行看起来像这样:

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

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

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

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

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

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

作为类比,returnyield是双胞胎。return意味着“返回并停止”,而“产量”意味着“返回,但继续”

  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. 来了yield

return替换为yield

In [10]: def num_list(n):...:     for i in range(n):...:         yield i...:
In [11]: num_list(3)Out[11]: <generator object num_list at 0x10327c990>
In [12]: list(num_list(3))Out[12]: [0, 1, 2]

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

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

  1. 还有一步我们可以用return重写yield语句
In [15]: def num_list(n):...:     result = []...:     for i in range(n):...:         result.append(i)...:     return result
In [16]: num_list(3)Out[16]: [0, 1, 2]

这是关于yield的核心。

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

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

最后,作为一个隐喻来理解它:

  • returnyield是双胞胎
  • listgenerator是双胞胎

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

另一个有趣的事情#0关键字是保存生成器函数的#1

因此,每次generator产生时,我们可以将number设置为不同的值。

这里有一个实例:

def getPrimes(number):while True:if isPrime(number):number = yield number     # a miracle occurs herenumber += 1
def printSuccessivePrimes(iterations, base=10):primeGenerator = getPrimes(base)primeGenerator.send(None)for power in range(iterations):print(primeGenerator.send(base ** power))

yield产生一些东西。就像有人让你做5个纸杯蛋糕。如果你至少做了一个纸杯蛋糕,你可以在做其他蛋糕的时候给他们吃。

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

这里factory被称为生成器,它让你成为蛋糕。如果你调用make_function,你会得到一个生成器而不是运行那个函数。这是因为当yield关键字出现在函数中时,它就变成了一个生成器。

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 0Cake 1Cake 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))...:FXpUBhhHDdUDHoHndvtebEqG
In [27]: next(rpg)Out[27]: 'mJbYRMNo'

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

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

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

机器代码:

def barcode_generator():serial_number = 10000  # Initial barcodewhile True:yield serial_numberserial_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()来获取下一个序列号。

懒惰迭代器

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

for barcode in barcode_generator():print(barcode)

这将无限打印条形码,但您不会运行内存溢出。

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

实际应用?

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

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

def fun():yield 1yield 2yield 3

来电者是:-

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

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

First value printing1Second value printing2Third value printing3

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

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

事实上,正如这里的许多答案所解释的那样,使用yield会创建generator

您可以使用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 languageUnknown word...

总结如下:

在生成器中使用send方法将数据发送回生成器。为此,使用(yield)

函数-返回。

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

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

# Using functiondef 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 generatordef 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 firstyield 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

备注:收益率不应该在try…最终构造中。

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

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

两者的输出将是相同的。

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

答案很简单

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

基准

创建一个列表并返回它:

def my_range(n):my_list = []i = 0while i < n:my_list.append(i)i += 1return my_list
@profiledef function():my_sum = 0my_values = my_range(1000000)for my_value in my_values:my_sum += my_value
function()

结果:

Total time: 1.07901 sTimer unit: 1e-06 s
Line #      Hits         Time  Per Hit   % Time  Line Contents==============================================================9                                           @profile10                                           def function():11         1          1.1      1.1      0.0      my_sum = 012         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   @profile10                                         def function():11   40.168 MiB    0.000 MiB           1       my_sum = 012   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 = 0while i < n:yield ii += 1
@profiledef function():my_sum = 0    
for my_value in my_range(1000000):my_sum += my_value
function()

结果:

Total time: 1.24841 sTimer unit: 1e-06 s
Line #      Hits         Time  Per Hit   % Time  Line Contents==============================================================7                                           @profile8                                           def function():9         1          1.1      1.1      0.0      my_sum = 01011   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   @profile8                                         def function():9   40.168 MiB    0.000 MiB           1       my_sum = 01011   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

总结

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

一个简单的用例:

>>> def foo():yield 100yield 20yield 3
    
>>> for i in foo(): print(i)
100203>>>

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

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

在ES6及更高版本等某些语言中,它的实现方式略有不同,因此next是生成器对象的成员函数,每次调用者获得下一个值时,你都可以传递值。因此,如果结果是生成器,那么你可以做类似的事情y=result.next(555),并且产生值的程序可以说类似于z=产量999。y的值将是下一个从产量中获得的999,z的值将是下一个从产量中获得的555。Python get和发送方法具有类似的效果。

生成器允许立即获取单个处理的项目(而无需等待整个集合被处理)。这在下面的示例中说明。

import time
def get_gen():for i in range(10):yield itime.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 secondsresult arrived after: 1 secondsresult arrived after: 2 secondsresult arrived after: 3 secondsresult arrived after: 4 secondsresult arrived after: 5 secondsresult arrived after: 6 secondsresult arrived after: 7 secondsresult arrived after: 8 secondsresult arrived after: 9 seconds
get_list iteration (results come all at once)result arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 secondsresult arrived after: 10 seconds

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

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

mylist是一个可迭代的。当你使用列表推导时,你创建了一个列表,因此是可迭代的:

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

所有可用于… in…的数据结构都是可迭代的;列表、字符串、文件…

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

发电机:发电机,发电机,发电机发电但不储存能量;)

>>> mygenerator = (x*x for x in range(3))>>> for i in mygenerator:...    print(i)014

只要使用()而不是[],列表理解就成为生成器理解。然而,由于生成器只能使用一次,你不能第二次在my生成器中执行i:生成器计算0,然后丢弃它,然后计算1,最后一次计算4。典型的黑盲人断玉米。

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

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

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

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

其次,要明白每次使用生成器时,代码都会从它停止的地方继续。

现在最困难的部分是:

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

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

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

下面的示例演示了yield的工作原理:

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

上面的代码生成以下输出:

Initial value of x:  <generator object counter at 0x7f0263020ac0>234

yield关键字用于枚举/迭代,其中函数预计将返回一个以上的输出。我想引用这个非常简单的例a

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

上面的函数即使被多次调用也只会返回1。现在,如果我们像例b一样将return替换为yield

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

当第一次调用2时,它将返回1,然后再次调用34,它会递增到10。

虽然例b在概念上是正确的,但要在python3中调用它,我们必须执行以下操作:

g = getNumber() #instanceprint(next(g)) #will print 1print(next(g)) #will print 2print(next(g)) #will print 3
# so to assign it to a variablesv = getNumber()v1 = next(v) #v1 will have 1v2 = next(v) #v2 will have 2v3 = next(v) #v3 will have 3

重点内容

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

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

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

  • 当在生成器上调用next(g)时,函数会从停止的地方恢复执行。

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

编写和理解生成器的技术

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

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

看看它输出了什么:

>>> f(3)0011022

理解该函数后,将yield替换为print以获得生成相同值的生成器:

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

其中给出:

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

迭代器协议

“产量是什么”的答案可以简短而简单,但它是更大世界的一部分,即所谓的“迭代器协议”。

在迭代器协议的发送方,有两种相关的对象。可迭代对象是你可以循环的东西。迭代器是跟踪循环状态的对象。

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

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

为了使这一切对我们来说更容易,for-loops代表我们调用iter和next:

>>> for x in s:...     print(x)...102030

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

yield用于创建generator。将生成器视为迭代器,在每次迭代中为您提供价值。当您在循环中使用产量时,您会得到一个生成器对象,您可以使用它以迭代方式从循环中获取项目

什么是Python中的产量?

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

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

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

当你使用带有返回值的函数时,每次调用该函数时,它都会从一组新的变量开始。相反,如果你使用生成器函数而不是普通函数,执行将从它最后离开的地方开始。

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

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

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

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

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

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

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

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

def valid_rows():for i, row in df.iterrows(): # iterate over each row of spreadsheetif row == blank: # pseudo code, check if row is non-blank...continueif past_last_row: # pseudo code, check for end of input databreakyield i, row

请注意,该方法的每次调用都将返回下一行,除非读取所有行并且for完成,否则该方法通常将return。下一次调用将开始一个新的for循环。

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

for i, row in valid_rows():f(row)
for i, row in valid_rows():g(row)
nr_valid_rows = len(list(valid_rows()))

这就是全部。请注意,我没有使用像迭代器、生成器、协议、协程这样的术语。我认为这个简单的例子适用于我们的许多日常编码。

#0

  • 可以通过停止函数多次从函数返回值。
  • 可以像yield from一样使用from
  • 在返回大数据时使用,将其划分为小块数据,以防止大量使用RAM。

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

def test():yield 'One'                  # Stop, return 'One' and resumeyield 'Two'                  # Stop, return 'Two' and resumeyield 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.pyOneTwoThreeFour

此外,当将returnyield一起使用时,无法从return获取值:

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.pyOneTwoThreeFourTraceback (most recent call last):File "C:\Users\kai\yield_test.py", line 12, in <module>print(next(x))^^^^^^^StopIteration: Five