我有一个非常大的列表。假设我这样做(是的,我知道这段代码非常不符合Pythonic,但为了举例子……):
n = (2**32)**2
for i in xrange(10**7)
li[i] = n
运行良好。然而:
for i in xrange(10**7)
li[i] = i**2
占用了显著更多的内存。我不明白为什么会这样——存储大数需要更多的位,在Java中,第二个选项确实更节省内存...
有人能解释一下这是为什么吗?
我有一个非常大的列表。假设我这样做(是的,我知道这段代码非常不符合Pythonic,但为了举例子……):
n = (2**32)**2
for i in xrange(10**7)
li[i] = n
运行良好。然而:
for i in xrange(10**7)
li[i] = i**2
占用了显著更多的内存。我不明白为什么会这样——存储大数需要更多的位,在Java中,第二个选项确实更节省内存...
有人能解释一下这是为什么吗?
Java会对一些值类型(包括整数)进行特殊处理,使它们按值存储(而不像其他所有东西一样通过对象引用存储)。Python不会对这样的类型进行特殊处理,因此将n分配给列表(或其他普通Python容器)中的多个条目时,不必进行复制。
编辑:请注意,引用始终是指 对象 ,而不是“变量”——在Python(或Java)中没有“变量的引用”。例如:
>>> n = 23
>>> a = [n,n]
>>> print id(n), id(a[0]), id(a[1])
8402048 8402048 8402048
>>> n = 45
>>> print id(n), id(a[0]), id(a[1])
8401784 8402048 8402048
从第一个打印输出结果可以看出,列表 a
中的两个条目引用了与 n
引用相同的对象--但是当 n
被重新赋值后,它 现在引用了一个不同的对象,而 a
中的两个条目仍然引用以前的那个对象。
array.array
(来自 Python 标准库模块array)与列表非常不同:它保留了同一类型的紧凑拷贝,每个项目需要尽可能少的位数来存储该类型的值的拷贝。所有正常容器都会保留引用(在 C 编写的 Python 运行时内部实现为指向 PyObject 结构的指针:每个指针在 32 位构建上占用 4 字节,每个 PyObject 至少占用 16 或更多字节 [包括指向类型的指针,引用计数,实际值和 malloc 舍入]),而数组则不会保留引用(因此它们不能是异构的,不能有除少数基本类型之外的项目等)。array.array('h')
处理需要约 2000 字节的数据,而作为一个列表需要约 20,000 个字节。但如果所有项目都是相同的数字,则数组仍需要 2000 个字节的数据,而列表只需要大约 20 个字节 [[在所有这些情况下,您都必须再添加大约 16 或 32 字节以用于容器对象本身,除了数据的内存]]。arr
实际上并不是数组——如果是数组,它就不能存储(2**32)*2(数组中最大的整数值为 32 位),并且问题中报告的内存行为实际上不会被观察到。因此,问题实际上可能是关于列表而不是数组。
编辑:@ooboo 的评论提出了许多合理的后续问题,而不是试图在评论中压缩详细的解释,我将其搬到这里。CPython 将引用存储为指向 PyObject 的指针(使用 Java 和 C# 编写的 Jython 和 IronPython 使用这些语言的隐式引用;使用 Python 编写的 PyPy 具有非常灵活的后端和可以使用许多不同的策略)。太奇怪了–毕竟,整数的引用是如何存储的?id(variable) 给出一个整数,该引用本身就是一个整数,使用整数是否更便宜?
id(v)
(仅适用于 CPython)给出指针的数字值(只是一种方便的方法来唯一地标识对象)。列表可以是异构的(某些项目可能是整数,其他对象是不同类型的),因此将某些项目存储为 PyObject 指针而将其他项目存储为不同的类型并不明智(每个对象还需要类型指示在 CPython 中,引用计数至少是这样)。- array.array
是同质且受限制的,因此它确实存储l = []
x = 2
for i in xrange(1000000):
l.append(x*2)
通常不会使用比更多的内存
l = []
x = 2
for i in xrange(1000000):
l.append(x)
然而,在这种情况下
l = []
x = 2
for i in xrange(1000000):
l.append(i)
arr[i]
都会获取对象的引用,无论它是n
还是i * 2
的结果对象。n
已经被定义,所以只需获取引用即可。但在第二个例子中,必须评估i * 2
,如果需要为这个新的结果对象分配空间,则GC将分配空间,然后使用其引用。