我正在阅读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.

其他回答

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循环的每次迭代中,都会执行生成器。如果这是生成器第一次执行,它将从开始开始,否则它将从上一次生成的时间开始。

生成器实际上是一个函数,它在完成之前返回(数据),但它在该点暂停,您可以在该点恢复该函数。

>>> 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   

生成器封装了一个无限循环,但这不是问题,因为每次您都只能得到每个答案。

我给出了这段代码,解释了关于生成器的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)])

通过使用生成器表达式来节省内存:

求和(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

这篇文章将使用斐波那契数作为工具来解释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允许任意大的整数)。