我可以帮您翻译成中文。此段内容涉及编程问题,有关生成器的使用。生成器在创建一个偶数列表时是否有意义?在哪些情况下使用生成器是有用的?这有点主观,但在某些情况下,列表可能行不通(例如因为硬件限制)。保存 CPU 循环(时间)想象一下,你有一个偶数列表,然后想要取前五个数字的总和。在 Python 中,我们可以使用 islice 来完成这个操作:
sumfirst5even = sum(islice(even(100), 5))
如果我们首先生成一个包含100个偶数的列表(不知道我们以后会用这个列表做什么),那么我们在构建这样的列表时花费了大量的CPU周期,而这些都是浪费的。通过使用生成器,我们可以将其限制为仅获取我们真正需要的元素。因此,我们只会
yield
前五个元素。该算法将永远不会计算大于10的元素。是的,在这里,这是否会产生任何(显着)影响还存在疑问。甚至可能发生“生成器协议”需要比生成列表更多的CPU周期,因此对于小型列表来说,没有优势。但现在想象一下,我们使用了
even(100000)
,那么我们在生成整个列表上花费的“无用CPU周期”的数量可以显著减少。
节省内存
另一个潜在的好处是节省内存,因为我们不需要同时在内存中保存生成器的所有元素。例如,看下面的例子:
for x in even(1000):
print(x)
如果
even(..)
构造一个包含
1000
个元素的列表,那么所有这些数字都需要同时存在于内存中作为对象。根据Python解释器的不同,对象可能占用相当大的内存空间。例如,在CPython中,一个
int
占用28字节的内存。因此,包含500个这样的
int
的列表可能需要大约14 kB的内存(还需要一些额外的内存用于列表)。是的,大多数Python解释器采用“轻量级”模式来减轻小整数的负担(这些整数是共享的,因此我们在过程中不会为每个构造的
int
创建单独的对象),但仍然可能很容易地累加。对于
even(1000000)
,我们将需要14 MB的内存。
如果我们使用生成器,那么取决于我们如何使用生成器,我们可能会节省内存。为什么?因为一旦我们不再需要数字123456(因为for循环前进到下一个项目),对象“占用”的空间可以被回收,并赋予值为12348的int对象。因此,这意味着 - 假设我们使用生成器的方式允许这样做 - 内存使用量保持恒定,而对于列表来说,则会按比例缩放。当然,生成器本身也需要进行适当的管理:如果在生成器代码中,我们建立了一个集合,那么内存当然也会增加。
在32位系统中,这甚至可能导致一些问题,因为Python列表具有最大长度。列表最多可以包含536'870'912个元素。是的,这是一个巨大的数字,但是如果您例如想要生成给定列表的所有排列,该怎么办?如果我们将排列存储在列表中,那么对于32位系统,13个或更多元素的列表,我们将永远无法构造这样的列表。
“在线”程序
在理论计算机科学中,“在线算法”被一些研究者定义为逐步接收输入的算法,因此事先不知道整个输入。
一个实际的例子可以是一个网络摄像头,每秒钟拍摄一张图像,并将其发送到Python Web服务器。我们此时不知道24小时内摄像头拍摄的图像会是什么样子。但是我们可能有兴趣检测盗贼的行踪。在这种情况下,帧列表不包含所有图像。然而,生成器可以构造一个优雅的“协议”,我们可以迭代地获取图像,检测盗贼并发出警报,例如:
for frame in from_webcam():
if contains_burglar(frame):
send_alarm_email('Maurice Moss')
无限生成器
我们不需要网络摄像头或其他硬件来利用生成器的优越性。生成器可以产生一个“无限”的序列。或者甚至
生成器可能看起来像:
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(..)
和朋友们
在Python中,许多类在迭代时并不会构造列表,例如
range(1000)
对象不会首先构造列表(在
python-2.x中会,但在
python-3.x中不会)。
range(..)
对象仅仅表示一个范围。
range(..)
对象不是一个生成器,但它是一个可以生成迭代器对象的类,其行为类似于生成器。
除了迭代,我们可以使用
range(..)
对象进行所有与列表相同的操作,但是效率不高。
例如,如果我们想知道1000000000
是否为range(400,10000000000,2)
的元素,则可以编写1000000000 in range(400,10000000000,2)
。现在有一个算法来检查这个元素是否属于range(..)
对象(因此大于或等于400
,小于10000000000
),并且是否被产生(考虑步长),这不需要迭代它。因此,成员资格检查可以立即完成。
如果我们生成了一个列表,这意味着Python必须枚举每个元素,直到最终找到该元素(或达到列表的末尾)。对于像1000000000
这样的数字,这可能需要几分钟,几小时,甚至几天。
我们还可以对范围对象进行“切片”,这将产生另一个
range(..)
对象,例如:
>>> range(123, 456, 7)[1::4]
range(130, 459, 28)
通过一个算法,我们可以立即将
range(..)
对象切片成一个新的
range
对象。对列表进行切片需要线性时间。这可能会再次(对于大型列表)花费相当长的时间和内存。
for i in even(100):
是否会一次性生成整个序列 - roganjoshfor i in even(100)
。 - Willem Van Onsem