【发布时间】:2010-12-17 21:14:57
【问题描述】:
我目前正在阅读 Python 食谱,目前正在研究生成器。我发现自己的脑袋很难转。
由于我来自 Java 背景,是否有 Java 等价物?这本书讲的是“生产者/消费者”,但是当我听到我想到线程时。
什么是生成器,为什么要使用它?显然,无需引用任何书籍(除非您可以直接从书中找到一个体面、简单的答案)。如果您觉得大方,也许可以举一些例子!
【问题讨论】:
我目前正在阅读 Python 食谱,目前正在研究生成器。我发现自己的脑袋很难转。
由于我来自 Java 背景,是否有 Java 等价物?这本书讲的是“生产者/消费者”,但是当我听到我想到线程时。
什么是生成器,为什么要使用它?显然,无需引用任何书籍(除非您可以直接从书中找到一个体面、简单的答案)。如果您觉得大方,也许可以举一些例子!
【问题讨论】:
注意:本文假定使用 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 在上述示例中,next 是一个函数,它在给定对象上调用方法 __next__。在 Python o.next() 而不是 next(o)。 Python 2.7 有 next() 调用 .next 所以你不需要在 2.7 中使用以下内容:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
【讨论】:
send 数据发送到生成器。一旦你这样做了,你就有了一个“协程”。使用协程实现像提到的消费者/生产者这样的模式非常简单,因为它们不需要Locks,因此不会死锁。不抨击线程很难描述协程,所以我只想说协程是线程的一个非常优雅的替代方案。
生成器实际上是一个在完成之前返回(数据)的函数,但它会在该点暂停,您可以在该点恢复该函数。
>>> 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
生成器封装了一个无限循环,但这不是问题,因为您每次请求时只会得到每个答案。
【讨论】:
首先,generator 一词最初在 Python 中定义不明确,导致很多混乱。您可能指的是 iterators 和 iterables(请参阅 here)。然后在 Python 中还有 生成器函数(返回生成器对象)、生成器对象(它们是迭代器)和 生成器表达式(它们是评估为生成器对象)。
根据the glossary entry for generator 看来,官方术语现在是 generator 是“generator function”的缩写。过去,文档对术语的定义不一致,但幸运的是,这已得到修复。
在没有进一步说明的情况下,准确并避免使用“生成器”一词可能仍然是一个好主意。
【讨论】:
生成器可以被认为是创建迭代器的简写。它们的行为类似于 Java 迭代器。示例:
>>> 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
希望这有助于/是您正在寻找的。p>
更新:
正如许多其他答案所显示的那样,创建生成器有不同的方法。您可以使用上面示例中的括号语法,也可以使用 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
...
【讨论】:
Streams,它与生成器更相似,只是显然你不能不费吹灰之力就获得下一个元素。
没有 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 循环的每次迭代中,都会执行生成器。如果是生成器第一次执行,则从头开始,否则从上一次 yield 继续。
【讨论】:
print "hello" 放在x=x+1 之后,“hello”将被打印 100 次,而 for 循环的主体仍将仅执行 33 次。
我喜欢从堆栈框架的角度向那些在编程语言和计算方面具有良好背景的人描述生成器。
在许多语言中,都有一个堆栈,其顶部是当前堆栈“框架”。堆栈帧包括为函数局部变量分配的空间,包括传递给该函数的参数。
当您调用一个函数时,当前执行点(“程序计数器”或等效项)被压入堆栈,并创建一个新的堆栈帧。然后执行转移到被调用函数的开头。
对于常规函数,有时函数会返回一个值,并且堆栈会“弹出”。函数的堆栈帧被丢弃,并在之前的位置继续执行。
当一个函数是一个生成器时,它可以使用yield语句返回一个值而堆栈帧被丢弃。函数内的局部变量和程序计数器的值被保留。这允许生成器在稍后恢复,从 yield 语句继续执行,它可以执行更多代码并返回另一个值。
在 Python 2.5 之前,这是所有生成器所做的。 Python 2.5 还添加了将值传回 in 到生成器的功能。在这样做的过程中,传入的值可作为由生成器暂时返回控制(和值)的 yield 语句产生的表达式。
生成器的关键优势在于函数的“状态”得以保留,与常规函数不同,每次丢弃堆栈帧时,您都会丢失所有“状态”。第二个优势是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个次要优势。
【讨论】:
有助于明确区分函数 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 错误。
【讨论】:
我可以在 Stephan202 的回答中添加的唯一内容是建议您查看 David Beazley 的 PyCon '08 演示文稿“系统程序员的生成器技巧”,这是对生成器的方式和原因的最佳单一解释,我'在任何地方都见过。这就是让我从“Python 看起来很有趣”到“这就是我一直在寻找的东西”的原因。在http://www.dabeaz.com/generators/。
【讨论】:
这篇文章将使用Fibonacci numbers 作为工具来解释Python generators 的用处。
这篇文章将介绍 C++ 和 Python 代码。
斐波那契数列定义为: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 中,并将输出语句更改为我们想要做的任何其他事情。
如果您复制并粘贴代码,那么您应该被枪杀。你不想中枪吧?
为了解决这些问题,并避免被击中,我们可以使用回调函数重写这段代码。每次遇到一个新的斐波那契数,我们都会调用回调函数。
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 中的逻辑没有那么混乱,而且你可以对斐波那契数做任何你想做的事情,只需定义新的回调。
但这仍然不完美。如果您只想获得前两个斐波那契数,然后做一些事情,然后再获得一些,然后再做其他事情怎么办?
好吧,我们可以像以前一样继续,我们可以再次开始向main 添加状态,允许 GetFibNumbers 从任意点开始。
但这会使我们的代码进一步膨胀,而且对于打印斐波那契数列这样的简单任务来说,它已经显得太大了。
我们可以通过几个线程实现生产者和消费者模型。但这使代码更加复杂。
相反,让我们谈谈生成器。
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 生成器结合使用。它保存函数的状态并返回产生的值。下次你在生成器上调用 next() 函数时,它将从 yield 停止的地方继续。
这比回调函数代码要简洁得多。我们有更简洁的代码,更小的代码,更不用说更多的功能代码(Python 允许任意大的整数)。
【讨论】:
我相信迭代器和生成器的第一次出现是在大约 20 年前的 Icon 编程语言中。
您可能会喜欢 the Icon overview,它可以让您在不专注于语法的情况下环绕它们(因为 Icon 是一种您可能不知道的语言,而 Griswold 正在向来自其他国家的人解释他的语言的好处语言)。
在这里阅读几段之后,生成器和迭代器的实用性可能会变得更加明显。
【讨论】:
我提出了这段代码,它解释了关于生成器的 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)
【讨论】:
列表推导式的经验显示了它们在整个 Python 中的广泛用途。但是,许多用例不需要在内存中创建完整列表。相反,他们只需要一次迭代一个元素。
例如,以下求和代码将在内存中构建一个完整的正方形列表,遍历这些值,并在不再需要引用时删除该列表:
sum([x*x for x in range(10)])
通过使用生成器表达式来节省内存:
sum(x*x for x in range(10))
容器对象的构造函数也有类似的好处:
s = Set(word for line in page for word in line.split())
d = dict( (k, func(k)) for k in keylist)
生成器表达式对于 sum()、min() 和 max() 等函数特别有用,它们可以将可迭代输入减少为单个值:
max(len(line) for line in file if line.strip())
【讨论】:
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 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 之后,这是因为生成器实际上并没有做任何事情,但它没有在内存中保存这百万个值,它正在等待我获取下一个值。
基本上它是didn't take any time,因为它一旦到达第一个yield 语句就会停止。
我认为它的生成器更具可读性,它还为您提供big performance boosts not only with execution time but with memory。
同样,您仍然可以在此处使用所有推导式和此生成器表达式,这样您就不会在该区域丢失任何内容。这就是为什么你会使用生成器的几个原因以及the advantages that come along with that 的一些原因。
【讨论】: