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

标签 python iterator generator

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

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

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

在这种情况下,生成器在哪些确切情况下有用?

最佳答案

Does this defeat the purpose of using a generator as it then creates this in an even list. In this case in what exact cases are generators useful?



这有点基于意见,但在某些情况下,列表可能不起作用(例如,由于硬件限制)。

节省 CPU 周期(时间)

想象一下,您有一个偶数列表,然后想要取前五个数字的总和。在 Python 中,我们可以使用 islice 来做到这一点,例如:
sumfirst5even = sum(islice(even(100), 5))

如果我们首先生成一个包含 100 个偶数的列表(不知道我们以后会用这个列表做什么),那么我们已经在构建这样的列表中花费了大量 CPU 周期,这些都被浪费了。

通过使用生成器,我们可以将其限制为我们真正需要的元素。所以我们只会对前五个元素进行 yield。该算法永远不会计算大于 10 的元素。是的,这是否会产生任何(显着)影响是可疑的。甚至有可能“生成器协议(protocol)”与生成列表相比需要更多的 CPU 周期,因此对于小列表,没有优势。但是现在想象一下,我们使用了 even(100000) ,那么我们用于生成整个列表的“无用 CPU 周期”的数量可能会很大。

节省内存

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

以下面的例子为例:
for x in even(1000):
    print(x)

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

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

在 32 位系统中,这甚至会导致一些问题,因为 Python 列表有一个 maximum length 。一个列表最多可以包含 536'870'912 个元素。是的,这是一个巨大的数字,但是如果您例如想要生成给定列表的所有排列呢?如果我们将排列存储在一个列表中,那么这意味着对于 32 位系统,一个包含 13 个(或更多元素)的列表,我们将永远无法构建这样一个列表。

“在线”程序

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

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

这是一个最终会生成所有偶数的生成器。如果我们继续迭代它,最终我们将得到数字 123'456'789'012'345'678(尽管可能需要很长时间)。

如果我们想实现一个程序,例如不断产生回文的偶数,则上述内容可能很有用。这可能看起来像:
for i in even():
    if is_palindrome(i):
        print(i)

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

“丰富”的生成器:range(..) 和 friend

在 Python 中,很多类在迭代时不会构造列表,例如 range(1000) 对象不会首先构造列表(它在 中构造,但不在 中构造)。 range(..) 对象只是表示一个范围。 range(..) 对象不是生成器,而是可以生成迭代器对象的类,其工作方式类似于生成器。

除了迭代之外,我们还可以用 range(..) 对象做各种事情,这对列表来说是可能的,但不是一种有效的方式。

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

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

我们还可以“切片” range 对象,这会产生另一个 range(..) 对象,例如:
>>> range(123, 456, 7)[1::4]
range(130, 459, 28)

使用算法,我们可以立即将 range(..) 对象切成新的 range 对象。切片列表需要线性时间。这可能再次(对于巨大的列表)需要大量的时间和内存。

关于python - 在什么情况下你应该在 python 中实际使用生成器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52458135/

相关文章:

c++ - 如何在删除 en 元素(双 for 循环)后正确指向 std::list?

python - 类生成器定义

Python Django MySQLdb 设置问题::setup.py dosen't build due to incorrect location of mysql

python - 如何将间隔添加到分组结果中

python - 难以在 2 维或更多维中列出 0 的列表

python - Pandas 时间序列与缺失数据/​​记录的比较

c++ - 为什么 std::distance 不适用于 const 和非 const 迭代器的混合?

ruby - ruby 中的 .each 迭代器是否保证每次都对相同的元素给出相同的顺序?

java - 有 Java 的 URL 有效生成器吗?

python - 生成器表达式因最大递归深度而失败