理解 Python 中的生成器

2024-11-22 08:47:00
admin
原创
5
摘要:问题描述:我目前正在阅读 Python 手册,目前正在研究生成器。我发现很难理解。因为我有 Java 背景,所以有没有 Java 的对应内容?这本书讲的是“生产者/消费者”,但是当我听到这个时,我想到的是线程。什么是生成器?为什么要使用它?显然,不需要引用任何书籍(除非您可以从书中找到一个体面、简单的答案)。...

问题描述:

我目前正在阅读 Python 手册,目前正在研究生成器。我发现很难理解。

因为我有 Java 背景,所以有没有 Java 的对应内容?这本书讲的是“生产者/消费者”,但是当我听到这个时,我想到的是线程。

什么是生成器?为什么要使用它?显然,不需要引用任何书籍(除非您可以从书中找到一个体面、简单的答案)。如果您愿意的话,也许可以举例说明!


解决方案 1:

注意:本文假设使用 Python 3.x 语法。

生成器只是一个函数,它返回一个可以调用的对象next,每次调用它都会返回一些值,直到引发StopIteration异常,表示所有值都已生成。这样的对象称为迭代器

普通函数使用 返回单个值return,就像在 Java 中一样。然而,在 Python 中,还有一种替代方法,称为yieldyield在函数中的任何位置使用都会使其成为生成器。观察以下代码:

>>> 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 + 1nextfor`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]

观察一下,生成器对象只生成一次,但其代码不会一次性全部运行。只有调用才能next实际执行(部分)代码。一旦yield到达语句,生成器中的代码执行就会停止,并返回一个值。下一次调用nextthen 会导致执行继续在生成器上次离开后的状态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方法的函数。在 Python <=2.6 中,使用略有不同的技术,即代替。Python 2.7 有调用,因此您不需要在 2.7 中使用以下内容:__next__`o.next()next(o)next()`.next

>>> g = (n for n in range(3, 5))
>>> g.next()
3

解决方案 2:

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

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

首先, Python 中“生成器”一词最初定义不明确,导致了很多混淆。您可能指的是迭代器可迭代对象(请参阅此处)。然后,Python 中还有生成器函数(返回生成器对象)、生成器对象(迭代器)和生成器表达式(求值为生成器对象)。

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

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

解决方案 4:

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

解决方案 5:

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生成器都会被执行。如果这是生成器第一次执行,它会从头开始,否则它会从上次执行的位置继续执行。

解决方案 6:

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

在许多语言中,都有一个堆栈,其顶部是当前堆栈“框架”。堆栈框架包括为函数本地变量(包括传递给该函数的参数)分配的空间。

调用函数时,当前执行点(“程序计数器”或等效物)被推送到堆栈上,并创建一个新的堆栈框架。然后执行转移到被调用函数的开头。

对于常规函数,函数在某个时刻会返回一个值,然后堆栈会被“弹出”。函数的堆栈框架会被丢弃,并在之前的位置恢复执行。

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

在 Python 2.5 之前,生成器只能这样做。Python 2.5 还添加了将值传回生成器的功能这样,传入的值可用作由暂时从生成器返回控制权(和值)的 Yield 语句产生的表达式。

生成器的主要优势在于函数的“状态”得以保留,而常规函数则不同,每次丢弃堆栈帧时,您都会丢失所有“状态”。第二个优势是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个次要的优势。

解决方案 7:

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

解决方案 8:

我对 Stephan202 的回答唯一能补充的是,我建议你看一下 David Beazley 的 PyCon '08 演讲“系统程序员的生成器技巧”,这是我见过的关于生成器的原理和原因的最好的解释。正是它让我从“Python 看起来很有趣”变成了“这就是我一直在寻找的东西”。它的网址是http://www.dabeaz.com/generators/

解决方案 9:

这篇文章将使用斐波那契数作为工具来解释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并将输出语句更改为我们想要执行的其他操作。如果你复制并粘贴代码,那么你应该被枪杀。你不想被枪杀,是吗?

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

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() 函数时,它将从产生的位置继续执行。

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

来源

解决方案 10:

性能差异:

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

解决方案 11:

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

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

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

解决方案 12:

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

解决方案 13:

列表推导的经验表明,它在整个 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())

更多的

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   609  
  在现代项目管理中,资源的有效利用是确保项目成功的关键因素之一。随着技术的不断进步,越来越多的工具和软件被开发出来,以帮助项目经理和团队更高效地管理资源。本文将介绍10款工具,这些工具可以帮助项目团队提升资源利用效率,从而实现项目目标。禅道项目管理软件禅道项目管理软件是一款开源的项目管理工具,广泛应用于软件开发和其他行业...
项目管理系统   3  
  在项目管理领域,软件工具的不断升级和创新是推动效率和协作的关键。2024年,众多项目管理软件将迎来一系列令人期待的升级功能,这些新特性不仅将提升团队的工作效率,还将增强用户体验和数据分析能力。本文将详细介绍10款项目管理软件的最新升级功能,帮助项目经理和团队成员更好地规划和执行项目。禅道项目管理软件禅道项目管理软件一直...
开源项目管理工具   2  
  信创国产系统的10个关键厂商及其技术生态随着全球信息技术格局的不断演变,信创(信息技术应用创新)产业作为国产化替代的重要阶段,正逐步成为推动我国信息技术自主可控、安全可靠的核心力量。信创产业不仅关乎国家信息安全,也是数字经济高质量发展的关键支撑。本文将深入探讨信创国产系统中的10个关键厂商及其技术生态,分析它们在信创浪...
项目管理流程   0  
  在探讨项目管理的广阔领域中,成功并非偶然,而是精心策划、高效执行与持续优化的结果。项目管理的成功之道,可以从明确的目标设定与规划、高效的团队协作与沟通、以及灵活的风险管理与适应变化这三个核心方面进行深入解析。每个方面都是项目成功的基石,它们相互交织,共同支撑起项目的顺利推进与最终成就。明确的目标设定与规划项目管理的首要...
建筑工程项目管理规范   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用