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

时间:2018-09-22 15:29:01

标签: 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

在这种情况下,生成器在什么确切情况下有用?

4 个答案:

答案 0 :(得分:5)

  

这样做会否决使用生成器的目的,因为它会在偶数列表中创建生成器。在这种情况下,生成器在什么情况下有用?

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

节省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解释器的不同,对象可能会占用大量内存。例如,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)对象先 不会构造列表(它在中,但不在中)。 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)

生成器,但通常是惰性语义,具有一些优点:

  • 您可以创建无限列表
  • 您可以节省大量内存,因为它不会将所有列表都保留在内存中。
  • 通常用于昂贵的IO操作,因此只有真正使用数据时,您才可以有效地检索数据

还有一些缺点:

  • 开销
    • 您需要将生成器函数的变量保留在内存中
    • 还有内存泄漏的风险
  • 每次要重复使用列表中的元素时,都必须重新生成

答案 3 :(得分:-1)

您可以使用./dist/test_build构造函数轻松高效地将生成器的输出转换为列表:

./test_build