我正在尝试了解发电机并了解如何使用它们。我看过很多例子,并且得到它们一次产生一个结果,而不是像在常规函数中那样一次输出它们。但是我看到的所有示例都涉及遍历列表并打印通过该函数生成的值。如果您想实际创建一个列表怎么办?
例如,我看过一个关于偶数的例子,它只生成偶数并将它们打印出来,但是如果我想要一个像这样的偶数列表怎么办:
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
循环前进到下一项),对象“占用”的空间可以被回收,并分配给值为 int
的 12348
对象。所以这意味着——考虑到我们使用生成器的方式允许这一点——内存使用量保持不变,而对于列表,它是线性缩放的。当然生成器本身也需要做适当的管理:如果在生成器代码中,我们建立了一个集合,那么内存当然也会增加。在 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)
对象不会首先构造列表(它在 python-2.x 中构造,但不在 python-3.x 中构造)。 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/