理解Python中的生成器

我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。

由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。

什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!

155087 次浏览

生成器可以看作是创建迭代器的简写。它们的行为类似于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

希望这有助于/是你正在寻找的。

更新:

正如许多其他答案所示,有不同的方法来创建生成器。你可以像上面的例子一样使用圆括号语法,也可以使用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
...

注意:本文假设Python 3。x语法。匕首;

发电机只是一个函数,它返回一个对象,你可以在该对象上调用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)是一个产生nn + 1的函数。每次调用next都会产生一个值,直到产生所有值为止。for循环在后台调用next,这样:

>>> for n in myGen(6):
...     print(n)
...
6
7

类似地,还有< em > < / em >生成器表达式,它们提供了一种方法来简洁地描述某些常见类型的生成器:

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

注意生成器表达式很像< em >列表理解< / em >:

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

观察生成器对象生成一次,但它的代码是一次运行。只有调用next才能实际执行(部分)代码。一旦到达yield语句,生成器中代码的执行就会停止,并返回一个值。对next的下一次调用会导致执行继续在上次yield之后生成器所处的状态下进行。这是常规函数的一个基本区别:常规函数总是在“顶部”开始执行,并在返回值时丢弃它们的状态。

关于这个问题还有很多要说的。例如,可以将send数据返回到生成器(参考)。但是,我建议您在了解生成器的基本概念之前不要研究这个问题。

现在你可能会问:为什么要使用生成器?有几个很好的理由:

  • 使用生成器可以更简洁地描述某些概念。
  • 与其创建一个返回值列表的函数,不如编写一个生成器来动态生成值。这意味着不需要构造列表,这意味着生成的代码具有更高的内存效率。通过这种方式,我们甚至可以描述那些太大而无法装入内存的数据流。
  • 生成器允许以自然的方式描述无限流。例如,考虑斐波纳契数:

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

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

它有助于明确区分函数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错误。

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

>>> 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中定义不清,导致了很多混乱。你可能指的是迭代器iterable(参见在这里)。然后在Python中还有发生器功能(返回生成器对象),生成器对象(迭代器)和生成器表达式(被求值为生成器对象)。

根据生成器的术语表条目,官方术语现在似乎是发电机是“生成器函数”的缩写。在过去,文档对术语的定义不一致,但幸运的是,这个问题已经得到了解决。

在没有进一步说明的情况下,精确地避免使用术语“生成器”可能仍然是一个好主意。

我相信迭代器和生成器的第一次出现是在Icon编程语言中,大约20年前。

你可能会喜欢图标概述,它可以让你在不关注语法的情况下理解它们(因为Icon是一种你可能不知道的语言,格里斯沃尔德是在向来自其他语言的人解释他的语言的好处)。

在阅读了几段之后,生成器和迭代器的效用可能会变得更加明显。

对于Stephan202的回答,我唯一能补充的是建议您看一看David Beazley的PyCon '08演示文稿“生成器技巧给系统程序员”,这是我所见过的关于如何以及为什么使用生成器的最好的解释。这就是让我从“Python看起来很有趣”变成“这就是我一直在寻找的东西”的原因。它在http://www.dabeaz.com/generators/

这篇文章将使用斐波纳契数作为一个工具来解释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函数中的状态。

我们可以返回值的vector,并使用iterator来遍历该值集,但对于大量的返回值,这需要一次性占用大量内存。

那么回到我们以前的方法,如果我们想做一些除了打印数字之外的事情,会发生什么?我们必须复制并粘贴main中的整个代码块,并将输出语句更改为我们想要做的任何事情。 如果你复制粘贴代码,你就会被枪毙。你不想中枪,对吧?< / p >

为了解决这些问题,并避免被击中,我们可以使用回调函数重写这段代码。每次遇到新的斐波那契数时,我们都会调用回调函数。

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

这给了我们结果:

< p > 0 1 1 2 3. 5 < / p >

yield语句与Python生成器一起使用。它保存函数的状态并返回生成的值。下次在生成器上调用next()函数时,它将继续执行yield停止的地方。

这比回调函数代码要简洁得多。我们有更干净的代码,更小的代码,更不用说更多的功能代码(Python允许任意大的整数)。

Source .

对于那些具有编程语言和计算背景的人,我喜欢从堆栈框架的角度来描述生成器。

在许多语言中,有一个堆栈在其上面是当前堆栈“帧”。堆栈框架包括分配给函数局部变量的空间,包括传递给该函数的参数。

当你调用一个函数时,当前的执行点(“程序计数器”或类似的东西)被压入堆栈,一个新的堆栈帧被创建。然后执行转移到被调用函数的开始。

对于常规函数,在某个时刻函数返回一个值,堆栈就会“弹出”。函数的堆栈帧将被丢弃,并在之前的位置继续执行。

当函数是生成器时,它可以使用yield语句返回被丢弃的堆栈帧的值没有。函数中局部变量和程序计数器的值将被保留。这允许生成器在稍后恢复,从yield语句开始继续执行,并且它可以执行更多代码并返回另一个值。

在Python 2.5之前,所有生成器都这样做。Python 2.5还增加了将值传回给生成器的能力。这样,传入的值可以作为yield语句的表达式使用,yield语句从生成器临时返回了控件(和值)。

生成器的关键优势是函数的“状态”被保留,不像常规函数,每次堆栈帧被丢弃,你就会失去所有的“状态”。第二个优点是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个次要的优点。

使用列表推导式的经验表明,它们在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())

more

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

性能差异:

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
  • 函数,返回1 million results的列表。
  • 在底部,我打印出内存使用情况和总时间。
  • 基本内存使用大约是50.38 megabytes,而这个内存是在我创建了1 million records的列表之后,所以你可以在这里看到它几乎增加了1140.41 megabytes,并占用了1,1 seconds

案例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
  • 在我运行这个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

enter image description here