我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。
由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。
什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!
我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。
由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。
什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!
当前回答
性能差异:
macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
案例1
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
函数,返回一个包含100万个结果的列表。 在底部,我打印出内存使用情况和总时间。 基本内存使用大约是50.38兆字节,在我创建了100万条记录的列表之后,你可以看到它增加了近1140.41兆字节,花了1.1秒。
案例2
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
After I ran this that the memory is almost exactly the same and that's because the generator hasn't actually done anything yet it's not holding those million values in memory it's waiting for me to grab the next one. Basically it didn't take any time because as soon as it gets to the first yield statement it stops. I think that it is generator a little bit more readable and it also gives you big performance boosts not only with execution time but with memory. As well and you can still use all of the comprehensions and this generator expression here so you don't lose anything in that area. So those are a few reasons why you would use generators and also some of the advantages that come along with that.
其他回答
这篇文章将使用斐波那契数作为工具来解释Python生成器的有用性。
这篇文章将同时介绍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中复制并粘贴整个代码块,并将输出语句更改为我们想要做的任何事情。 如果你复制粘贴代码,你就会被枪毙。你不想中枪,对吧?
为了解决这些问题,并避免被击中,我们可以使用回调函数重写这段代码。每次遇到新的斐波那契数时,我们都会调用回调函数。
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中添加state,允许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允许任意大的整数)。
源
生成器实际上是一个函数,它在完成之前返回(数据),但它在该点暂停,您可以在该点恢复该函数。
>>> 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
生成器封装了一个无限循环,但这不是问题,因为每次您都只能得到每个答案。
使用列表推导式的经验表明,它们在Python中具有广泛的实用性。然而,许多用例不需要在内存中创建一个完整的列表。相反,它们每次只需要迭代一个元素。
例如,下面的求和代码将在内存中构建一个完整的方块列表,遍历这些值,当引用不再需要时,删除列表:
Sum ([x*x for x in range(10)])
通过使用生成器表达式来节省内存:
求和(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())
more
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循环的每次迭代中,都会执行生成器。如果这是生成器第一次执行,它将从开始开始,否则它将从上一次生成的时间开始。
注意:本文假设Python 3。x语法。__
生成器只是一个函数,它返回一个对象,接下来可以对其调用,这样对于每次调用它都会返回一些值,直到引发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
同样,还有生成器表达式,它提供了一种方法来简洁地描述某些常见类型的生成器:
>>> 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
注意,生成器表达式很像列表推导式:
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Observe that a generator object is generated once, but its code is not run all at once. Only calls to next actually execute (part of) the code. Execution of the code in a generator stops once a yield statement has been reached, upon which it returns a value. The next call to next then causes execution to continue in the state in which the generator was left after the last yield. This is a fundamental difference with regular functions: those always start execution at the "top" and discard their state upon returning a value.
关于这个问题还有很多要说的。例如,可以将数据发送回生成器(参考)。但是,我建议您在了解生成器的基本概念之前不要研究这个问题。
现在你可能会问:为什么要使用生成器?有几个很好的理由:
Certain concepts can be described much more succinctly using generators. Instead of creating a function which returns a list of values, one can write a generator which generates the values on the fly. This means that no list needs to be constructed, meaning that the resulting code is more memory efficient. In this way one can even describe data streams which would simply be too large to fit in memory. Generators allow for a natural way to describe infinite streams. Consider for example the 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] This code uses itertools.islice to take a finite number of elements from an infinite stream. You are advised to have a good look at the functions in the itertools module, as they are essential tools for writing advanced generators with great ease.
†关于Python <=2.6:在上面的例子中,next是一个函数,它调用给定对象的__next__方法。在Python <=2.6中使用了稍微不同的技术,即o.next()而不是next(o)。Python 2.7有next()调用.next,所以你不需要在2.7中使用下面的方法:
>>> g = (n for n in range(3, 5))
>>> g.next()
3