Python 中“yield”关键字起什么作用?
- 2024-11-15 08:37:00
- admin 原创
- 16
问题描述:
Python 中的关键字提供什么功能yield
?
例如,我试图理解这个代码1:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
这是呼叫者:
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
调用该方法时会发生什么_get_child_candidates
?返回的是列表吗?还是单个元素?它会再次被调用吗?后续调用何时会停止?
这段代码是由 Jochen Schulz (jrschulz) 编写的,他创建了一个很棒的度量空间 Python 库。这是完整源代码的链接:Module mspace。
解决方案 1:
要理解它的作用,你必须理解生成器yield
是什么。在理解生成器之前,你必须理解可迭代对象。
可迭代对象
创建列表后,可以逐个读取其项目。逐个读取其项目称为迭代:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
是一个可迭代对象。当你使用列表推导式时,你会创建一个列表,因此可迭代对象如下:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
您可以使用“ for... in...
”的所有内容都是可迭代的;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 create_generator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
这是一个无用的例子,但是当你知道你的函数将返回一组只需要读取一次的值时,它很方便。
要掌握yield
,你必须明白,当你调用函数时,你在函数体中写的代码不会运行。函数只返回生成器对象,这有点棘手。
for
然后,每次使用生成器时,您的代码都会从中断的地方继续。
现在到了困难的部分:
第一次调用从你的函数创建的生成器对象时for
,它将从头开始运行你的函数中的代码,直到命中yield
,然后它将返回循环的第一个值。然后,每个后续调用将运行你在函数中编写的循环的另一次迭代并返回下一个值。这将持续到生成器被视为空,当函数运行时没有命中时就会发生这种情况yield
。这可能是因为循环已经结束,或者因为你不再满足"if/else"
。
您的代码解释
发电机:
# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):
# Here is the code that will be called each time you use the generator object:
# If there is still a child of the node object on its left
# AND if the distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# If there is still a child of the node object on its right
# AND if the distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# If the function arrives here, the generator will be considered empty
# There are no more than two values: the left and the right children
呼叫者:
# Create an empty list and a list with the current object reference
result, candidates = list(), [self]
# Loop on candidates (they contain only one element at the beginning)
while candidates:
# Get the last candidate and remove it from the list
node = candidates.pop()
# Get the distance between obj and the candidate
distance = node._get_dist(obj)
# If the distance is ok, then you can fill in the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate to the candidate's list
# so the loop will keep running until it has looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
此代码包含几个智能部分:
循环在列表上进行迭代,但列表在循环迭代时会扩展。这是一种遍历所有这些嵌套数据的简洁方法,即使它有点危险,因为您可能会陷入无限循环。在这种情况下,
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
会耗尽生成器的所有值,但while
会不断创建新的生成器对象,这些对象将产生与之前不同的值,因为它不应用于同一节点。该
extend()
方法是一个列表对象方法,它需要一个可迭代对象并将其值添加到列表中。
通常我们传递一个列表给它:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
但是在您的代码中,它得到了一个生成器,这很好,因为:
您不需要读取两次这些值。
您可能有很多孩子,并且您不希望将他们都存储在记忆中。
它之所以有效,是因为 Python 并不关心方法的参数是否是列表。Python 需要可迭代对象,因此它可以处理字符串、列表、元组和生成器!这称为鸭子类型,也是 Python 如此酷的原因之一。但这是另一个故事,另一个问题...
您可以在这里停下来,或者阅读一点来了解生成器的高级用法:
控制发电机消耗
>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
注意:对于 Python 3,使用print(corner_street_atm.__next__())
或print(next(corner_street_atm))
它对于控制资源访问等各种事情很有用。
Itertools,你最好的朋友
该itertools
模块包含用于操作可迭代对象的特殊函数。您是否希望复制生成器?链接两个生成器?使用一行代码将嵌套列表中的值分组?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__()
方法)的过程。可迭代对象是任何可以从中获取迭代器的对象。迭代器是允许您对可迭代对象进行迭代的对象。
这篇文章中有更多关于循环如何for
工作的内容。
解决方案 2:
理解的捷径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()
方法内的代码最终可能看起来非常复杂并且容易出现错误。在这里,生成器提供了一个干净而简单的解决方案。
解决方案 3:
这样想吧:
迭代器只是具有next()
方法的对象的一个花哨术语。因此,yield 函数最终会变成这样:
原始版本:
def some_function():
for i in xrange(4):
yield i
for i in some_function():
print i
这基本上就是 Python 解释器对上述代码所做的事情:
class it:
def __init__(self):
# Start at -1 so that we get 0 when we add 1 below.
self.count = -1
# The __iter__ method will be called once by the 'for' loop.
# The rest of the magic happens on the object returned by this method.
# In this case it is the object itself.
def __iter__(self):
return self
# The next method will be called repeatedly by the 'for' loop
# until it raises StopIteration.
def next(self):
self.count += 1
if self.count < 4:
return self.count
else:
# A StopIteration exception is raised
# to signal that the iterator is done.
# This is caught implicitly by the 'for' loop.
raise StopIteration
def some_func():
return it()
for i in some_func():
print i
为了更深入地了解幕后发生的事情,for
可以将循环重写如下:
iterator = some_func()
try:
while 1:
print iterator.next()
except StopIteration:
pass
这是否更有意义或只是让你更加困惑?:)
我应该指出,为了说明目的,这只是为了简化。:)
解决方案 4:
该yield
关键词简化为两个简单的事实:
如果编译器在函数内部的任何地方
yield
检测到该关键字,则该函数不再通过语句返回。相反,它会立即返回一个惰性“待处理列表”对象,称为生成器return
生成器是可迭代的。什么是可迭代的?它类似于
list
、、、字典视图,或任何其他具有内置协议的对象,用于按特定顺序访问每个set
元素。range
简而言之:最常见的情况是,生成器是一个惰性的、增量待定的列表,语句允许您使用函数符号来编程生成器应增量吐出的yield
列表值。此外,高级用法允许您将生成器用作协同程序(见下文)。
generator = myYieldingFunction(...) # basically a list (but lazy)
x = list(generator) # evaluate every element into a list
generator
v
[x[0], ..., ???]
generator
v
[x[0], x[1], ..., ???]
generator
v
[x[0], x[1], x[2], ..., ???]
StopIteration exception
[x[0], x[1], x[2]] done
基本上,每当遇到该yield
语句时,函数就会暂停并保存其状态,然后根据 Python 迭代器协议发出“‘列表’中的下一个返回值”(对于某些语法结构,如重复调用next()
和捕获StopIteration
异常的 for 循环等)。您可能遇到过带有生成器表达式的生成器;生成器函数更强大,因为您可以将参数传递回暂停的生成器函数,并使用它们来实现协同程序。稍后会详细介绍。
基本示例 ('list')
让我们定义一个makeRange
类似于 Python 的函数range
。调用makeRange(n)
将返回一个生成器:
def makeRange(n):
# return 0,1,2,...,n-1
i = 0
while i < n:
yield i
i += 1
>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>
要强制生成器立即返回其待处理值,您可以将其传递给list()
(就像任何可迭代对象一样):
>>> list(makeRange(5))
[0, 1, 2, 3, 4]
将示例与“仅返回列表”进行比较
可以将上面的例子看作仅仅是创建一个附加并返回的列表:
# return a list # # return a generator
def makeRange(n): # def makeRange(n):
"""return [0,1,2,...,n-1]""" # """return 0,1,2,...,n-1"""
TO_RETURN = [] #
i = 0 # i = 0
while i < n: # while i < n:
TO_RETURN += [i] # yield i
i += 1 # i += 1
return TO_RETURN #
>>> makeRange(5)
[0, 1, 2, 3, 4]
但有一个主要的区别;请参阅最后一节。
如何使用生成器
可迭代是列表推导的最后一部分,并且所有生成器都是可迭代的,因此它们通常像这样使用:
# < ITERABLE >
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]
为了更好地了解生成器,您可以尝试使用itertools
模块(确保在必要时使用chain.from_iterable
而不是chain
)。例如,您甚至可以使用生成器来实现无限长的惰性列表,如itertools.count()
。您可以实现自己的,或者在 while 循环中def enumerate(iterable): zip(count(), iterable)
使用关键字来实现。yield
请注意:生成器实际上可以用于更多用途,例如实现协程、非确定性编程和其他优雅用途。不过,我在这里提出的“惰性列表”观点是您会发现的最常见的用途。
幕后
这就是“Python 迭代协议”的工作方式。也就是说,当你执行 时会发生什么list(makeRange(5))
。这就是我之前描述的“惰性增量列表”。
>>> x=iter(range(5))
>>> next(x) # calls x.__next__(); x.next() is deprecated
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
内置函数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
关键字 eg接受输入的生成器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))
。那些绝对需要克隆生成器的人(例如,正在进行可怕的黑客元编程的人)可以在绝对必要时使用itertools.tee
(在 Python 3 中仍然有效),因为可复制迭代器 Python PEP 标准提案已被推迟。
解决方案 5:
yield
关键字在 Python 中起什么作用?
答案提纲/摘要
带有 的函数
yield
在调用时返回一个Generator。生成器是迭代器,因为它们实现了迭代器协议,所以您可以对它们进行迭代。
生成器还可以发送信息,从概念上来说,这使得它成为一个协同程序。
在 Python 3 中,你可以使用 将一个生成器双向委托
yield from
给另一个生成器。(附录批评了几个答案,包括最上面的答案,并讨论了
return
在生成器中的使用。)
生成器:
yield
仅在函数定义内部合法,并且在函数定义中包含会使其返回一个生成器。yield
生成器的理念源自其他语言(见脚注 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 iterable
True
>>> hasattr(gen, '__next__') # and with .__next__
True # implements the iterator protocol.
生成器类型是迭代器的子类型:
from types import GeneratorType
from collections.abc import Iterator
>>> issubclass(GeneratorType, Iterator)
True
如果有必要,我们可以像这样进行类型检查:
>>> isinstance(gen, GeneratorType)
True
>>> isinstance(gen, Iterator)
True
的一个特点Iterator
是,一旦用尽,就无法重复使用或重置它:
>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]
如果您想再次使用它的功能,则必须再做一次(参见脚注 2):
>>> list(func())
['I am', 'a generator!']
可以通过编程方式产生数据,例如:
def func(an_iterable):
for item in an_iterable:
yield item
上面的简单生成器也等同于下面的 - 从 Python 3.3 开始,您可以使用yield from
:
def func(an_iterable):
yield from an_iterable
但是,yield from
也允许委托给子生成器,这将在下一节关于子协程的合作委托中解释。
协程:
yield
形成一个表达式,允许将数据发送到生成器(参见脚注 3)
这是一个例子,请注意received
变量,它将指向发送到生成器的数据:
def bank_account(deposited, interest_rate):
while True:
calculated_interest = interest_rate * deposited
received = yield calculated_interest
if received:
deposited += received
>>> my_account = bank_account(1000, .05)
首先,我们必须使用内置函数对生成器进行排队next
。它将根据您使用的 Python 版本调用适当的next
或方法:__next__
>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0
现在我们可以将数据发送到生成器了。(发送None
与调用相同next
):
>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5
合作委托给子协程yield from
现在,回想一下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_management
if additional_investment:
under_management += additional_investment
except GeneratorExit:
'''TODO: write function to send unclaimed funds to state'''
raise
finally:
'''TODO: write function to mail tax info to client'''
def investment_account(deposited, manager):
'''very simple model of an investment account that delegates to a manager'''
# must queue up manager:
next(manager) # <- same as manager.send(None)
# This is where we send the initial deposit to the manager:
manager.send(deposited)
try:
yield from manager
except GeneratorExit:
return manager.close() # delegate?
现在我们可以将功能委托给子生成器,并且可以像上面一样由生成器使用它:
my_manager = money_manager(.06)
my_account = investment_account(1000, my_manager)
first_year_return = next(my_account) # -> 60.0
现在模拟向账户中再增加 1,000 加上账户的回报(60.0):
next_year_return = my_account.send(first_year_return + 1000)
next_year_return # 123.6
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 "<stdin>", line 4, in <module>
File "<stdin>", line 6, in money_manager
File "<stdin>", line 2, in <module>
ValueError
结论
我相信我已经涵盖了以下问题的所有方面:
yield
关键字在 Python 中起什么作用?
事实证明这yield
很有用。我相信我可以添加更多详尽的例子。如果您想要更多信息或有建设性的批评,请通过下面的评论告诉我。
附录:
对最佳/被接受答案的批评**
仅使用列表作为示例,对什么是可迭代对象感到困惑。请参阅我上面的参考资料,但总结一下:可迭代对象有一个
__iter__
返回迭代器的方法。迭代器还提供了一种.__next__
方法,该方法由循环隐式调用,for
直到它引发StopIteration
,并且一旦它引发StopIteration
,它将继续这样做。然后它使用生成器表达式来描述生成器是什么。由于生成器表达式只是创建迭代器的一种便捷方式,因此它只会使事情变得混乱,而且我们还没有到达那
yield
部分。在控制生成器耗尽时,他调用
.next
方法(仅在 Python 2 中有效),而他应该使用内置函数。next
调用next(obj)
将是一个合适的间接层,因为他的代码在 Python 3 中不起作用。yield
Itertools?这与它的作用完全无关。没有讨论 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
由于yield 是一个表达式,有些人认为在理解或生成器表达式中使用它很有趣——尽管没有特别好的用例。
CPython 核心开发人员正在讨论是否应取消允许使用该功能。以下是邮件列表中的相关文章:
2017 年 1 月 30 日 19:05,Brett Cannon 写道:
Craig Rodrigues 于 2017 年 1 月 29 日(星期日) 16:39 写道:
我对这两种方法都满意。在我看来,在 Python 3 中保持现状是不好的。
我的意见是这是一个 SyntaxError,因为你没有得到你所期望的语法。
我同意这是我们最终的合理结果,因为任何依赖当前行为的代码都太聪明而无法维护。
为了实现这一目标,我们可能希望:
3.7 版中的 SyntaxWarning 或 DeprecationWarning
2.7.x 中的 Py3k 警告
3.8 中的语法错误
干杯,尼克。
-- Nick Coghlan | ncoghlan@gmail.com | 澳大利亚布里斯班
此外,还有一个未解决的问题(10544)似乎表明这永远不是一个好主意(PyPy,一个用 Python 编写的 Python 实现,已经引发了语法警告。)
底线是,直到 CPython 的开发人员告诉我们其他情况:不要放入yield
生成器表达式或理解。
return
生成器中的语句
在Python 3中:
在生成器函数中,该
return
语句表示生成器已完成并将引发StopIteration
。返回值(如果有)用作构造的参数StopIteration
并成为StopIteration.value
属性。
历史记录,在Python 2中:“在生成器函数中,return
语句不允许包含expression_list
。在这种情况下,return
表示生成器已完成并将导致StopIteration
被引发。”expression_list
基本上是任意数量的用逗号分隔的表达式 - 本质上,在 Python 2 中,你可以用 停止生成器return
,但不能返回值。
脚注
提案中引用了 CLU、Sather 和 Icon 等语言,以将生成器的概念引入 Python。一般的想法是,函数可以维护内部状态并根据用户需求生成中间数据点。这有望在性能上优于其他方法,包括 Python 线程,而 Python 线程在某些系统上甚至不可用。
例如,这意味着
range
对象不是可Iterator
迭代的,因为它们可以重复使用。与列表一样,它们的__iter__
方法返回迭代器对象。yield
最初是作为语句引入的,这意味着它只能出现在代码块中一行的开头。现在yield
创建一个yield表达式。https
://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt提出
此更改是为了允许用户将数据发送到生成器,就像接收数据一样。要发送数据,必须能够将其分配给某个东西,而为此,语句是行不通的。
解决方案 6:
yield
就像return
- 它返回您告诉它的任何内容(作为生成器)。不同之处在于,下次调用生成器时,执行从对yield
语句的最后一次调用开始。与 return 不同,当发生yield时,堆栈框架不会被清理,但是控制权会转移回调用者,因此其状态将在下次调用该函数时恢复。
就您的代码而言,该函数get_child_candidates
就像一个迭代器,因此当您扩展列表时,它会一次将一个元素添加到新列表中。
list.extend
调用迭代器直到它耗尽。就您发布的代码示例而言,返回一个元组并将其附加到列表中会更清楚。
解决方案 7:
还有一件事需要提一下:yield 函数实际上不必终止。我写过这样的代码:
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);
它确实有助于简化一些问题,并使一些事情更容易处理。
解决方案 8:
对于那些喜欢最少的工作示例的人来说,可以思考一下这个交互式 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
解决方案 9:
总结
而不是这样:
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 时,我们称其为生成器函数。迭代器就是它返回的内容。这些术语很有启发性。我们失去了容器的便利性,但获得了根据需要计算的任意长度的系列的功能。
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 执行直到迭代器前进。
解决方案 10:
它返回一个生成器。我对 Python 不是特别熟悉,但我相信它与C# 的迭代器块是同一种东西,如果你熟悉的话。
关键思想是编译器/解释器/无论什么都会做一些小把戏,这样就调用者而言,他们可以继续调用 next(),并且它会继续返回值 -就像生成器方法被暂停一样。现在显然你不能真正“暂停”一个方法,所以编译器会为你构建一个状态机来记住你当前的位置以及局部变量等的样子。这比自己编写迭代器要容易得多。
解决方案 11:
Yield 给你一个生成器。
def get_odd_numbers(i):
return range(1, i, 2)
def yield_odd_numbers(i):
for x in range(1, i, 2):
yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5
如您所见,第一种情况会foo
一次性将整个列表保存在内存中。对于包含 5 个元素的列表来说,这不是什么大问题,但如果您想要一个包含 500 万个元素的列表怎么办?这不仅会占用大量内存,而且在调用该函数时构建也会花费大量时间。
在第二种情况下,bar
只为您提供一个生成器。生成器是可迭代的 - 这意味着您可以在for
循环等中使用它,但每个值只能访问一次。所有值也不会同时存储在内存中;生成器对象“记住”上次调用它时它在循环中的位置 - 这样,如果您使用可迭代来(例如)计数到 500 亿,您不必一次计数到 500 亿并存储要计数的 500 亿个数字。
再次重申,这是一个相当做作的例子,如果你真的想数到 500 亿,你可能会使用 itertools。:)
这是生成器最简单的用例。正如您所说,它可用于编写高效的排列,使用yield将事物通过调用堆栈向上推送,而不是使用某种堆栈变量。生成器还可用于专门的树遍历以及所有其他方式。
解决方案 12:
下面是一个用通俗易懂的语言来举例子,我将给出人类高级概念和 Python 低级概念之间的对应关系。
我想对数字序列进行操作,但我不想为创建该序列而烦恼,我只想专注于我想要执行的操作。因此,我执行以下操作:
我打电话给你,告诉你我想要一个以特定方式计算的数字序列,并让你知道算法是什么。
此步骤对应于def
构造生成器函数,即包含 的函数yield
。
过了一会儿,我告诉你,“好的,准备告诉我数字序列”。
此步骤对应于调用生成器函数,该函数返回一个生成器对象。请注意,你还没有告诉我任何数字;你只需拿起纸和笔。
我问你“告诉我下一个数字”,你告诉我第一个数字;之后,你等着我问你下一个数字。你的工作是记住你刚才在哪里,你已经说过哪些数字,以及下一个数字是什么。我不关心细节。
此步骤对应于调用next(generator)
生成器对象。
(在 Python 2 中,.next
是生成器对象的一个方法;在 Python 3 中,它被命名为.__next__
,但调用它的正确方法是使用内置next()
函数,就像len()
和一样.__len__
)
…重复上一步,直到…
最终,你可能会结束。你不用告诉我一个数字;你只需大喊:“冷静!我完成了!不要再说数字了!”
此步骤对应于生成器对象结束其工作并引发StopIteration
异常。
生成器函数不需要引发异常。当函数结束或发出时,它会自动引发return
。
这就是生成器 (包含 的函数yield
) 的功能;它从第一个 开始执行next()
,每当执行到 时暂停yield
,当被要求输入next()
值时,它会从最后一个点继续。它在设计上与 Python 的迭代器协议完美契合,该协议描述了如何按顺序请求值。
迭代器协议最著名的用户是for
Python 中的命令。因此,每当你执行以下任一操作时:
for item in sequence:
无论sequence
是列表,字符串,字典还是如上所述的生成器对象;结果都是一样的:您从序列中逐一读取项目。
请注意,def
内置包含关键字的函数yield
并不是创建生成器的唯一方法;它只是创建生成器的最简单方法。
有关更准确的信息,请阅读Python 文档中的迭代器类型、yield 语句和生成器。
解决方案 13:
在众多描述如何使用生成器的优秀答案中,有一种答案我认为还没有给出。以下是编程语言理论答案:
yield
Python 中的语句返回一个生成器。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_continuation
return value
其中yield
关键字实际上是真实生成器函数的语法糖,基本上是这样的:
def generatorfun(iterable):
if len(iterable) == 0:
raise StopIteration
else:
return (iterable[0], lambda:generatorfun(iterable[1:]))
请记住,这只是伪代码,Python 中生成器的实际实现更为复杂。但作为理解正在发生的事情的练习,请尝试使用连续传递样式来实现生成器对象,而无需使用关键字yield
。
解决方案 14:
虽然很多答案都说明了为什么要使用yield
来创建生成器,但 的用途更多yield
。创建协程非常容易,它可以在两个代码块之间传递信息。我不会重复任何已经给出的关于使用 来创建生成器的优秀示例yield
。
为了帮助理解 ayield
在以下代码中的作用,您可以用手指跟踪任何包含 的代码的循环yield
。每次您的手指点击 时yield
,您都必须等待输入anext
或 a 。当调用 a 时,您将跟踪代码直到您点击... 右侧的代码被评估并返回给调用者...然后您等待。当再次调用 时,您将执行另一个代码循环。但是,您会注意到在协程中,也可以与 a 一起使用...它会将一个值从调用者发送到收益函数。如果给出了 a,则接收发送的值,并将其吐出左侧...然后跟踪代码一直进行,直到您再次点击 (在最后返回值,就像被调用一样)。send
`nextyield
yieldnext
yieldsend
sendyield
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()
解决方案 15:
还有另一种yield
用途和含义(自 Python 3.3 起):
yield from <expr>
来自PEP 380-委托给子生成器的语法:
提出了一种语法,让生成器将其部分操作委托给另一个生成器。这样可以将包含“yield”的代码段分解出来并放在另一个生成器中。此外,子生成器可以返回一个值,并且该值可供委托生成器使用。
当一个生成器重新产生另一个生成器产生的值时,新的语法也为优化开辟了一些机会。
此外这将引入(自 Python 3.5 起):
async def new_coroutine(data):
...
await blocking_action()
以避免协程与常规生成器混淆(今天yield
两者都使用)。
解决方案 16:
所有答案都很棒,但是对于新手来说有点难。
我认为你已经学会了这个return
说法。
打个比方,return
和yield
是双胞胎。return
表示“返回并停止”,而 'yield' 表示“返回但继续”
尝试使用 获取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]: <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
。
下一步我们可以
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]: <generator object num_list at 0x10327c990>
。
总而言之,用一个比喻来理解:
return
是yield
双胞胎list
是generator
双胞胎
解决方案 17:
从编程角度来看,迭代器是作为thunk实现的。
为了实现迭代器、生成器、通过消息进行并发执行等概念,人们使用发送到闭包对象的消息,该闭包对象有一个调度程序,调度程序会应答“消息” (这个概念来自 Simula,是Smalltalk的核心部分)。
“ next ”是发送给闭包的消息,由“ iter ” 调用创建。
有很多方法可以实现这种计算。我使用了变异,但无需变异也可以进行这种计算,只需返回当前值和下一个yielder(使其具有引用透明性)。Racket 在某些中间语言中使用了一系列初始程序的转换,其中一种重写使yield运算符在某些具有更简单运算符的语言中进行转换。
这里演示了如何重写yield,它使用了R6RS的结构,但语义与Python的相同。这是相同的计算模型,只需要更改语法就可以使用Python的yield重写它。
Welcome to Racket v6.5.0.3. -> (define gen (lambda (l) (define yield (lambda () (if (null? l) 'END (let ((v (car l))) (set! l (cdr l)) v)))) (lambda(m) (case m ('yield (yield)) ('init (lambda (data) (set! l data) 'OK)))))) -> (define stream (gen '(1 2 3))) -> (stream 'yield) 1 -> (stream 'yield) 2 -> (stream 'yield) 3 -> (stream 'yield) 'END -> ((stream 'init) '(a b)) 'OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END ->
解决方案 18:
下面是一些 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)
解决方案 19:
我本来要发布“阅读 Beazley 的《Python:基本参考》第 19 页,快速了解生成器”,但其他人已经发布了很好的描述。
另请注意,yield
在协程中可以用作生成器函数中的双重用法。虽然它与您的代码片段的用法不同,但(yield)
可以用作函数中的表达式。当调用者使用该send()
方法向方法发送值时,协程将执行,直到(yield)
遇到下一个语句。
生成器和协程是设置数据流类型应用程序的一种很酷的方式。我认为了解该yield
语句在函数中的其他用途是值得的。
解决方案 20:
这是一个简单的例子:
def isPrimeNumber(n):
print "isPrimeNumber({}) call".format(n)
if n==1:
return False
for x in range(2,n):
if n % x == 0:
return False
return True
def primes (n=1):
while(True):
print "loop step ---------------- {}".format(n)
if isPrimeNumber(n): yield n
n += 1
for n in primes():
if n> 10:break
print "writing result {}".format(n)
输出:
loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
writing result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
writing result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
writing result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call
我不是 Python 开发人员,但在我看来,它yield
占据了程序流的位置,下一个循环从“yield”位置开始。它似乎在那个位置等待,就在那之前,在外面返回一个值,下次继续工作。
这似乎是一个有趣又不错的能力:D
解决方案 21:
以下是对此的心理想象yield
。
我喜欢将线程想象为拥有一个堆栈(即使它不是以这种方式实现的)。
当一个普通函数被调用时,它会将局部变量放入堆栈,进行一些计算,然后清除堆栈并返回。其局部变量的值永远不会再被看到。
对于yield
函数,当其代码开始运行时(即在调用函数后,返回生成器对象,next()
然后调用其方法),它同样会将其局部变量放入堆栈并计算一段时间。但是,当它遇到语句时yield
,在清除堆栈部分并返回之前,它会对其局部变量进行快照并将其存储在生成器对象中。它还会记下它当前在代码中的位置(即特定yield
语句)。
所以它是生成器挂起的一种冻结函数。
随后调用时next()
,它会检索函数的内容并将其放入堆栈中,然后重新启动它。函数会继续从上次中断的地方进行计算,完全没有意识到它刚刚在冷存储中度过了漫长的等待。
比较以下例子:
def normalFunction():
return
if False:
pass
def yielderFunction():
return
if False:
yield 12
当我们调用第二个函数时,它的行为与第一个函数截然不同。该yield
语句可能无法到达,但如果它出现在任何地方,它就会改变我们所处理内容的性质。
>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>
调用yielderFunction()
不会运行其代码,而是用代码生成一个生成器。(为了便于阅读,用前缀命名这些东西可能是一个好主意yielder
。)
>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
...
'__iter__', #Returns gen itself, to make it work uniformly with containers
... #when given to a for loop. (Containers return an iterator instead.)
'close',
'gi_code',
'gi_frame',
'gi_running',
'next', #The method that runs the function's body.
'send',
'throw']
gi_code
和字段gi_frame
是存储冻结状态的地方。使用 探索它们dir(..)
,我们可以确认上面的思维模型是可信的。
解决方案 22:
想象一下,你创造了一台了不起的机器,它每天能够生产成千上万个灯泡。这台机器将这些灯泡装在盒子里,每个盒子都有一个唯一的序列号。你没有足够的空间同时存储所有这些灯泡,所以你想调整它以按需生产灯泡。
Python 生成器与此概念没有太大区别。假设您有一个名为的函数barcode_generator
,它为盒子生成唯一的序列号。显然,该函数可以返回大量此类条形码,但要受到硬件(RAM)限制。更明智且节省空间的选择是按需生成这些序列号。
机器代码:
def barcode_generator():
serial_number = 10000 # Initial barcode
while True:
yield serial_number
serial_number += 1
barcode = barcode_generator()
while True:
number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
print(barcodes)
# function_to_create_the_next_batch_of_lightbulbs(barcodes)
produce_more = input("Produce more? [Y/n]: ")
if produce_more == "n":
break
注意这next(barcode)
一点。
如您所见,我们有一个独立的“函数”来每次生成下一个唯一的序列号。此函数返回一个生成器!如您所见,我们不会每次需要新序列号时都调用该函数,而是使用next()
给定的生成器来获取下一个序列号。
惰性迭代器
更准确地说,这个生成器是一个惰性迭代器!迭代器是一个帮助我们遍历对象序列的对象。它之所以被称为惰性next
,是因为它不会在需要时才将序列的所有项加载到内存中。上例中的使用是从迭代器中获取下一个项的显式方法。隐式方法是使用 for 循环:
for barcode in barcode_generator():
print(barcode)
这将无限地打印条形码,但不会耗尽内存。
换句话说,生成器看起来像一个函数,但行为却像一个迭代器。
现实世界的应用?
最后,现实世界中的应用?它们通常在处理大序列时很有用。想象一下从磁盘读取一个包含数十亿条记录的大型文件。在处理其内容之前,在内存中读取整个文件可能是不可行的(即,您将耗尽内存)。
解决方案 23:
一个简单的例子来理解它是什么: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
解决方案 24:
就像每个答案所暗示的那样,yield
用于创建序列生成器。它用于动态生成一些序列。例如,在网络上逐行读取文件时,可以按yield
如下方式使用该函数:
def getNextLines():
while con.isOpen():
yield con.read()
您可以在代码中使用它,如下所示:
for line in getNextLines():
doSomeThing(line)
执行控制转移陷阱
当执行yield时,执行控制将从getNextLines()转移到循环for
。因此,每次调用getNextLines()时,执行都会从上次暂停的位置开始。
简而言之,具有以下代码的函数
def simpleYield():
yield "first time"
yield "second time"
yield "third time"
yield "Now some useful value {}".format(12)
for i in simpleYield():
print i
将打印
"first time"
"second time"
"third time"
"Now some useful value 12"
解决方案 25:
(我下面的回答只是从使用Python生成器的角度讲的,而不是生成器机制的底层实现,其中涉及到一些堆栈和堆操作的技巧。)
当在 python 函数中yield
使用 而不是时return
,该函数将变成一个特殊的函数generator function
。该函数将返回一个generator
类型为 的对象。关键字yield
是一个标志,用于通知 python 编译器对此类函数进行特殊处理。普通函数一旦返回某个值就会终止。 但是在编译器的帮助下,生成器函数可以被认为是可恢复的。 也就是说,执行上下文将被恢复,并且执行将从上次运行继续。 直到您明确调用 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 方法可能有点复杂。 无论如何,使用哪一个取决于您。
解决方案 26:
总之,该yield
语句将您的函数转换为一个工厂,该工厂生成一个称为 的特殊对象,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”,在下一次调用时,它将执行增量语句,跳回到“while”,对其进行评估,如果为真,它将停止并再次产生“n”,它将继续这种方式,直到while条件返回false并且生成器跳转到函数末尾。
解决方案 27:
Yield 是一个对象
return
函数中的A将返回单个值。
如果您希望函数返回大量值,请使用yield
。
更重要的是,yield
是一道屏障。
类似于 CUDA 语言中的屏障,它只有在完成时才会转移控制权。
也就是说,它会从头开始运行函数中的代码,直到到达yield
。然后,它会返回循环的第一个值。
然后,每个其他调用都会再次运行您在函数中编写的循环,返回下一个值,直到没有任何值可返回。
解决方案 28:
许多人使用return
而不是yield
,但在某些情况下yield
可能更有效且更容易使用。
这是一个绝对最适合的例子yield
:
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件