在什么情况下你应该在 python 中实际使用生成器?

2024-01-09

我正在尝试了解发电机并了解如何使用它们。我看过很多例子,发现它们一次产生一个结果,而不是像常规函数那样立即输出结果。但我见过的所有示例都涉及遍历列表并打印通过函数生成的值。如果您想实际创建一个列表怎么办?

例如,我见过一个关于偶数的示例,它只生成偶数并将其打印出来,但是如果我想要一个像这样的偶数列表怎么办:

def even(k):
    for i in range(k):
        if (i%2):
           yield k

even_list = []
for i in even(100):
    even_list.append(i)

这是否违背了使用生成器的目的,因为它然后在偶数列表中创建了它。这种方法还节省一些内存/时间吗?

或者下面的方法不使用生成器同样有效。

def even(k):
    evens_list = []
    for i in range(k):
        if (i%2):
           evens_list.append(i)
    return evens_list

在这种情况下,生成器在哪些具体情况下有用?


这是否违背了使用生成器的目的,因为它然后在偶数列表中创建了它。在这种情况下,生成器在哪些具体情况下有用?

这有点基于意见,但在某些情况下,列表可能无法解决问题(例如由于硬件限制)。

节省 CPU 周期(时间)

想象一下,您有一个偶数列表,然后想要计算前五个数字的总和。在Python中我们可以用islice, like:

sumfirst5even = sum(islice(even(100), 5))

如果我们首先生成一个包含 100 个偶数的列表(不知道稍后我们将如何处理该列表),那么我们会花费大量的 CPU 周期来构建这样的列表,这些周期都被浪费了。

通过使用生成器,我们可以将其限制为仅包含我们真正需要的元素。所以我们只会yield前五个要素。该算法将never计算大于 10 的元素。是的,这是否会产生任何(重大)影响值得怀疑。甚至有可能“生成器协议“与生成列表相比,需要更多的 CPU 周期,因此对于小列表来说,没有优势。但现在想象一下我们使用even(100000),那么我们在生成整个列表上花费的“无用的 CPU 周期”量可能会很大。

节省内存

另一个潜在的好处是节省内存,因为我们这样做not需要内存中生成器的所有元素同时存在。

以下面的例子为例:

for x in even(1000):
    print(x)

If even(..)构造一个列表1000元素,那么这意味着所有这些数字需要同时成为内存中的对象。根据 Python 解释器的不同,对象可能会占用大量内存。例如一个int接受CPython,28字节内存。这意味着包含 500 个这样的列表ints 大约需要 14 kB 的内存(列表需要一些额外的内存)。是的,大多数 Python 解释器都维护“享元”模式,以减轻小整数的负担(这些是共享的,所以我们这样做not为每个对象创建一个单独的对象int我们在这个过程中构建),但它仍然可以很容易地加起来。为even(1000000),我们将需要 14 MB 内存。

如果我们使用发电机,则取决于我们如何use生成器,我们可能会节省内存。为什么?因为一旦我们不再需要这个号码123456(自从for循环前进到下一项),对象“占用”的空间可以回收,并给予int有价值的对象12348。因此,这意味着 - 考虑到我们使用生成器的方式允许这样做 - 内存使用量保持不变,而对于列表来说,它是线性缩放的。当然生成器本身也需要做适当的管理:如果在生成器代码中,我们建立了一个集合,那么内存当然也会增加。

在 32 位系统中,这甚至可能会导致一些问题,因为 Python 列表有一个最大长度 https://stackoverflow.com/a/855455/67579。一个列表最多可以包含 536'870'912 个元素。是的,这是一个巨大的数字,但是如果您想要生成给定列表的所有排列怎么办?如果我们将排列存储在一个列表中,那么这意味着对于 32 位系统,一个包含 13 个(或更多元素)的列表,我们将永远无法构造这样的列表。

“在线”节目

在理论计算机科学中,一些研究人员将“在线算法”定义为一种逐渐接收输入的算法,因此无法提前知道整个输入。

一个实际的例子是网络摄像头,它每秒生成一个图像,并将其发送到 Python 网络服务器。那时我们还不知道网络摄像头在 24 小时内捕获的图片会是什么样子。但我们可能对侦查意图偷东西的窃贼感兴趣。在这种情况下,帧列表将不会包含所有图像。然而,生成器可以构建一个优雅的“协议”,我们可以在其中迭代地获取图像、检测窃贼并发出警报,例如:

for frame in from_webcam():
    if contains_burglar(frame):
        send_alarm_email('Maurice Moss')

无限发电机

我们不需要网络摄像头或其他硬件来利用发电机的优雅。生成器可以产生“无限”序列。或者even例如,生成器可能如下所示:

def even():
    i = 0
    while True:
        yield i
        i += 2

这是一个发电机,它将最终产生all偶数。如果我们继续迭代它,最终我们将得到数字 123'456'789'012'345'678 (尽管这可能需要很长时间)。

如果我们想要实现一个程序,例如不断产生回文偶数,则上述内容可能很有用。这可能看起来像:

for i in even():
    if is_palindrome(i):
        print(i)

因此,我们可以假设该程序将继续工作,并且不需要“更新”偶数列表。在一些pure函数式语言使惰性编程变得透明,编写程序就像创建一个列表一样,但实际上它通常是一个生成器。

“丰富”发电机:range(..)和朋友

在 Python 中,很多类在迭代它们时不会构造列表,例如range(1000)对象确实not首先构建一个列表(它在python-2.x /questions/tagged/python-2.x,但不在python-3.x /questions/tagged/python-3.x). The range(..)简单地对象代表一个范围。 Arange(..)对象是not一个生成器,但它是一个可以生成迭代器对象的类,其工作方式类似于生成器。

除了迭代之外,我们还可以用range(..)对象,这对于列表是可能的,但是not以有效的方式。

例如,如果我们想知道是否1000000000是一个元素range(400, 10000000000, 2),那么我们可以写1000000000 in range(400, 10000000000, 2)。现在有一个算法可以检查这一点without生成范围,或构建列表:它查看元素是否是int,在范围内range(..)对象(因此大于或等于400,并且小于10000000000),以及是否产生(考虑到步骤),这确实not需要对其进行迭代。因此,会员资格检查可以立即完成。

如果我们生成了一个列表,这意味着 Python 必须枚举每个元素,直到它最终找到该元素(或到达列表的末尾)。对于像这样的数字1000000000,这很容易需要几分钟、几小时,甚至几天。

我们还可以“切片”范围对象,这会产生另一个range(..)对象,例如:

>>> range(123, 456, 7)[1::4]
range(130, 459, 28)

通过算法,我们可以立即切片range(..)对象变成一个新的range目的。对列表进行切片需要线性时间。这又会(对于巨大的列表)需要大量的时间和内存。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在什么情况下你应该在 python 中实际使用生成器? 的相关文章

随机推荐