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),他为度量空间创建了一个很棒的Python库.这是完整源代码的链接:模块mspace.
要了解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...
迭代的; lists
,strings
,文件...
这些迭代很方便,因为您可以根据需要阅读它们,但是您将所有值存储在内存中,当您拥有大量值时,这并不总是您想要的.
生成器是迭代器,是一种只能迭代一次的迭代器.生成器不会将所有值存储在内存中,它们会动态生成值:
>>> 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 createGenerator(): ... mylist = range(3) ... for i in mylist: ... yield i*i ... >>> mygenerator = createGenerator() # create a generator >>> print(mygenerator) # mygenerator is an object!>>> for i in mygenerator: ... print(i) 0 1 4
这是一个无用的例子,但是当你知道你的函数将返回一组你只需要阅读一次的大量值时它会很方便.
要掌握yield
,您必须明白,当您调用该函数时,您在函数体中编写的代码不会运行.该函数只返回生成器对象,这有点棘手:-)
然后,您的代码将从每次for
使用生成器时停止的位置继续.
现在困难的部分:
第一次for
从函数调用生成器对象时,它将从头开始运行函数中的代码,直到它命中yield
,然后它将返回循环的第一个值.然后,每个其他调用将再次运行您在函数中写入的循环,并返回下一个值,直到没有值返回.
一旦函数运行,生成器被认为是空的,但是不再命中yield
.这可能是因为循环已经结束,或者因为你不再满足"if/else"
了.
发电机:
# Here you create the method of the node object that will return the generator def _get_child_candidates(self, distance, min_dist, max_dist): # Here is the code that will be called each time you use the generator object: # If there is still a child of the node object on its left # AND if the distance is ok, return the next child if self._leftchild and distance - max_dist < self._median: yield self._leftchild # If there is still a child of the node object on its right # AND if the distance is ok, return the next child if self._rightchild and distance + max_dist >= self._median: yield self._rightchild # If the function arrives here, the generator will be considered empty # there is no more than two values: the left and the right children
呼叫者:
# Create an empty list and a list with the current object reference result, candidates = list(), [self] # Loop on candidates (they contain only one element at the beginning) while candidates: # Get the last candidate and remove it from the list node = candidates.pop() # Get the distance between obj and the candidate distance = node._get_dist(obj) # If distance is ok, then you can fill the result if distance <= max_dist and distance >= min_dist: result.extend(node._values) # Add the children of the candidate in the candidate's list # so the loop will keep running until it will have looked # at all the children of the children of the children, etc. of the candidate candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) return result
此代码包含几个智能部分:
循环在列表上迭代,但是循环迭代时列表会扩展:-)这是一种简单的方法来遍历所有这些嵌套数据,即使它有点危险,因为你最终可以得到一个无限循环.在这种情况下,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
耗尽生成器的所有值,但while
不断创建新的生成器对象,这些对象将生成与之前的值不同的值,因为它不应用于同一节点.
该extend()
方法是一个列表对象方法,它需要一个iterable并将其值添加到列表中.
通常我们将列表传递给它:
>>> a = [1, 2] >>> b = [3, 4] >>> a.extend(b) >>> print(a) [1, 2, 3, 4]
但是在你的代码中它得到了一个生成器,这很好,因为:
您不需要两次读取值.
您可能有很多孩子,并且您不希望它们都存储在内存中.
它的工作原理是因为Python不关心方法的参数是否是列表.Python期望iterables所以它将适用于字符串,列表,元组和生成器!这叫做鸭子打字,这也是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())>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs >>> print(wall_street_atm.next()) >>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty >>> print(corner_street_atm.next()) >>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business >>> for cash in brand_new_atm: ... print cash $100 $100 $100 $100 $100 $100 $100 $100 $100 ...
注意:对于Python 3,请使用print(corner_street_atm.__next__())
或print(next(corner_street_atm))
它可用于控制对资源的访问等各种事物.
itertools模块包含操作iterables的特殊函数.曾经希望复制一台发电机?链两个发电机?使用单行分组嵌套列表中的值?Map / Zip
没有创建另一个列表?
然后就是import itertools
.
一个例子?让我们来看看四匹马比赛的可能到达顺序:
>>> horses = [1, 2, 3, 4] >>> races = itertools.permutations(horses) >>> print(races)>>> 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
循环如何工作的更多信息.
yield
当你看到一个带yield
语句的函数时,应用这个简单的技巧来理解会发生什么:
result = []
在函数的开头插入一行.
替换每个yield expr
有result.append(expr)
.
return result
在函数底部插入一行.
耶 - 没有更多的yield
陈述!阅读并找出代码.
将功能与原始定义进行比较
这个技巧可以让你了解函数背后的逻辑,但实际发生的情况与yield
基于列表的方法中发生的情况明显不同.在许多情况下,yield方法将更高效,更快.在其他情况下,即使原始函数工作得很好,这个技巧也会让你陷入无限循环.请继续阅读以了解更多信息...
首先,迭代器协议 - 当你写
for x in mylist: ...loop body...
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
进来的地方:
def f123(): yield 1 yield 2 yield 3 for item in f123(): print item
而不是yield
语句,如果只有第一个return
语句中有三个语句f123()
将被执行,函数将退出.但是f123()
没有普通的功能.当f123()
被调用时,它不返回任何值在yield语句!它返回一个生成器对象.此外,该功能并没有真正退出 - 它进入暂停状态.当for
循环尝试遍历生成器对象时,该函数从yield
之前返回的最后一行恢复其挂起状态,执行下一行代码,在本例中为一个yield
语句,并将其作为下一项返回.这种情况一直发生,直到函数退出,此时发生器上升StopIteration
,循环退出.
因此,生成器对象有点像适配器 - 在一端它展示了迭代器协议,通过公开__iter__()
和next()
方法来保持for
循环满意.然而,在另一端,它运行该功能足以从中获取下一个值,并将其重新置于挂起模式.
通常,您可以编写不使用生成器但实现相同逻辑的代码.一种选择是使用我之前提到的临时列表'技巧'.这在所有情况下都不起作用,例如,如果你有无限循环,或者当你有一个很长的列表时,它可能会使内存的使用效率低下.另一种方法是实现一个新的可迭代类SomethingIter
,它将状态保存在实例成员中,并在其中next()
(或__next__()
在Python 3中)方法中执行下一个逻辑步骤.根据逻辑,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 += 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
为了更深入地了解幕后发生的事情,next()
可以将循环重写为:
iterator = some_func() try: while 1: print iterator.next() except StopIteration: pass
这更有意义还是让你更加迷惑?:)
我要指出,这是为了说明的目的过于简单化.:)
该yield
关键字简化为两个简单的事实:
如果编译器在函数内的任何位置检测到yield
关键字,则该函数不再通过该语句返回.相反,它会立即返回一个称为生成器的惰性"挂起列表"对象return
生成器是可迭代的.什么是可迭代的?这就像一个东西list
或set
或range
或字典视图,具有内置的协议以某种顺序访问每一个元素.
简而言之:生成器是一个惰性的,递增挂起的列表,并且yield
语句允许您使用函数表示法来编程生成器应逐渐吐出的列表值.
generator = myYieldingFunction(...) x = list(generator) 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 list==[x[0], x[1], x[2]]
让我们定义一个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)
要强制生成器立即返回其挂起值,您可以将其传递给list()
(就像任何可迭代的一样):
>>> list(makeRange(5)) [0, 1, 2, 3, 4]
上面的例子可以被认为只是创建一个你追加并返回的列表:
# list-version # # generator-version 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 ## indented 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)
,或者yield
使用while循环中的关键字来实现.
请注意:生成器实际上可以用于更多的事情,例如实现协同程序或非确定性编程或其他优雅的东西.但是,我在这里提出的"懒惰列表"观点是您会发现的最常见的用途.
这就是"Python迭代协议"的工作原理.也就是说,当你这样做时会发生什么list(makeRange(5))
.这就是我之前描述的"懒惰的增量列表".
>>> x=iter(range(5)) >>> next(x) 0 >>> next(x) 1 >>> next(x) 2 >>> next(x) 3 >>> next(x) 4 >>> next(x) Traceback (most recent call last): File "", line 1, in StopIteration
内置函数next()
只调用对象.next()
函数,它是"迭代协议"的一部分,可以在所有迭代器上找到.您可以手动使用next()
函数(以及迭代协议的其他部分)来实现奇特的东西,通常以牺牲可读性为代价,因此尽量避免这样做......
通常情况下,大多数人不会关心以下区别,可能想在这里停止阅读.
在Python语言中,iterable是任何"理解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))
.那些绝对需要克隆生成器的人(例如,谁正在做可怕的hackish元编程)可以使用,itertools.tee
如果绝对必要,因为可复制的迭代器Python PEP标准提案已被推迟.
回答大纲/摘要什么是
yield
关键词在Python呢?
yield
调用时,函数返回一个Generator.
生成器是迭代器,因为它们实现了迭代器协议,因此您可以迭代它们.
还可以向生成器发送信息,使其在概念上成为协程.
在Python 3中,您可以使用两个方向从一个生成器委派给另一个生成器yield from
.
(附录批评了几个答案,包括最重要的答案,并讨论了return
在发电机中的使用.)
发电机:
yield
在函数定义中只是合法的,并且在函数定义中包含yield
它使它返回一个生成器.
生成器的想法来自其他语言(见脚注1),具有不同的实现.在Python的Generators中,代码的执行在yield的时候被冻结.当调用生成器时(下面讨论方法),执行重新开始,然后在下一个yield时冻结.
yield
提供了一种实现迭代器协议的简单方法,该协议由以下两种方法定义:
__iter__
和next
(Python 2)或__next__
(Python 3).这两种方法都使对象成为迭代器,您可以使用模块中的Iterator
Abstract Base Class进行类型检查collections
.
>>> def func(): ... yield 'I am' ... yield 'a generator!' ... >>> type(func) # A function with yield is still a function>>> gen = func() >>> type(gen) # but it returns a 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
上面的简单生成器也等同于下面 - 从Python 3.3开始(在Python 2中不可用),你可以使用yield from
:
def func(an_iterable): yield from an_iterable
但是,yield from
也允许委托给子发电机,这将在下面关于与子协同程序的协同授权的部分中解释.
yield
形成一个表达式,允许将数据发送到发生器(见脚注3)
下面是一个示例,请注意该received
变量,该变量将指向发送到生成器的数据:
def bank_account(deposited, interest_rate): while True: calculated_interest = interest_rate * deposited received = yield calculated_interest if received: deposited += received >>> my_account = bank_account(1000, .05)
首先,我们必须使用内置函数排队生成器next
.它将调用适当的next
或__next__
方法,具体取决于您使用的Python版本:
>>> first_year_interest = next(my_account) >>> first_year_interest 50.0
现在我们可以将数据发送到生成器.(发送None
与呼叫相同next
.):
>>> next_year_interest = my_account.send(first_year_interest + 1000) >>> next_year_interest 102.5
yield from
现在,回想一下yield from
Python 3中提供的内容.这允许我们将协同程序委托给子协会:
def money_manager(expected_rate): under_management = yield # must receive deposited value 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''' 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''' next(manager) # must queue up manager manager.send(deposited) while True: try: yield from manager except GeneratorExit: return manager.close()
现在我们可以将功能委托给子发生器,它可以像生成器一样使用,如上所述:
>>> my_manager = money_manager(.06) >>> my_account = investment_account(1000, my_manager) >>> first_year_return = next(my_account) >>> first_year_return 60.0 >>> next_year_return = my_account.send(first_year_return + 1000) >>> next_year_return 123.6
你可以阅读更多的精确语义yield from
在PEP 380.
该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 "结论", line 4, in File " ", line 2, in ValueError
我相信我已经涵盖了以下问题的所有方面:
什么是
yield
关键词在Python呢?
事实证明,yield
做了很多.我相信我可以为此添加更全面的例子.如果您想要更多或有一些建设性的批评,请通过下面的评论告诉我.
关于什么使可迭代成为混乱,仅使用列表作为示例.请参阅上面的参考资料,但总结一下:iterable有一个__iter__
返回迭代器的方法.一个迭代器提供了一个.next
(Python 2里或.__next__
(Python 3的)方法,它是隐式由称为for
循环,直到它提出StopIteration
,并且一旦这样做,将继续这样做.
然后它使用生成器表达式来描述生成器是什么.因为生成器只是创建迭代器的一种方便方法,所以它只会混淆事情,我们还没有完成这个yield
部分.
在控制生成器耗尽时,他调用该.next
方法,而不是他应该使用内置函数,next
.这将是一个适当的间接层,因为他的代码在Python 3中不起作用.
Itertools?这与什么yield
都没有关系.
没有讨论在Python 3 中yield
提供的新功能的方法.顶部/接受的答案是一个非常不完整的答案.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写道:
在Sun,2017年1月29日16:39 Craig Rodrigues写道:
无论采用哪种方法我都行.恕我直言,将它们放在Python 3中的方式并不好.
我的投票是它是一个SyntaxError,因为你没有得到你对语法的期望.
我同意这对我们来说是一个明智的地方,因为任何依赖当前行为的代码实在太聪明而无法维护.
在达到目标方面,我们可能希望:
3.7中的SyntaxWarning或DeprecationWarning
2.7.x中的Py3k警告
3.8中的SyntaxError
干杯,尼克.
- Nick Coghlan | ncoghlan at gmail.com | 澳大利亚布里斯班
此外,还有一个突出的问题(10544)似乎指向了这个永远不是一个好主意的方向(PyPy,一个用Python编写的Python实现,已经提出了语法警告.)
最重要的是,直到CPython的开发人员告诉我们:不要投入yield
生成器表达或理解.
return
发电机中的声明在Python 2中:
在生成器函数中,
return
不允许该语句包含expression_list
.在该上下文中,裸return
表示生成器已完成并将导致StopIteration
生成.
An expression_list
基本上是用逗号分隔的任意数量的表达式 - 实际上,在Python 2中,您可以使用停止生成器return
,但不能返回值.
在Python 3中:
在生成器函数中,该
return
语句指示生成器已完成并将导致StopIteration
生成.返回值(如果有)用作构造的参数StopIteration
并成为StopIteration.value
属性.
提案中引用了语言CLU,Sather和Icon,以将生成器的概念引入Python.一般的想法是函数可以维持内部状态并根据用户的要求产生中间数据点.这承诺在性能上优于其他方法,包括Python线程,这在某些系统上甚至不可用.
这意味着,例如,xrange
对象(range
在Python 3中)不是Iterator
s,即使它们是可迭代的,因为它们可以被重用.与列表一样,它们的__iter__
方法返回迭代器对象.
yield
最初是作为语句引入的,这意味着它只能出现在代码块中一行的开头.现在yield
创建一个yield表达式.
https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt
这种变化提出以允许用户将数据发送到发电机,就像一个会接受它.要发送数据,必须能够将其分配给某些内容,为此,语句将无效.
yield
就像return
- 它返回你告诉它的任何东西(作为一个发生器).区别在于下次调用生成器时,执行从最后一次调用yield
语句开始.与返回不同,当产生收益时,不会清除堆栈帧,但是控制将被传回给调用者,因此其状态将在下次调用函数时恢复.
对于代码,该函数get_child_candidates
的作用类似于迭代器,因此当您扩展列表时,它会一次向新列表添加一个元素.
list.extend
调用迭代器直到它耗尽.对于您发布的代码示例,只返回一个元组并将其附加到列表中将更加清晰.
还有一件事需要提及:一个实际上不必终止收益的函数.我编写了这样的代码:
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);
它确实有助于简化一些问题,并使一些事情更容易使用.
对于那些喜欢最小工作示例的人,请冥想这个交互式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
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时,我们将其称为生成函数.和迭代器是它返回.这些条款很有启发性.我们失去了容器的便利性,但是获得了根据需要计算的系列的能力,并且任意长.
收益是懒惰的,它推迟了计算.当你调用它时,一个带有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执行直到迭代器前进.
产量为您提供发电机.
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] barbar.next() 1 bar.next() 3 bar.next() 5
如您所见,在第一种情况下,foo会立即将整个列表保存在内存中.对于包含5个元素的列表来说,这不是什么大问题,但是如果你想要一个500万的列表怎么办?这不仅是一个巨大的内存消耗者,而且在调用函数时也需要花费大量时间来构建.在第二种情况下,bar只给你一个发电机.生成器是可迭代的 - 这意味着您可以在for循环等中使用它,但每个值只能被访问一次.所有值也不会同时存储在内存中; 生成器对象"记住"上次调用它时循环的位置 - 这样,如果你使用一个可迭代(比方说)计数到500亿,你就不必数到500亿全部立即存储500亿个数字.同样,这是一个非常人为的例子,如果你真的想要数到500亿,你可能会使用itertools.:)
这是生成器最简单的用例.正如你所说,它可以用来编写有效的排列,使用yield来通过调用堆栈推送,而不是使用某种堆栈变量.生成器也可以用于专门的树遍历,以及其他各种方式.
它正在返回一台发电机.我对Python并不是特别熟悉,但我相信它与C#的迭代器块相同,如果你熟悉它们的话.
关键的想法是编译器/解释器/无论做什么都有一些技巧,所以就调用者而言,他们可以继续调用next()并且它将保持返回值 - 就好像生成器方法被暂停一样.现在显然你不能真正"暂停"一个方法,所以编译器会建立一个状态机,让你记住你当前的位置以及局部变量等.这比自己编写迭代器容易得多.
在描述如何使用发电机的许多重要答案中,有一种我认为尚未给出的答案.这是编程语言理论的答案:
yield
Python中的语句返回一个生成器.Python中的生成器是一个返回continuation的函数(特别是一种coroutine,但continuation代表了理解正在发生的事情的更通用的机制).
编程语言理论的延续是一种更为基础的计算,但它们并不经常使用,因为它们极难推理并且也很难实现.但是延续的概念是直截了当的:它是尚未完成的计算状态.在此状态下,将保存变量的当前值,尚未执行的操作等.然后在程序的某个时刻,可以调用continuation,以便程序的变量重置为该状态,并执行保存的操作.
以这种更一般的形式,可以以两种方式实现继续.在call/cc
方式,程序的堆栈字面上保存,然后调用延续时,堆栈恢复.
在连续传递样式(CPS)中,continuation只是普通函数(仅在函数是第一类的语言中),程序员明确地管理它并传递给子例程.在这种风格中,程序状态由闭包(以及碰巧在其中编码的变量)表示,而不是驻留在堆栈中某处的变量.管理控制流的函数接受继续作为参数(在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)
在这个(非常简单的)示例中,程序员将实际写入文件的操作保存到一个延续中(这可能是一个非常复杂的操作,需要写出许多细节),然后传递该延续(即,作为第一个 - class closure)到另一个执行更多处理的运算符,然后在必要时调用它.(我在实际的GUI编程中经常使用这种设计模式,因为它节省了我的代码行,或者更重要的是,在GUI事件触发后管理控制流.)
在不失一般性的情况下,本文的其余部分将继续概念化为CPS,因为它更容易理解和阅读.
现在让我们来谈谈Python中的生成器.生成器是延续的特定子类型.虽然continuation通常能够保存计算的状态(即程序的调用堆栈),但生成器只能通过迭代器保存迭代状态.虽然这个定义对于某些发电机的使用情况略有误导.例如:
def f(): while True: yield 4
这显然是一个合理的迭代,其行为很明确 - 每次生成器迭代它,它返回4(并且永远这样做).但是,在考虑迭代器(即,for x in collection: do_something(x)
)时,它可能不是想到的典型迭代类型.这个例子说明了生成器的强大功能:如果有任何东西是迭代器,生成器可以保存它的迭代状态.
重申:Continuations可以保存程序堆栈的状态,生成器可以保存迭代状态.这意味着continuation比生成器更强大,但生成器也很多,更容易.它们对于语言设计者来说更容易实现,并且程序员更容易使用它们(如果你有时间刻录,尝试阅读和理解这个页面有关continuation和call/cc).
但是您可以轻松地将生成器实现(并概念化)为继续传递样式的简单特定情况:
无论什么时候yield
被调用,它都会告诉函数返回一个延续.再次调用该函数时,它从它停止的任何地方开始.因此,在伪伪代码(即,不是伪代码,而不是代码)中,生成器的next
方法基本如下:
class Generator(): def __init__(self,iterable,generatorfun): self.next_continuation = lambda:generatorfun(iterable) def next(self): value, next_continuation = self.next_continuation() self.next_continuation = next_continuation return value
其中yield
关键字实际上是实际生成器函数的语法糖,基本上类似于:
def generatorfun(iterable): if len(iterable) == 0: raise StopIteration else: return (iterable[0], lambda:generatorfun(iterable[1:]))
请记住,这只是伪代码,Python中生成器的实际实现更复杂.但是,作为一个理解正在发生的事情的练习,尝试使用延续传递样式来实现生成器对象而不使用yield
关键字.
这是一个简单语言的例子.我将提供高级人类概念与低级Python概念之间的对应关系.
我想操作一系列数字,但我不想因为创建该序列而烦扰我自己,我只想专注于我想要做的操作.所以,我做了以下事情:
我打电话给你,告诉你我想要一个以特定方式产生的数字序列,我告诉你算法是什么.
该步骤对应于def
生成器函数,即包含a的函数yield
.
过了一会儿,我告诉你,"好的,准备告诉我数字的顺序".
此步骤对应于调用返回生成器对象的生成器函数.请注意,你还没有告诉我任何数字; 你抓住你的纸和铅笔.
我问你,"告诉我下一个号码",你告诉我第一个号码; 之后,你等我问你下一个号码.这是你的工作,要记住你在哪里,你已经说过什么数字,以及下一个数字是什么.我不关心细节.
此步骤对应于调用.next()
生成器对象.
...重复上一步,直到......
最终,你可能会走到尽头.你没告诉我一个号码; 你只是喊道,"抓住你的马!我已经完成了!没有更多的数字!"
此步骤对应于生成器对象结束其作业,并引发StopIteration
异常生成器函数不需要引发异常.当函数结束或发出时,它会自动引发return
.
这就是生成器的作用(包含a的函数yield
); 它开始执行,只要它执行一次就暂停yield
,当被要求输入一个.next()
值时,它会从最后一次继续执行.它完全符合Python的迭代器协议的设计,它描述了如何顺序请求值.
迭代器协议最着名的用户是for
Python中的命令.所以,每当你做一个:
for item in sequence:
如果sequence
是如上所述的列表,字符串,字典或生成器对象并不重要; 结果是一样的:你逐个读取序列中的项目.
请注意,def
包含yield
关键字的函数不是创建生成器的唯一方法; 这只是创建一个最简单的方法.
有关更准确的信息,请阅读Python文档中的迭代器类型,yield语句和生成器.
虽然很多答案都说明了为什么要使用a yield
创建生成器,但有更多的用途yield
.制作协程非常容易,它可以在两个代码块之间传递信息.我不会重复任何已经给出的关于使用yield
创建生成器的精细示例.
为了帮助理解yield
以下代码中的功能,您可以用手指在任何具有代码的代码中跟踪循环yield
.每当你的手指击中时yield
,你必须等待a next
或a send
进入.当a next
被调用时,你会遍历代码,直到你点击yield
... yield
评估右侧的代码并返回给调用者...然后你等待.当next
被再次调用,您通过代码进行另一次循环.但是,您会注意到在协程中,yield
也可以使用send
...,它将从调用者发送一个值到屈服函数.如果send
给出a,则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()
还有另一种yield
用途和含义(自Python 3.3起):
yield from
从PEP 380 - 委托给子发电机的语法:
提出了一种语法,用于生成器将其部分操作委托给另一个生成器.这允许将包含'yield'的代码段分解出来并放在另一个生成器中.此外,允许子生成器返回一个值,并使该值可用于委派生成器.
当一个生成器重新生成另一个生成器生成的值时,新语法也会为优化提供一些机会.
此外,这将介绍(自Python 3.5):
async def new_coroutine(data): ... await blocking_action()
避免协程与常规发生器混淆(今天yield
两者都使用).
所有伟大的答案,但新手有点困难.
我假设你已经学会了这个return
陈述.
作为类比,return
并且yield
是双胞胎.return
意思是"回归和停止",而"产量"意味着"回归,但继续"
尝试获取num_list
return
.
def num_list(n): for i in range(n): return i
运行:
In [5]: num_list(3) Out[5]: 0
看,你只得到一个数字而不是它们的列表.return
永远不会让你高兴,只执行一次并退出.
来了
yield
替换return
为yield
:
In [10]: def num_list(n): ...: for i in range(n): ...: yield i ...: In [11]: num_list(3) Out[11]:In [12]: list(num_list(3)) Out[12]: [0, 1, 2]
现在,你赢了所有数字.
比较return
哪个运行一次并停止,yield
运行时间计划.你可以理解return
为return one of them
,和yield
作为return all of them
.这叫做iterable
.
我们可以
yield
用另一个步骤重写语句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]
这是关键的核心yield
.
列表return
输出和对象yield
输出之间的区别是:
您将总是从列表对象中获取[0,1,2],但只能从"对象yield
输出"中检索一次.因此,它有一个新的名称generator
对象,如图所示Out[11]:
.
总之,作为一个隐喻它的隐喻:
return
并且yield
是双胞胎
list
并且generator
是双胞胎
下面是一些如何实际实现生成器的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)
我将发布"阅读Beazley的'Python:基本参考'第19页以快速描述发生器",但是很多其他人已经发布了很好的描述.
另外,请注意,yield
可以在协同程序中使用它们作为它们在生成器函数中的双重使用.虽然它与您的代码片段的用法不同,(yield)
但可以用作函数中的表达式.当调用者使用该send()
方法向方法发送值时,协程将执行,直到(yield)
遇到下一个语句.
生成器和协同程序是设置数据流类型应用程序的一种很酷的方法.我认为yield
在函数中知道语句的其他用法是值得的.
从编程的角度来看,迭代器实现为thunk.
为了实现并发执行的迭代器,生成器和线程池等作为thunks(也称为匿名函数),使用发送给具有调度程序的闭包对象的消息,并且调度程序回答"消息".
http://en.wikipedia.org/wiki/Message_passing
" next "是发送到闭包的消息,由" 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 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
这是一个心理形象yield
.
我喜欢将一个线程视为具有堆栈(即使它没有以这种方式实现).
当调用普通函数时,它将其局部变量放在堆栈上,进行一些计算,然后清除堆栈并返回.它的局部变量的值再也看不到了.
使用yield
函数,当它的代码开始运行时(即在调用函数之后,返回一个生成器对象,next()
然后调用其方法),它同样将其局部变量放入堆栈并计算一段时间.但是,当它到达yield
语句时,在清除其部分堆栈并返回之前,它会获取其局部变量的快照并将它们存储在生成器对象中.它还会在其代码(即特定yield
语句)中写下它当前所处的位置.
所以它是发电机悬挂的一种冻结功能.
当next()
随后被调用时,它检索功能的物品入堆栈,重新蓬勃生机.该功能继续从它停止的位置进行计算,而不知道它刚刚在冷库中度过了永恒的事实.
比较以下示例:
def normalFunction(): return if False: pass def yielderFunction(): return if False: yield 12
当我们调用第二个函数时,它的行为与第一个函数的行为非常不同.该yield
声明可能是无法访问的,但如果它的存在的任何地方,它改变了我们正在处理什么用的性质.
>>> yielderFunction()
调用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_code
和gi_frame
字段是冻结状态的存储位置.通过探索它们dir(..)
,我们可以确认我们的上述心理模型是可信的.
像每个答案所暗示的那样,yield
用于创建序列生成器.它用于动态生成一些序列.例如,在网络上逐行读取文件时,可以使用以下yield
函数:
def getNextLines(): while con.isOpen(): yield con.read()
您可以在代码中使用它,如下所示:
for line in getNextLines(): doSomeThing(line)
执行控制转移问题
执行for
yield时,执行控件将从getNextLines()传送到循环.因此,每次调用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"
(我的下面的回答只是从使用Python生成器的角度讲,而不是生成器机制的底层实现,它涉及堆栈和堆操作的一些技巧.)
当在python函数中yield
使用when 而不是a return
时,该函数被转换为一些特殊的函数generator function
.该函数将返回一个generator
类型的对象.该yield
关键字是一个标志,通知蟒蛇编译器将特殊对待这样的功能.一旦从其返回某个值,正常函数将终止.但是在编译器的帮助下,生成器函数可以被认为是可恢复的.也就是说,将恢复执行上下文,并且执行将从上次运行继续.直到你显式调用return,这将引发StopIteration
异常(也是迭代器协议的一部分),或者到达函数的末尾.我发现了很多关于引用的generator
,但这一个从functional programming perspective
最消化的.
(现在我想谈谈背后的基本原理generator
,并iterator
基于我自己的理解.我希望这可以帮助你掌握迭代器和生成器的基本动机.这样的概念也出现在其他语言中,比如C#.)
据我了解,当我们想要处理大量数据时,我们通常首先将数据存储在某处,然后逐个处理.但这种天真的方法是有问题的.如果数据量很大,那么事先将它们作为一个整体存储起来是很昂贵的.因此,不是data
直接存储自身,为什么不metadata
间接存储某种,即the logic how the data is computed
.
有两种方法来包装这样的元数据.
OO方法,我们包装元数据as a class
.这就是所谓的iterator
实现迭代器协议(即__next__()
,和__iter__()
方法)的人.这也是常见的迭代器设计模式.
功能方法,我们包装元数据as a function
.这就是所谓的generator function
.但在引擎盖下,返回的generator object
仍然是IS-A
迭代器,因为它还实现了迭代器协议.
无论哪种方式,都会创建一个迭代器,即一些可以为您提供所需数据的对象.OO方法可能有点复杂.无论如何,使用哪一个取决于你.
总之,该yield
语句将您的函数转换为一个工厂,该工厂生成一个名为a的特殊对象,该对象generator
环绕原始函数的主体.当generator
迭代时,它执行你的函数,直到它到达下一个,yield
然后暂停执行并计算传递给的值yield
.它在每次迭代时重复此过程,直到执行路径退出函数.例如,
def simple_generator(): yield 'one' yield 'two' yield 'three' for i in simple_generator(): print i
只是输出
one two three
电源来自使用带有计算序列的循环的发生器,发生器每次执行循环停止以"产生"下一个计算结果,这样它就可以动态计算列表,其好处是内存保存用于特别大的计算
假设你想创建一个自己的range
函数,产生一个可迭代的数字范围,你可以这样做,
def myRangeNaive(i): n = 0 range = [] while n < i: range.append(n) n = n + 1 return range
并像这样使用它;
for i in myRangeNaive(10): print i
但这是低效的,因为
您创建一个只使用一次的数组(这会浪费内存)
这段代码实际上循环遍历该数组两次!:(
幸运的是,Guido和他的团队足够慷慨地开发发电机,所以我们可以做到这一点;
def myRangeSmart(i): n = 0 while n < i: yield n n = n + 1 return for i in myRangeSmart(10): print i
现在,在每次迭代时,调用的生成器上next()
的函数执行函数,直到它达到'yield'语句,在该语句中它停止并"产生"该值或到达函数的末尾.在这种情况下,在第一次调用时,next()
执行yield语句并生成'n',在下一次调用时它将执行increment语句,跳回'while',计算它,如果为true,它将停止并且再次屈服'n',它将继续这样,直到while条件返回false并且生成器跳转到函数的末尾.
收益率是一个对象
return
函数中的A 将返回单个值.
如果您希望函数返回一组大量值,请使用yield
.
更重要的yield
是,是一个障碍.
就像CUDA语言中的障碍一样,它不会在控制完成之前进行转移.
也就是说,它将从头开始运行代码,直到它命中yield
.然后,它将返回循环的第一个值.
然后,每隔一个调用将再次运行您在函数中写入的循环,返回下一个值,直到没有任何值返回.
许多人使用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
使用三行而不是五行,并且有一个较少的变量需要担心.
这是代码的结果:
正如你所看到的,两个函数都做同样的事情.唯一的区别是return_dates()
给出一个列表并yield_dates()
给出一个生成器.
一个现实生活中的例子就像是逐行读取文件或者只是想制作一个生成器.
它简单解释的一个简单例子: 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
yield
就像一个函数的返回元素.不同之处在于,yield
元素将函数转换为生成器.在某些东西"屈服"之前,生成器的行为就像一个函数.发电机停止,直到下一次调用,并从它开始的完全相同的点继续.您可以通过调用将所有"已产生"值的序列合二为一list(generator())
.
该yield
关键字简单地收集返回结果.想想yield
就好return +=
这是一个yield
基于简单的方法来计算斐波纳契系列,解释如下:
def fib(limit=50): a, b = 0, 1 for i in range(limit): yield b a, b = b, a+b
当你将它输入你的REPL然后尝试调用它时,你会得到一个神秘的结果:
>>> fib()
这是因为yield
您希望创建一个生成器,即一个按需生成值的对象.
那么,你如何生成这些值?这可以通过使用内置函数直接完成,也可以next
通过将其提供给消耗值的构造来间接完成.
使用内置next()
函数,您可以直接调用.next
/ __next__
,强制生成器生成一个值:
>>> g = fib() >>> next(g) 1 >>> next(g) 1 >>> next(g) 2 >>> next(g) 3 >>> next(g) 5
间接地,如果你提供fib
一个for
循环,一个list
初始化器,一个tuple
初始化器或任何其他需要生成/生成值的对象的东西,你将"消耗"生成器,直到它不再生成值(并返回) :
results = [] for i in fib(30): # consumes fib results.append(i) # can also be accomplished with results = list(fib(30)) # consumes fib
同样,使用tuple
初始化程序:
>>> tuple(fib(5)) # consumes fib (1, 1, 2, 3, 5)
生成器与函数的不同之处在于它是惰性的.它通过维护本地状态并允许您随时恢复来实现此目的.
当您第一次fib
通过调用它调用时:
f = fib()
Python编译函数,遇到yield
关键字并简单地返回一个生成器对象.看起来不是很有帮助.
然后,当您请求它直接或间接生成第一个值时,它会执行它找到的所有语句,直到遇到a yield
,然后它会返回您提供给的值yield
并暂停.有一个更好地演示这个的例子,让我们使用一些print
调用(print "text"
在Python 2上用if 替换):
def yielder(value): """ This is an infinite generator. Only use next on it """ while 1: print("I'm going to generate the value for you") print("Then I'll pause for a while") yield value print("Let's go through it again.")
现在,输入REPL:
>>> gen = yielder("Hello, yield!")
你有一个生成器对象现在正在等待命令让它生成一个值.使用next
并查看打印内容:
>>> next(gen) # runs until it finds a yield I'm going to generate the value for you Then I'll pause for a while 'Hello, yield!'
不带引号的结果是印刷的.引用的结果是从中返回的结果yield
.next
现在再打电话:
>>> next(gen) # continues from yield and runs again Let's go through it again. I'm going to generate the value for you Then I'll pause for a while 'Hello, yield!'
发电机记得它暂停yield value
并从那里恢复.打印下一条消息yield
,再次执行搜索暂停的语句(由于while
循环).
另一个TL; DR
列表上的迭代器:next()
返回列表的下一个元素
迭代器生成器:next()
将动态计算下一个元素(执行代码)
您可以看到yield/generator作为一种从外部手动运行控制流的方法(如继续循环一步),通过调用next
,无论流程如何复杂.
注意:生成器不是正常功能.它会记住以前的状态,如局部变量(堆栈).有关详细说明,请参阅其他答案或文章.生成器只能迭代一次.你可以没有yield
,但它不会那么好,所以它可以被认为是'非常好'的语言糖.
收益率与收益率相似.不同之处是:
yield使函数可迭代(在下面的示例中,primes(n = 1)
函数变为可迭代).
它本质上意味着下次调用该函数时,它将从它离开的位置继续(在该行之后yield expression
).
def isprime(n): if n == 1: return False for x in range(2, n): if n % x == 0: return False else: return True def primes(n = 1): while(True): if isprime(n): yield n n += 1 for n in primes(): if n > 100: break print(n)
在上面的例子中,如果isprime(n)
为真,它将返回素数.在下一次迭代中,它将从下一行继续
n += 1
打个比方可能有助于在这里理解这个想法:
想象一下,您创造了一台惊人的机器,每天能够产生成千上万个灯泡。机器会在具有唯一序列号的盒子中生成这些灯泡。您没有足够的空间来同时存储所有这些灯泡(即,由于存储限制,您无法跟上机器的速度),因此您希望调整此机器以根据需要生成灯泡。
Python生成器与此概念没有太大区别。
想象一下,您有一个x
为盒子生成唯一序列号的函数。显然,您可以通过函数生成大量此类条形码。一个更明智,更节省空间的选择是按需生成这些序列号。
机器代码:
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()
给定的生成器来获取下一个序列号。
输出:
How many lightbulbs to generate? 5 [10000, 10001, 10002, 10003, 10004] Produce more? [Y/n]: y How many lightbulbs to generate? 6 [10005, 10006, 10007, 10008, 10009, 10010] Produce more? [Y/n]: y How many lightbulbs to generate? 7 [10011, 10012, 10013, 10014, 10015, 10016, 10017] Produce more? [Y/n]: n
这里的所有答案都很棒; 但只有其中一个(投票最多的一个)与您的代码的工作方式有关.其他人与发电机有关,以及它们如何工作.
所以我不会重复发电机是什么或产量是多少; 我认为这些都是现有的答案.然而,在花了几个小时试图理解你的类似代码之后,我会分解它是如何工作的.
您的代码遍历二叉树结构.我们以这棵树为例:
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__
函数是一个生成器,所以它内部的代码每次迭代都会执行.因此执行将如下所示:
根元素是第一个; 检查它是否已离开子for
节点并迭代它们(让我们称之为it1,因为它是第一个迭代器对象)
它有一个孩子所以for
执行.在for child in self.left
创建一个新的迭代器从self.left
,这是一个节点对象本身(IT2)
与2相同的逻辑,并iterator
创建一个新的(it3)
现在我们到达了树的左端.it3
没有留下的孩子,所以它继续和yield self.value
在下一次调用next(it3)
它时会引发StopIteration
并存在,因为它没有正确的子项(它到达函数的末尾而没有产生任何东西)
it1
并且it2
仍处于活跃状态 - 他们没有筋疲力尽,而且召唤next(it2)
会产生价值而不是提高价值StopIteration
现在我们回到it2
上下文,并next(it2)
在它停止的地方继续调用:在yield child
声明之后.由于它没有剩下的孩子,它继续并产生它self.val
.
这里的问题是每次迭代都会创建遍历树的子迭代器,并保持当前迭代器的状态.一旦到达末尾,它就会遍历堆栈,并以正确的顺序返回值(最小的收益率值).
您的代码示例在不同的技术中执行了类似的操作:它为每个子项填充了一个元素列表,然后在下一次迭代时弹出它并在当前对象上运行函数代码(因此self
).
我希望这对这个传奇话题有所贡献.我花了几个小时来绘制这个过程来理解它.
Python generators
(一种特殊类型iterators
)用于生成一系列值,yield
关键字就像return
生成器函数的关键字一样.
另一个有趣的yield
关键字是保存state
生成器功能.
因此,我们可以在number
每次generator
收益时设置一个不同的值.
这是一个实例:
def getPrimes(number): while True: if isPrime(number): number = yield number # a miracle occurs here number += 1 def printSuccessivePrimes(iterations, base=10): primeGenerator = getPrimes(base) primeGenerator.send(None) for power in range(iterations): print(primeGenerator.send(base ** power))
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)in ----> 1 next(factory) StopIteration:
他们被告知不要再提出更多要求。因此,一旦消耗了生成器,就可以完成它。make_cake
如果您想要更多的蛋糕,则需要再次致电。这就像下一次订购纸杯蛋糕一样。
In [13]: factory = make_cake(3) In [14]: for cake in factory: ...: print(cake) ...: Cake 0 Cake 1 Cake 2
您还可以将for循环与上述生成器一并使用。
再举一个例子:假设您需要一个随机密码。
In [22]: import random In [23]: import string In [24]: def random_password_generator(): ...: while True: ...: yield ''.join([random.choice(string.ascii_letters) for _ in range(8)]) ...: In [25]: rpg = random_password_generator() In [26]: for i in range(3): ...: print(next(rpg)) ...: FXpUBhhH DdUDHoHn dvtebEqG In [27]: next(rpg) Out[27]: 'mJbYRMNo'
这rpg
是一个生成器,可以生成无限数量的随机密码。因此,我们也可以说,当我们不知道序列的长度时,生成器很有用,这与list的元素数量有限不同。
>>> def create_generator(): ... my_list = range(3) ... for i in my_list: ... yield i*i ... >>> my_generator = create_generator() # create a generator >>> print(my_generator) # my_generator is an object!>>> for i in my_generator: ... print(i) 0 1 4
简而言之,您可以看到循环不会停止并且即使在发送对象或变量之后也会继续运行(与return
循环在执行后停止的位置不同).