我正在努力使发电机运转,并学习如何使用它们。我已经看到了很多示例,并得到它们一次生成一个结果,而不是像常规函数那样一次输出它们。但是我看到的所有示例都涉及遍历列表并打印通过函数生成的值。如果您想实际创建列表怎么办?
例如,我看过一个关于偶数的示例,它仅生成偶数并将其打印出来,但是如果我想要这样的偶数列表怎么办?
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
在这种情况下,生成器在什么确切情况下有用?
答案 0 :(得分:5)
这样做会否决使用生成器的目的,因为它会在偶数列表中创建生成器。在这种情况下,生成器在什么情况下有用?
这是基于观点的,但是在某些情况下列表可能无法解决问题(例如,由于硬件限制)。
想象一下,您有一个偶数列表,然后想取前五个数字的和。在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解释器的不同,对象可能会占用大量内存。例如,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小时内将捕获的图像。但是我们可能会对检测旨在窃取某些东西的防盗软件感兴趣。在那种情况下,帧列表将不包含所有图像。但是,生成器可以构造一个优雅的“协议”,在该协议中,我们可以迭代地获取图像,检测到盗贼并发出警报,例如:
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)
因此,我们可以假定该程序将继续运行,并且不需要“更新”偶数列表。在某些使惰性编程透明化的 pure 功能语言中,程序的编写就像创建列表一样,但实际上它通常是适当的生成器。
range(..)
和朋友在Python中,许多类在迭代它们时都不会构造列表,例如range(1000)
对象先 不会构造列表(它在python-2.x中,但不在python-3.x中)。 range(..)
对象只是表示一个范围。 range(..)
对象不是生成器,而是一个可以生成迭代器对象的类,类似于生成器。
除了迭代之外,我们还可以使用range(..)
对象来做各种事情,这可以通过列表来完成,但是不是是一种有效的方式。
例如,如果我们想知道1000000000
是否是range(400, 10000000000, 2)
的元素,那么我们可以编写1000000000 in range(400, 10000000000, 2)
。现在有一种算法可以检查此 而不会生成范围或构建列表:它查看元素是否为int
,是否在{{1 }}对象(大于或等于range(..)
,小于400
),以及是否产生(考虑了这一步),不是都需要遍历它。因此,可以立即完成成员资格检查。
如果我们生成了一个列表,这意味着Python必须枚举每个元素,直到最终找到该元素(或到达列表的末尾)为止。对于10000000000
之类的数字,这很容易花费几分钟,几小时甚至几天的时间。
我们还可以“切片”范围对象,这会产生另一个1000000000
对象,例如:
range(..)
使用算法,我们可以立即将>>> range(123, 456, 7)[1::4]
range(130, 459, 28)
对象切成一个新的range(..)
对象。切片列表需要线性时间。再次(对于庞大的列表)可能会占用大量时间和内存。
答案 1 :(得分:1)
生成器更短,更易读:
在您的示例中,您必须创建一个空列表,使用append
并返回结果列表:
def even(k):
evens_list = []
for i in range(k):
if i % 2 != 0:
evens_list.append(i)
return evens_list
生成器只需要yield
:
def even(k):
for i in range(k):
if i % 2 != 0:
yield i
如果您确实需要列表,则用途几乎相同。代替
event_list = even(100)
行变成
event_list = list(even(100))
答案 2 :(得分:1)
生成器,但通常是惰性语义,具有一些优点:
还有一些缺点:
答案 3 :(得分:-1)
您可以使用./dist/test_build
构造函数轻松高效地将生成器的输出转换为列表:
./test_build