了解Python中的生成器

Fed*_*rer 194 python generator

我正在阅读Python食谱,目前我正在研究发电机.我发现很难理解我的头脑.

因为我来自Java背景,是否有Java等价物?这本书讲的是"制片人/消费者",但是当我听到我想到线程时.

什么是发电机,为什么要使用它?显然没有引用任何书籍(除非你能直接从书中找到一个体面的,简单的答案).也许有例子,如果你感觉很慷慨!

Ste*_*202 368

注意:本文假定使用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
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,myGen(n)是一个产生n和的函数n + 1.每次调用都会next产生一个值,直到产生所有值.for循环调用next后台,因此:

>>> for n in myGen(6):
...     print(n)
... 
6
7
Run Code Online (Sandbox Code Playgroud)

同样,有生成器表达式,它提供了简洁描述某些常见类型的生成器的方法:

>>> 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
Run Code Online (Sandbox Code Playgroud)

请注意,生成器表达式与列表推导非常相似:

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Run Code Online (Sandbox Code Playgroud)

观察生成器对象生成一次,但其代码不会一次全部运行.只调用next实际执行(部分)代码.一旦yield达到语句,生成器中代码的执行就会停止,然后返回一个值.然后,下一次调用next会导致执行继续处于最后一个生成器之后的状态yield.这与常规函数有根本区别:它们总是在"顶部"开始执行,并在返回值时丢弃它们的状态.

关于这个问题还有更多的话要说.例如,可以将send数据返回到发电机(参考)中.但是,在你理解发电机的基本概念之前,我建议你不要研究这个问题.

现在您可能会问:为什么要使用发电机?有几个很好的理由:

  • 使用生成器可以更简洁地描述某些概念.
  • 可以编写一个动态生成值的生成器,而不是创建一个返回值列表的函数.这意味着不需要构造列表,这意味着生成的代码更有效.通过这种方式,人们甚至可以描述数据流,这些数据流可能太大而无法容纳在内存中.
  • 生成器允许以自然的方式描述无限流.考虑例如Fibonacci数:

    >>> 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]
    
    Run Code Online (Sandbox Code Playgroud)

    此代码用于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
Run Code Online (Sandbox Code Playgroud)

  • 你提到可以将'发送'数据发送给生成器.一旦你这样做,你就有了一个'coroutine'.使用协同程序实现像上面提到的Consumer/Producer这样的模式非常简单,因为它们不需要`Lock`s,因此不会死锁.没有抨击线程就很难描述协同程序,因此我只想说协同程序是一种非常优雅的替代线程. (7认同)

Cal*_*ngh 47

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

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words
Run Code Online (Sandbox Code Playgroud)

等等.生成器的(或一个)好处是,因为它们一次处理一个数据,所以可以处理大量数据; 对于列表,过多的内存要求可能会成为一个问题.生成器,就像列表一样,是可迭代的,因此它们可以以相同的方式使用:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time
Run Code Online (Sandbox Code Playgroud)

请注意,例如,生成器提供了另一种处理无穷大的方法

>>> 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   
Run Code Online (Sandbox Code Playgroud)

生成器封装了一个无限循环,但这不是问题,因为每次你要求它时你只得到每个答案.


nik*_*kow 26

首先,术语生成器最初在Python中有些不明确,导致很多混乱.你可能意味着迭代器迭代(见这里).然后在Python中还有生成器函数(返回生成器对象),生成器对象(它们是迭代器)和生成器表达式(它们被计算到生成器对象).

根据发电机的词汇表条目,似乎官方术语现在是发电机是"发电机功能"的缩写.在过去,文档不一致地定义了这些术语,但幸运的是,这已得到修复.

确切地说,在没有进一步说明的情况下避免使用术语"发电机"可能仍然是一个好主意.

  • 嗯,我认为你是对的,至少根据Python 2.6中几行的测试.生成器表达式返回迭代器(也称为"生成器对象"),而不是生成器. (2认同)

ove*_*ink 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
Run Code Online (Sandbox Code Playgroud)

希望这有助于/正是您所寻找的.

更新:

正如许多其他答案所示,创建生成器的方法有很多种.您可以使用上面示例中的括号语法,也可以使用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
...
Run Code Online (Sandbox Code Playgroud)


Wer*_*sey 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
Run Code Online (Sandbox Code Playgroud)

生成器中有一个循环,从0到n,如果循环变量是3的倍数,则生成变量.

for循环的每次迭代期间,执行生成器.如果它是第一次执行发生器,它将从头开始,否则它将从上一次产生的时间开始继续.

  • 最后一段非常重要:生成器函数的状态在每次产生sth时都被"冻结",并在下次调用时继续处于完全相同的状态. (2认同)

Pet*_*sen 8

在堆栈框架方面,我喜欢向那些在编程语言和计算方面具有良好背景的人描述生成器.

在许多语言中,堆栈顶部是当前堆栈"帧".堆栈帧包括为函数本地变量分配的空间,包括传递给该函数的参数.

调用函数时,将当前执行点("程序计数器"或等效函数)压入堆栈,并创建新的堆栈帧.然后执行转移到被调用函数的开头.

使用常规函数,在某些时候函数返回一个值,并且"弹出"堆栈.函数的堆栈帧被丢弃,执行将在之前的位置恢复.

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

在Python 2.5之前,这是所有生成器都做到的.Python 2.5的加入将值传递回的能力到发电机为好.在这样做时,传入的值可用作由yield语句得到的表达式,该语句暂时从生成器返回控制(和值).

生成器的关键优势在于保留了函数的"状态",与常规函数不同,每次丢弃堆栈帧时,都会丢失所有"状态".第二个优点是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个小优势.


unu*_*tbu 6

它有助于明确区分函数foo和生成器foo(n):

def foo(n):
    yield n
    yield n+1
Run Code Online (Sandbox Code Playgroud)

foo是一个功能.foo(6)是一个生成器对象.

使用生成器对象的典型方法是循环:

for n in foo(6):
    print(n)
Run Code Online (Sandbox Code Playgroud)

循环打印

# 6
# 7
Run Code Online (Sandbox Code Playgroud)

将发电机视为可恢复功能.

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.
Run Code Online (Sandbox Code Playgroud)

当foo结束(并且没有更多的生成值)时,调用next(bar)抛出StopInteration错误.


Rob*_*ney 6

我唯一可以添加到Stephan202的答案是建议您看看David Beazley的PyCon '08演示文稿"系统程序员的生成器技巧",这是我见过的生成器的方法和原因的最佳单一解释任何地方.这让我从"Python看起来很有趣"到"这就是我一直在寻找的东西".它位于http://www.dabeaz.com/generators/.


Mil*_*vić 6

性能差异:

macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
Run Code Online (Sandbox Code Playgroud)

情况1

macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
Run Code Online (Sandbox Code Playgroud)

输出:

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))
Run Code Online (Sandbox Code Playgroud)
  • 返回 . 列表的函数1 million results
  • 在底部我打印出内存使用情况和总时间。
  • 基本内存使用量大约50.38 megabytes是在我创建该列表之后的内存使用量1 million records,因此您可以在这里看到它几乎增加了1140.41 megabytes,并且花费了1,1 seconds

案例2

Memory (Before): 50.38 MB
Memory (After) : 1140.41 MB
Took 0:00:01.056423 Seconds
Run Code Online (Sandbox Code Playgroud)

输出:

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))
Run Code Online (Sandbox Code Playgroud)
  • 在我运行这个之后,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.


Nos*_*dna 5

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

您可能会喜欢Icon 概述,它可以让您全神贯注于它们,而不必专注于语法(因为 Icon 是一种您可能不知道的语言,而 Griswold 正在向来自其他语言的人们解释他的语言的好处)。

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


Bri*_*ndy 5

这篇文章将使用斐波那契数作为工具来解释Python生成器的有用性。

这篇文章将同时介绍C ++和Python代码。

斐波那契数定义为以下顺序:0、1、1、2、3、5、8、13、21、34,...。

或一般来说:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Run Code Online (Sandbox Code Playgroud)

这可以非常容易地转移到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);
}
Run Code Online (Sandbox Code Playgroud)

但是,如果要打印前六个斐波那契数字,则将使用上述函数重新计算很多值。

例如: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;
}
Run Code Online (Sandbox Code Playgroud)

但这很丑陋,并且使中的逻辑复杂化main。最好不必担心我们main功能的状态。

我们可以返回一个vector值a ,并使用一个a 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;
}
Run Code Online (Sandbox Code Playgroud)

显然,这是一种改进,您的输入逻辑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()
Run Code Online (Sandbox Code Playgroud)

这给了我们结果:

0 1 1 2 3 5

yield语句与Python生成器结合使用。它保存函数的状态并返回yeilded值。下次您在生成器上调用next()函数时,它将在中断收益率的地方继续。

到目前为止,这比回调函数代码更干净。我们拥有更干净的代码,更小的代码,更不用说更多的功能代码(Python允许任意大的整数)。

资源


Ste*_*ncu 5

我放置了这段代码,它解释了有关生成器的 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)
Run Code Online (Sandbox Code Playgroud)