“ yield”关键字有什么作用?
yield
关键字在Python中的用途是什么?
例如,我试图理解这段代码1:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
这是呼叫者:
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
_get_child_candidates
调用该方法会发生什么?是否返回列表?一个元素?再叫一次吗?后续通话什么时候停止?
1.这段代码是由Jochen Schulz(jrschulz)编写的,Jochen Schulz是一个很好的用于度量空间的Python库。这是完整源代码的链接:Module mspace。
(我下面的回答仅从使用Python生成器的角度讲,而不是生成器机制的基础实现,它涉及堆栈和堆操作的一些技巧。)
在python函数中
yield
使用when 代替areturn
时,该函数变成了一个特殊的名称generator function
。该函数将返回一个generator
类型的对象。该yield
关键字是一个标志,通知蟒蛇编译器将特殊对待这样的功能。普通函数将在返回一些值后终止。但是在编译器的帮助下,可以将 generator函数视为可恢复的。也就是说,将恢复执行上下文,并且将从上次运行继续执行。在您显式调用return之前,它将引发StopIteration
异常(这也是迭代器协议的一部分),或到达函数的结尾。我发现了很多关于引用的generator
,但是这一个从中functional programming perspective
是最易消化的。(现在,我想根据我自己的理解来讨论其背后的原理
generator
和iterator
基础。我希望这可以帮助您掌握迭代器和生成器的基本动机。这种概念也出现在其他语言中,例如C#。)据我了解,当我们要处理一堆数据时,通常先将数据存储在某个地方,然后再逐一处理。但是这种幼稚的方法是有问题的。如果数据量巨大,则预先存储它们是很昂贵的。因此
data
,为什么不直接存储自身,为什么不metadata
间接存储某种形式,即the logic how the data is computed
。有两种包装此类元数据的方法。
as a class
。这就是所谓的iterator
实现迭代器协议的人(即__next__()
和__iter__()
方法)。这也是常见的迭代器设计模式。as a function
。这就是所谓的generator function
。但是在后台,返回的generator object
静态IS-A
迭代器仍然存在,因为它也实现了迭代器协议。无论哪种方式,都会创建一个迭代器,即某个可以为您提供所需数据的对象。OO方法可能有点复杂。无论如何,要使用哪一个取决于您。
以下是一些Python示例,这些示例说明如何实际实现生成器,就像Python没有为其提供语法糖一样:
作为Python生成器:
使用词法闭包而不是生成器
使用对象闭包而不是生成器(因为ClosuresAndObjectsAreEquivalent)
这是一个简单的示例:
输出:
我不是Python开发人员,但是在我看来,它
yield
保持着程序流程的位置,并且下一个循环从“ yield”位置开始。好像它在那个位置上等待,就在那之前,在外面返回一个值,下一次继续工作。这似乎是一种有趣而又不错的能力:D
该
yield
关键字简单地收集返回结果。想想yield
像return +=
产量是一个对象
一个
return
函数中的将返回单个值。如果要让函数返回大量值,请使用
yield
。更重要的
yield
是,是一个障碍。也就是说,它将从头开始运行您函数中的代码,直到命中为止
yield
。然后,它将返回循环的第一个值。然后,其他所有调用将再次运行您在函数中编写的循环,返回下一个值,直到没有任何值可返回为止。
yield
就像函数的返回元素一样。区别在于,yield
元素将功能转换为生成器。生成器的行为就像函数一样,直到“屈服”为止。生成器将停止运行,直到下一次调用为止,并从与启动时完全相同的位置继续运行。您可以通过调用来获得所有“屈服”值的序列list(generator())
。所有好的答案,但是对于新手来说有点困难。
我想你已经学会了
return
声明。作为一个比喻,
return
和yield
是一对双胞胎。return
表示“返回并停止”,而“收益”则表示“返回但继续”运行:
看,您只会得到一个数字,而不是列表。
return
永远不要让你高高兴兴,只实现一次就退出。替换
return
为yield
:现在,您将赢得所有数字。
与
return
计划运行一次和停止的yield
时间进行比较。你可以理解return
为return one of them
,和yield
作为return all of them
。这称为iterable
。这是的核心
yield
。列表
return
输出和对象yield
输出之间的区别是:您将始终从列表对象获取[0,1,2],但只能从“对象
yield
输出”中检索一次。因此,它具有一个新的名称generator
对象,如中所示Out[11]: <generator object num_list at 0x10327c990>
。最后,作为一个隐喻,它可以:
return
并且yield
是双胞胎list
并且generator
是双胞胎这是简单语言的示例。我将提供高级人类概念与低级Python概念之间的对应关系。
我想对数字序列进行运算,但是我不想为创建该序列而烦恼自己,我只想着重于自己想做的运算。因此,我执行以下操作:
此步骤对应于
def
生成器函数,即包含yield
。此步骤对应于调用生成器函数,该函数返回生成器对象。请注意,您还没有告诉我任何数字。你只要拿起纸和铅笔。
此步骤对应于调用
.next()
生成器对象。此步骤对应于生成器对象结束其工作并引发
StopIteration
异常。生成器函数不需要引发异常。函数结束或发出时,它将自动引发return
。这就是生成器的功能(包含的函数
yield
);它开始执行,在执行时暂停yield
,并在要求输入.next()
值时从上一个点继续执行。它在设计上与Python的迭代器协议非常吻合,该协议描述了如何顺序请求值。迭代器协议最著名的用户是
for
Python中的命令。因此,无论何时执行以下操作:不管
sequence
是列表,字符串,字典还是如上所述的生成器对象,都没有关系;结果是一样的:您从一个序列中逐个读取项目。注意,
def
包含一个yield
关键字的函数并不是创建生成器的唯一方法;这是创建一个的最简单方法。有关更准确的信息,请阅读Python文档中有关迭代器类型,yield语句和生成器的信息。
就像每个答案所建议的那样,
yield
用于创建序列生成器。它用于动态生成一些序列。例如,在网络上逐行读取文件时,可以使用以下yield
功能:您可以在代码中使用它,如下所示:
执行控制转移陷阱
执行控制将从getNextLines()转移到
for
yield时循环中。因此,每次调用getNextLines()时,都会从上次暂停的位置开始执行。因此,简而言之,具有以下代码的函数
将打印
一个简单的例子来了解它是什么:
yield
输出为:
在描述如何使用生成器的许多很棒的答案中,我还没有给出一种答案。这是编程语言理论的答案:
yield
Python中的语句返回一个生成器。Python中的生成器是一个返回延续的函数(特别是协程的一种,但是延续代表了一种更通用的机制来了解正在发生的事情)。编程语言理论中的延续是一种更为基础的计算,但是由于它们很难推理而且也很难实现,因此并不经常使用。但是关于延续是什么的想法很简单:只是尚未完成的计算状态。在此状态下,将保存变量的当前值,尚未执行的操作等。然后,在程序的稍后某个点,可以调用延续,以便将程序的变量重置为该状态,并执行保存的操作。
以这种更一般的形式进行的延续可以两种方式实现。在
call/cc
方式,程序的堆栈字面上保存,然后调用延续时,堆栈恢复。在延续传递样式(CPS)中,延续只是普通的函数(仅在函数是第一类的语言中),程序员可以对其进行显式管理并传递给子例程。用这种风格,程序状态由闭包(以及恰好在其中编码的变量)表示,而不是驻留在栈中某个位置的变量。管理控制流的函数接受连续作为参数(在CPS的某些变体中,函数可以接受多个连续),并通过简单地调用它们并随后返回来调用它们来操纵控制流。延续传递样式的一个非常简单的示例如下:
在这个(非常简单的)示例中,程序员保存了将文件实际写入连续的操作(该操作可能是非常复杂的操作,需要写出很多细节),然后传递该连续(即,首先类关闭)到另一个进行更多处理的运算符,然后在必要时调用它。(我在实际的GUI编程中经常使用此设计模式,这是因为它节省了我的代码行,或更重要的是,在GUI事件触发后管理控制流。)
在不失一般性的前提下,本文的其余部分将连续性概念化为CPS,因为它很容易理解和阅读。
现在让我们谈谈Python中的生成器。生成器是延续的特定子类型。而延续能够在一般的保存状态计算(即程序调用堆栈),发电机只能保存迭代的状态经过一个迭代器。虽然,对于发电机的某些用例,此定义有些误导。例如:
这显然是一个合理的迭代器,其行为已得到很好的定义-每次生成器对其进行迭代时,它都将返回4(并且永远如此)。但是,在考虑迭代器(即
for x in collection: do_something(x)
)时,可能不会想到可迭代的原型类型。此示例说明了生成器的功能:如果有什么是迭代器,生成器可以保存其迭代状态。重申一下:连续可以保存程序堆栈的状态,而生成器可以保存迭代的状态。这意味着延续比生成器强大得多,但是生成器也非常简单。它们对于语言设计者来说更容易实现,对程序员来说也更容易使用(如果您有时间要燃烧,请尝试阅读并理解有关延续和call / cc的本页)。
但是您可以轻松地将生成器实现(并概念化)为连续传递样式的一种简单的特定情况:
每当
yield
调用时,它告诉函数返回一个延续。再次调用该函数时,将从中断处开始。因此,在伪伪代码(即不是伪代码,而不是代码)中,生成器的next
方法基本上如下:其中,
yield
关键字实际上是真正的发电机功能语法糖,基本上是这样的:请记住,这只是伪代码,Python中生成器的实际实现更为复杂。但是,作为练习以了解发生了什么,请尝试使用连续传递样式来实现生成器对象,而不使用
yield
关键字。我本来打算发布“阅读Beazley的“ Python:基本参考”的第19页,以快速了解生成器”,但是已经有许多其他人已经发布了不错的描述。
另外,请注意,它们
yield
可以在协程中用作生成器函数的双重用法。尽管它与您的代码段用法不同,(yield)
但是可以用作函数中的表达式。当调用者使用该send()
方法向该方法发送值时,协程将执行直到(yield)
遇到下一条语句。生成器和协程是设置数据流类型应用程序的一种很酷的方法。我认为有必要了解该
yield
语句在函数中的其他用法。TL; DR
代替这个:
做这个:
每当您发现自己从头开始建立清单时,就
yield
逐一列出。这是我第一次屈服。
yield
是一种含糖的方式来表达相同的行为:
不同的行为:
收益是单次通过:您只能迭代一次。当一个函数包含一个yield时,我们称其为Generator函数。而迭代器就是它返回的内容。这些术语在揭示。我们失去了容器的便利性,但获得了按需计算且任意长的序列的功效。
产量懒惰,它推迟了计算。当您调用函数时,其中包含yield的函数实际上根本不会执行。它返回一个迭代器对象,该对象记住它从何处中断。每次您调用
next()
迭代器(这在for循环中发生)时,执行都会向前推进到下一个收益。return
引发StopIteration并结束系列(这是for循环的自然结束)。产量多才多艺。数据不必全部存储在一起,可以一次存储一次。它可以是无限的。
如果您需要多次通过并且系列不太长,只需调用
list()
它:单词的出色选择,
yield
因为两种含义都适用:...提供系列中的下一个数据。
...放弃CPU执行,直到迭代器前进。
还有另一个
yield
用途和含义(自Python 3.3起):从PEP 380-委托给子生成器的语法:
此外,这将引入(自Python 3.5起):
为了避免将协程与常规生成器混淆(今天
yield
两者都使用)。该
yield
关键字被减少到两个简单的事实:yield
关键字,则该函数不再通过该语句返回。相反,它立即返回一个懒惰的“等候名单”对象称为发电机return
list
or或set
orrange
或dict-view一样,它带有用于以特定顺序访问每个元素的内置协议。简而言之:生成器是一个懒惰的,增量待定的list,并且
yield
语句允许您使用函数符号来编程生成器应逐渐吐出的列表值。例
让我们定义一个
makeRange
类似于Python的函数range
。调用makeRange(n)
“返回生成器”:要强制生成器立即返回其待处理的值,可以将其传递给
list()
(就像任何可迭代的一样):将示例与“仅返回列表”进行比较
可以将上面的示例视为仅创建一个列表,将其追加并返回:
但是,有一个主要区别。请参阅最后一节。
您如何使用发电机
可迭代是列表理解的最后一部分,并且所有生成器都是可迭代的,因此经常像这样使用它们:
为了更好地了解发电机,您可以使用该
itertools
模块(一定要使用chain.from_iterable
而不是chain
在保修期内)。例如,您甚至可以使用生成器来实现无限长的惰性列表,例如itertools.count()
。您可以实现自己的def enumerate(iterable): zip(count(), iterable)
,也可以yield
在while循环中使用关键字来实现。请注意:生成器实际上可以用于更多事情,例如实现协程或不确定性编程或其他优雅的事情。但是,我在这里提出的“惰性列表”观点是您会发现的最常见用法。
幕后花絮
这就是“ Python迭代协议”的工作方式。就是说,当你做什么的时候
list(makeRange(5))
。这就是我之前所说的“懒惰的增量列表”。内置函数
next()
只调用对象.next()
函数,它是“迭代协议”的一部分,可以在所有迭代器上找到。您可以手动使用该next()
函数(以及迭代协议的其他部分)来实现一些奇特的事情,通常是以牺牲可读性为代价的,因此请避免这样做。细节
通常,大多数人不会关心以下区别,并且可能想在这里停止阅读。
用Python来说,可迭代对象是“了解for循环的概念”的任何对象,例如列表
[1,2,3]
,而迭代器是所请求的for循环的特定实例,例如[1,2,3].__iter__()
。甲发生器是完全一样的任何迭代器,除了它是写(带有功能语法)的方式。当您从列表中请求迭代器时,它将创建一个新的迭代器。但是,当您从迭代器请求迭代器时(很少这样做),它只会为您提供自身的副本。
因此,在极少数情况下,您无法执行此类操作...
...然后记住生成器是迭代器 ; 即是一次性使用。如果要重用它,应该
myRange(...)
再次调用。如果需要两次使用结果,请将结果转换为列表并将其存储在变量中x = list(myRange(5))
。那些绝对需要克隆生成器的人(例如,正在进行骇人的骇人的元编程的人)可以itertools.tee
在绝对必要的情况下使用,因为可复制的迭代器Python PEP标准建议已被推迟。收益给您一个发电机。
如您所见,在第一种情况下,
foo
将整个列表立即保存在内存中。对于包含5个元素的列表来说,这并不是什么大问题,但是如果您想要500万个列表,该怎么办?这不仅是一个巨大的内存消耗者,而且在调用该函数时还花费大量时间来构建。在第二种情况下,
bar
只需为您提供一个生成器。生成器是可迭代的-意味着您可以在for
循环等中使用它,但是每个值只能被访问一次。所有的值也不会同时存储在存储器中。生成器对象“记住”您上次调用它时在循环中的位置-这样,如果您使用的是一个迭代的(例如)计数为500亿,则不必计数为500亿立即存储500亿个数字以进行计算。再次,这是一个非常人为的示例,如果您真的想计数到500亿,则可能会使用itertools。:)
这是生成器最简单的用例。如您所说,它可以用来编写有效的排列,使用yield可以将内容推入调用堆栈,而不是使用某种堆栈变量。生成器还可以用于特殊的树遍历以及所有其他方式。
对于那些偏爱简单工作示例的人,请在此交互式Python会话中进行冥想:
它正在返回发电机。我对Python并不是特别熟悉,但是我相信,如果您熟悉Python,它与C#的迭代器块是一样的东西。
关键思想是,编译器/解释器/无论做什么都做一些技巧,以便就调用者而言,他们可以继续调用next(),并且将继续返回值- 就像Generator方法已暂停一样。现在显然您无法真正“暂停”方法,因此编译器将构建状态机,以供您记住当前位置以及局部变量等。这比自己编写迭代器要容易得多。
还有另外一件事要提到:yield的函数实际上不必终止。我写了这样的代码:
然后我可以在其他代码中使用它:
它确实有助于简化某些问题,并使某些事情更易于使用。
理解的捷径
yield
当您看到带有
yield
语句的函数时,请应用以下简单技巧,以了解将发生的情况:result = []
在函数的开头插入一行。yield expr
有result.append(expr)
。return result
在函数底部插入一行。yield
陈述!阅读并找出代码。这个技巧可能会让您对函数背后的逻辑
yield
有所了解,但是实际发生的事情与基于列表的方法发生的事情明显不同。在许多情况下,yield方法也将具有更高的内存效率和更快的速度。在其他情况下,即使原始函数运行正常,此技巧也会使您陷入无限循环。请继续阅读以了解更多信息...不要混淆您的Iterable,Iterators和Generators
首先,迭代器协议 -当您编写时
Python执行以下两个步骤:
获取以下项的迭代器
mylist
:调用
iter(mylist)
->这将返回带有next()
方法的对象(或__next__()
在Python 3中)。[这是大多数人忘记告诉您的步骤]
使用迭代器遍历项目:
继续
next()
在从步骤1返回的迭代器上调用该方法。从的返回值next()
被分配给x
并执行循环体。如果StopIteration
从内部引发异常next()
,则意味着迭代器中没有更多值,并且退出了循环。事实是,Python在想要遍历对象内容的任何时候都执行上述两个步骤-因此它可能是for循环,但也可能是类似的代码
otherlist.extend(mylist)
(其中otherlist
是Python列表)。这
mylist
是一个可迭代的,因为它实现了迭代器协议。在用户定义的类中,可以实现该__iter__()
方法以使您的类的实例可迭代。此方法应返回迭代器。迭代器是带有next()
方法的对象。它可以同时实现__iter__()
,并next()
在同一类,并有__iter__()
回报self
。这适用于简单的情况,但是当您希望两个迭代器同时在同一个对象上循环时,则不能使用。这就是迭代器协议,许多对象都实现了该协议:
__iter__()
。请注意,
for
循环不知道它要处理的是哪种对象-它仅遵循迭代器协议,并且很高兴在调用时逐项获取next()
。内置列表一一返回其项,字典一一返回键,文件一一返回行,依此类推。生成器返回...就是这样yield
:yield
如果没有三个return
语句,f123()
则只执行第一个语句,而不是语句,然后函数将退出。但是f123()
没有普通的功能。当f123()
被调用时,它不会返回yield语句中的任何值!它返回一个生成器对象。另外,该函数并没有真正退出-进入了挂起状态。当for
循环尝试遍历生成器对象时,该函数从yield
先前返回的下一行从其挂起状态恢复,执行下一行代码(在这种情况下为yield
语句),并将其作为下一行返回项目。这会一直发生,直到函数退出,此时生成器将引发StopIteration
,然后循环退出。因此,生成器对象有点像适配器-一端通过公开
__iter__()
和next()
保持for
循环满意的方法展示了迭代器协议。但是,在另一端,它恰好运行该函数以从中获取下一个值,并将其放回暂停模式。为什么使用发电机?
通常,您可以编写不使用生成器但实现相同逻辑的代码。一种选择是使用我之前提到的临时列表“技巧”。这并非在所有情况下都可行,例如,如果您有无限循环,或者当您的列表很长时,这可能会导致内存使用效率低下。另一种方法是实现一个新的可迭代类SomethingIter,该类将状态保留在实例成员中,并在其
next()
(或__next__()
Python 3)方法中执行下一个逻辑步骤。根据逻辑,next()
方法中的代码可能最终看起来非常复杂,并且容易出现错误。在这里,发电机提供了一种干净而简单的解决方案。yield
就像return
-它返回您告诉的内容(作为生成器)。不同之处在于,下次调用生成器时,执行将从上一次调用yield
语句开始。与return不同的是,在产生良率时不会清除堆栈帧,但是会将控制权转移回调用方,因此下次调用该函数时,其状态将恢复。就您的代码而言,该函数
get_child_candidates
的作用类似于迭代器,以便在扩展列表时,它一次将一个元素添加到新列表中。list.extend
调用迭代器,直到耗尽为止。在您发布的代码示例的情况下,只返回一个元组并将其附加到列表中会更加清楚。你的回答