Python 切片赋值的内存使用

25

我在Stack Overflow的评论中读到,改变列表时使用切片赋值可以更有效地利用内存。例如:

a[:] = [i + 6 for i in a]

应该比较节省内存

a = [i + 6 for i in a]

因为前者替换现有列表中的元素,而后者创建一个新列表并重新绑定a到该新列表,直到旧的a可以被垃圾回收之前,它仍然存在于内存中。对这两个操作进行速度基准测试,后者稍微更快一些:

$ python -mtimeit -s 'a = [1, 2, 3]' 'a[:] = [i + 6 for i in a]'
1000000 loops, best of 3: 1.53 usec per loop
$ python -mtimeit -s 'a = [1, 2, 3]' 'a = [i + 6 for i in a]'
1000000 loops, best of 3: 1.37 usec per loop

我希望看到这样的结果,因为重新绑定变量应该比替换列表中的元素更快。然而,我无法找到任何官方文档来支持内存使用声明,并且我不知道如何进行基准测试。

表面上看,内存使用声明对我来说是有意义的。然而,经过更深思考,我预计在前一种方法中,解释器将从列表推导式创建一个新列表,然后从该列表复制值到a中,使匿名列表漂浮直至垃圾回收。如果是这种情况,那么前一种方法将使用相同数量的内存,同时速度较慢。

有人能够明确地展示(通过基准测试或官方文档)哪种方法更省内存/哪种方法是首选方法吗?

提前感谢。


2
性能方面可能值得考虑,但我认为你更有可能在实际情况(在较大的程序中)中遇到这种情况,在这种情况下,你从Class1传递一个列表的引用到Class2。在第一种情况下,使用切片赋值来修改Class1的列表将保留Class2的引用。在你提到的第二种情况中,修改Class1的列表意味着Class2将持有对不再有效的列表的引用。 - Brandon
@Brandon:这也是正确的,我可能应该在我的问题中提到这个区别。感谢您的意见。 - Mitch Lindgren
1个回答

49

这行代码

a[:] = [i + 6 for i in a]

不会保存任何内存。如语言文档所述,Python确实先评估右侧:

赋值语句会评估表达式列表(请记住,这可以是单个表达式或逗号分隔的列表,后者产生一个元组),并将单个结果对象分配给每个目标列表,从左到右进行。

在此情况下,单个结果对象将是一个新列表,目标列表中的单个目标将是 a[:]

我们可以通过生成器表达式来替换列表推导式:

a[:] = (i + 6 for i in a)

现在,右侧评估为生成器而不是列表。基准测试显示这仍然比朴素方法慢。

a = [i + 6 for i in a]

那么生成器表达式是否真正节省了任何内存?乍一看可能是这样。 但是深入研究 list_ass_slice() 函数的源代码 就会发现它并没有。该行

v_as_SF = PySequence_Fast(v, "can only assign an iterable");

使用 PySequence_Fast() 将可迭代对象(在本例中为生成器)首先转换为元组,然后将其复制到旧列表中。元组使用与列表相同的内存量,因此在这种情况下使用生成器表达式基本上与使用列表推导式相同。在最后一次复制期间,重用原始列表的项目。

道德似乎是,在这种情况下,最简单的方法在各个方面都是最好的。


6
+1 表示对于过早优化的严厉打击。 - user395760
3
谢谢你详细且有深度的回答!针对上面的评论者,我想补充一点:如果你要处理一个包含五百万个元素的列表,并且需要在复制和不复制之间做出选择,那么这可能并不是过早优化。 :) - Mitch Lindgren
2
@Mitch:如果你有500万个条目,最好使用NumPy数组而不是Python列表。 - Sven Marnach
查看引擎底层机制使得这个答案非常好且明确。+1 - eyquem
1
我认为 a[:] = (x + 6 for x in a) 不会节省任何内存,而且生成器在任何项目分配给 a[:] 之前就被消耗掉了,这让人感到有些安心。否则,您将同时在修改 a 的同时迭代 a,这可能很容易混淆。考虑 a[::-1] = (x + 6 for x in a)。乍一看,它似乎应该颠倒 a 并将6添加到每个项目中。实际上,确实如此。但是,如果生成器在赋值期间懒惰地被消耗,则它将等同于 for i,x in enumerate(a): a[~i] = x + 6,其确切行为很难一眼理解。 - Stef

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