我正在阅读Python食谱,目前正在研究发电机。我发现我很难理解。
由于我来自Java背景,是否有Java等价物?这本书讲的是“制片人/消费者”,但是当我听到我想到线程时。
什么是发电机?为什么要使用它?显然没有引用任何书籍(除非你能直接从书中找到一个体面的,简单的答案)。也许有例子,如果你感到慷慨!
答案 0 :(得分:352)
注意:这篇文章假定使用Python 3.x语法。 †
generator只是一个函数,它返回一个可以调用next
的对象,这样每次调用它都会返回一些值,直到它引发StopIteration
异常,发出信号所有值都已生成。这样的对象称为迭代器。
普通函数使用return
返回单个值,就像在Java中一样。但是,在Python中,有一种替代方法,称为yield
。在函数中的任何位置使用yield
使其成为生成器。请注意以下代码:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
如您所见,myGen(n)
是一个产生n
和n + 1
的函数。每次调用next
都会产生一个值,直到所有值都被生成为止。 for
循环在后台调用next
,因此:
>>> for n in myGen(6):
... print(n)
...
6
7
同样有generator expressions,它提供了一种简洁描述某些常见类型的生成器的方法:
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
请注意,生成器表达式与list comprehensions非常相似:
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
观察生成器生成一次,但其代码不一次全部运行。只有next
的调用实际执行(部分)代码。一旦达到yield
语句,生成器中代码的执行就会停止,然后返回一个值。然后,对next
的下一次调用会导致执行继续在最后yield
之后生成器停留的状态。这是常规函数的根本区别:它们始终在“顶部”开始执行,并在返回值时丢弃它们的状态。
关于这个问题,还有更多的事要说。它是例如可以send
数据返回到生成器(reference)。但是,在你理解发电机的基本概念之前,我建议你不要研究这个问题。
现在您可能会问:为什么要使用发电机?有几个很好的理由:
生成器允许以自然的方式描述无限流。例如,考虑Fibonacci numbers:
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
此代码使用itertools.islice
从无限流中获取有限数量的元素。建议您仔细查看itertools
模块中的功能,因为它们是轻松编写高级生成器的基本工具。
† 关于Python&lt; = 2.6: next
是一个调用给定对象上的方法__next__
的函数。在Python&lt; = 2.6中,使用稍微不同的技术,即o.next()
而不是next(o)
。 Python 2.7有next()
调用.next
所以你不需要在2.7中使用以下内容:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
答案 1 :(得分:44)
生成器实际上是一个在完成之前返回(数据)的函数,但它在此时暂停,您可以在那时恢复该函数。
>>> def myGenerator():
... yield 'These'
... yield 'words'
... yield 'come'
... yield 'one'
... yield 'at'
... yield 'a'
... yield 'time'
>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words
等等。生成器的(或一个)好处是,因为它们一次处理一个数据,所以可以处理大量数据;对于列表,过多的内存要求可能会成为一个问题。与列表一样,生成器是可迭代的,因此它们可以以相同的方式使用:
>>> for word in myGeneratorInstance:
... print word
These
words
come
one
at
a
time
请注意,生成器提供了另一种处理无穷大的方法,例如
>>> from time import gmtime, strftime
>>> def myGen():
... while True:
... yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000
生成器封装了一个无限循环,但这不是问题,因为每次你要求时它都会得到每个答案。
答案 2 :(得分:25)
首先,术语 generator 最初在Python中有些不明确,导致很多混乱。您可能意味着迭代器和迭代(请参阅here)。然后在Python中还有生成器函数(返回生成器对象),生成器对象(它们是迭代器)和生成器表达式(它们是评估到生成器对象)。
根据the glossary entry for generator,似乎官方术语现在生成器是“生成器功能”的缩写。在过去,文档不一致地定义了这些术语,但幸运的是,这已被修复。
准确并避免使用术语“发电机”而没有进一步说明可能仍然是一个好主意。
答案 3 :(得分:22)
生成器可以被认为是创建迭代器的简写。它们的行为类似于Java Iterator。例如:
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g) # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next() # iterator is at the end; calling next again will throw
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
希望这有助于/正在寻找你。
<强>更新强>
正如许多其他答案所示,创建生成器的方法有很多种。您可以使用上面示例中的括号语法,也可以使用yield。另一个有趣的特性是生成器可以是“无限的” - 不停止的迭代器:
>>> def infinite_gen():
... n = 0
... while True:
... yield n
... n = n + 1
...
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
答案 4 :(得分:11)
没有Java等价物。
这是一个人为的例子:
#! /usr/bin/python
def mygen(n):
x = 0
while x < n:
x = x + 1
if x % 3 == 0:
yield x
for a in mygen(100):
print a
生成器中有一个从0到n的循环,如果循环变量是3的倍数,它会产生变量。
在for
循环的每次迭代期间,执行生成器。如果它是第一次执行发生器,它将从头开始,否则它将从上一次产生的时间开始。
答案 5 :(得分:8)
我喜欢在堆栈框架方面向那些在编程语言和计算方面具有良好背景的人描述生成器。
在许多语言中,堆栈顶部是当前堆栈“帧”。堆栈帧包括为函数本地变量分配的空间,包括传递给该函数的参数。
调用函数时,当前执行点(“程序计数器”或等效函数)将被压入堆栈,并创建新的堆栈帧。执行然后转移到被调用函数的开头。
使用常规函数时,函数会在某个时刻返回一个值,并且“弹出”堆栈。函数的堆栈帧被丢弃,执行将在之前的位置恢复。
当一个函数是一个生成器时,它可以使用yield语句返回一个值而不用放弃堆栈帧。保留局部变量的值和函数内的程序计数器。这允许生成器在以后恢复,从yield语句继续执行,并且它可以执行更多代码并返回另一个值。
在Python 2.5之前,所有发电机都做到了。 Python 2.5还增加了将值传递回的能力。在这样做时,传入的值可用作yield语句的结果,该语句暂时从生成器返回控制(和值)。
生成器的关键优势在于保留了函数的“状态”,与常规函数不同,每次丢弃堆栈帧时,都会丢失所有“状态”。第二个优点是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个小优势。
答案 6 :(得分:6)
有助于明确区分函数foo和生成器foo(n):
def foo(n):
yield n
yield n+1
foo是一个功能。 foo(6)是一个生成器对象。
使用生成器对象的典型方法是循环:
for n in foo(6):
print(n)
循环打印
# 6
# 7
将发电机视为可恢复功能。
yield
的行为类似于return
,因为生成的值会被生成器“返回”。然而,与return不同,下次生成器被要求输入一个值时,生成器的函数foo将在它停止的地方恢复 - 在最后一个yield语句之后 - 并继续运行直到它到达另一个yield语句。
在幕后,当您致电bar=foo(6)
时,系统会定义生成器对象栏,以便您拥有next
属性。
你可以自己调用它来检索foo产生的值:
next(bar) # Works in Python 2.6 or Python 3.x
bar.next() # Works in Python 2.5+, but is deprecated. Use next() if possible.
当foo结束时(并且没有更多的值),调用next(bar)
会抛出StopInteration错误。
答案 7 :(得分:6)
我唯一可以添加到Stephan202的答案是建议您看看David Beazley的PyCon '08演示文稿“系统程序员的生成器技巧”,这是关于我如何以及为什么生成器的最佳单一解释在任何地方都见过。这让我从“Python看起来很有趣”到“这就是我一直在寻找的东西”。它位于http://www.dabeaz.com/generators/。
答案 8 :(得分:4)
这篇文章将使用Fibonacci numbers作为工具来构建解释Python generators的实用性。
这篇文章将包含C ++和Python代码。
Fibonacci数被定义为序列:0,1,1,2,3,5,8,13,21,34 .......
或者一般来说:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
这可以非常容易地转移到C ++函数中:
size_t Fib(size_t n)
{
//Fib(0) = 0
if(n == 0)
return 0;
//Fib(1) = 1
if(n == 1)
return 1;
//Fib(N) = Fib(N-2) + Fib(N-1)
return Fib(n-2) + Fib(n-1);
}
但是如果你想打印前六个斐波那契数字,你将用上述函数重新计算很多值。
例如:Fib(3) = Fib(2) + Fib(1)
,但Fib(2)
也会重新计算Fib(1)
。您想要计算的值越高,您的情况就会越差。
因此,人们可能会想通过跟踪main
中的状态来重写上述内容。
// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
int result = pp + p;
pp = p;
p = result;
return result;
}
int main(int argc, char *argv[])
{
size_t pp = 0;
size_t p = 1;
std::cout << "0 " << "1 ";
for(size_t i = 0; i <= 4; ++i)
{
size_t fibI = GetNextFib(pp, p);
std::cout << fibI << " ";
}
return 0;
}
但这非常难看,它使我们在main
中的逻辑变得复杂。最好不要担心main
函数中的状态。
我们可以返回一个vector
的值并使用iterator
来迭代这组值,但是这需要大量的内存来同时获得大量的返回值。
回到我们的旧方法,如果我们除了打印数字之外还想做其他事情会发生什么?我们必须在main
中复制并粘贴整个代码块,并将输出语句更改为我们想要做的任何其他事情。
如果您复制并粘贴代码,那么您应该被拍摄。你不想被枪杀,是吗?
为了解决这些问题,并避免被射击,我们可以使用回调函数重写这段代码。每次遇到新的Fibonacci数时,我们都会调用回调函数。
void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
if(max-- == 0) return;
FoundNewFibCallback(0);
if(max-- == 0) return;
FoundNewFibCallback(1);
size_t pp = 0;
size_t p = 1;
for(;;)
{
if(max-- == 0) return;
int result = pp + p;
pp = p;
p = result;
FoundNewFibCallback(result);
}
}
void foundNewFib(size_t fibI)
{
std::cout << fibI << " ";
}
int main(int argc, char *argv[])
{
GetFibNumbers(6, foundNewFib);
return 0;
}
这显然是一种改进,你在main
中的逻辑并不那么混乱,你可以用Fibonacci数字做任何你想做的事情,只需定义新的回调。
但这仍然不完美。如果你只想获得前两个Fibonacci数字,然后做一些事情,然后再获得更多信息,然后做其他事情,该怎么办?
好吧,我们可以像往常一样继续,我们可以开始再次将状态添加到main
,允许GetFibNumbers从任意点开始。
但这会使我们的代码进一步膨胀,而且对于像打印Fibonacci数字这样的简单任务而言,它看起来已经太大了。
我们可以通过几个线程实现生产者和消费者模型。但这使代码更加复杂化。
相反,我们来谈谈发电机。
Python有一个非常好的语言功能,可以解决像这些称为生成器的问题。
生成器允许您执行一个函数,在任意点停止,然后在您停止的位置再次继续。 每次返回一个值。
请考虑以下使用生成器的代码:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
这给了我们结果:
0 1 1 2 3 5
yield
语句与Python生成器结合使用。它保存函数的状态并返回yeilded值。下次在生成器上调用next()函数时,它将在yield停止的地方继续。
这比回调函数代码更干净。我们有更清晰的代码,更小的代码,更不用说更多功能代码(Python允许任意大整数)。
答案 9 :(得分:2)
我相信迭代器和生成器的第一次出现大约是20年前的Icon编程语言。
你可能会喜欢the Icon overview,这可以让你在不专注于语法的情况下绕过它们(因为Icon是一种你可能不知道的语言,Griswold正在向人们解释他的语言的好处来自其他语言)。
在阅读了几段之后,生成器和迭代器的效用可能会变得更加明显。
答案 10 :(得分:2)
我编写了这段代码,解释了有关生成器的3个关键概念:
def numbers():
for i in range(10):
yield i
gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers
for i in gen: #we iterate over the generator and the values are printed
print(i)
#the generator is now empty
for i in gen: #so this for block does not print anything
print(i)
答案 11 :(得分:1)
macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_list(num_people):
result = []
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
result.append(person)
return result
t1 = datetime.now()
people = people_list(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
输出:
Memory (Before): 50.38 MB
Memory (After) : 1140.41 MB
Took 0:00:01.056423 Seconds
1 million results
列表的函数。50.38 megabytes
,此后的内存是在我创建 1 million records
列表之后,因此您可以在此处看到它上升了近 1140.41 megabytes
并且花费了 {{ 1}}。1,1 seconds
输出:
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_generator(num_people):
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
yield person
t1 = datetime.now()
people = people_generator(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
在我运行这个 Memory (Before): 50.52 MB
Memory (After) : 50.73 MB
Took 0:00:00.000008 Seconds
之后,这是因为生成器实际上还没有做任何事情,它还没有在内存中保存那百万个值,它正在等待我获取下一个。
基本上它是 the memory is almost exactly the same
因为一旦它到达第一个 yield 语句它就会停止。
我认为它是一个更具可读性的生成器,它还为您提供了 didn't take any time
。
同样,你仍然可以在这里使用所有的推导式和这个生成器表达式,这样你就不会在该区域丢失任何东西。所以这些是您使用生成器的几个原因以及一些 big performance boosts not only with execution time but with memory
。