Python中的迭代器是否可以节省内存?

8

我不太明白Python中的迭代器如何具有记忆功能。

>>> l1 = [1, 2, 3, 4, 5, 6]
>>> l2 = [2, 3, 4, 5, 6, 7]
>>> iz = izip(l1, l2)

我们仍需要O(min(l1, l2))的内存,因为我们需要在内存中加载列表l1和l2。
我认为迭代器的主要用途之一是节省内存,但在这里似乎没有用处。
同样,下面的代码对我来说不太清楚:
>>> l1 = ( n for n in [1, 2, 3, 4, 5, 6] )
>>> l2 = ( n for n in [2, 3, 4, 5, 6, 7] )
>>> iz = izip(l1, l2)

我们需要在将列表转换为生成器之前加载它们,对吧?这意味着我们会浪费内存。那么,在这里使用生成器的意义是什么呢?
我只能想到一种有意义的情况:
def build_l1():
    for n in xrange(1, 6):
       yield n

def build_l2:
    for n in xrange(2, 7):
       yield n

l1 = build_l1()
l2 = build_l2()
iz = izip(l1, l2)

没有任何一个数组被加载到内存中。因此,我们处于O(1)的内存使用量。

Python中迭代器函数的内存使用情况是如何工作的?前两种情况似乎使用O(min(l1, l2))的内存。我认为迭代器的主要目的是节省内存,这使得前两种情况似乎毫无用处。


7
如果您迭代列表,它不会节省内存。关键是,通常您可以避免首先创建该列表。此外,只有在可以渐进式地节省内存时,才有意义去节省内存。 - L3viathan
2
你的build_l1build_l2没有太多意义,xrange已经只存储(起始值,终止值,步长)了。 - Kos
2个回答

14

你的例子太过简单。考虑这个:

nums = [1, 2, 3, 4, 5, 6]
nums_it = (n for n in nums)

nums_it是一个生成器,它以不修改nums中所有项目的方式返回所有项目。显然你没有任何优势。但请考虑以下情况:

squares_it = (n ** 2 for n in nums)

并将其与以下内容进行比较:

squares_lst = [n ** 2 for n in nums]

使用squares_it时,仅在请求时才会生成nums的平方。而使用squares_lst时,会一次性生成所有平方并存储在新列表中。

因此,当您执行以下操作时:

for n in squares_it:
    print(n)

这就像你在做:

for n in nums:
    print(n ** 2)

但是当你这样做时:

for n in squares_lst:
    print(n)

就好像你正在做:

squares_lst = []
for n in nums:
    squares_lst.append(n ** 2)
for n in squares_lst:
    print(n)

如果您不需要(或没有)列表nums,那么您可以通过使用以下代码来节省更多空间:

```python _ = next(iter(range(10))) ```
squares_it = (n ** 2 for n in xrange(1, 7))

生成器和迭代器还提供另一个重要的优势(根据情况实际上可能是一个缺点):它们被惰性地评估。

此外,生成器和迭代器可能会产生无限数量的元素。一个例子是itertools.count(),它生成0、1、2、3、...而永远不会停止。


@zero:squares_itO(1) 的内存。或者你也在考虑 nums 的内存吗? - Andrea Corbellini
如果我没错的话,平方这些数字的整个算法将是O(n) - user1008537
@zero:是的。但将其更改为(n ** 2 for n in xrange(1, 7)),然后您就拥有了一个O(1)空间算法。 - Andrea Corbellini
那么,如果不使用显式生成器(如xrangebuild_l1/build_l2),我们就无法实现O(1)吗? - user1008537
1
@zero:在考虑空间复杂度时,你只需考虑输入所需的内存空间。 - chepner
显示剩余3条评论

0
>>> l1 = [1, 2, 3, 4, 5, 6]
>>> l2 = [2, 3, 4, 5, 6, 7]
>>> iz = izip(l1, l2)

我们仍然需要O(min(l1, l2))的内存,因为我们需要将列表l1和l2加载到内存中。

使用zip,您需要为两个原始列表加上压缩后的列表的存储空间。使用izip,您不需要存储压缩后的列表。

如果您必须与实际物理计算机而非某些抽象概念的计算机一起工作,那么大O符号在这里并不特别有用。您的O(n)计算中存在隐藏的常数乘数,这可能会影响代码的实用性,在n趋于无穷之前就已经产生了影响。

>>> l1 = ( n for n in [1, 2, 3, 4, 5, 6] )
>>> l2 = ( n for n in [2, 3, 4, 5, 6, 7] )
>>> iz = izip(l1, l2)

我们需要在将列表转换为生成器之前加载它们,对吧?这意味着我们会浪费内存。那么,在这里使用生成器的意义是什么呢?
在这里使用生成器没有意义。任何时候你看到 n for n in <expr>,而在 for 之前没有更复杂的表达式或者在其后没有 if <expr> 过滤器,那就是代码异味,因为你可以直接使用原始序列。只有当你将输入值转换为其他内容或过滤它们时,生成器才变得有用。

我应该注意到,我正在为编程面试练习,并尝试使用迭代器获得O(1)内存的解决方案。 - user1008537
生成器还可以过滤项目,因此n for n in sequence if predicate(n)可能有用,而不修改任何单个输入值。 - chepner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接