Python因其灵活性和动态性而广受赞誉,但同时也因其内存占用较大而备受诟病。为了实现这种灵活性和动态性,Python在内存方面做出了一些牺牲。在这里,我们不会深入解释Python的机制,因为这过于复杂。相反,我们将简要展示几个关于内存消耗的示例,比如:一个整型数值占用多少内存?列表又是如何占用内存的?为什么一个整型数值需要28个字节的内存?通过了解这些基本示例,我们可以为今后的程序开发和内存优化提供有益的建议。
如何测量变量的内存使用情况?
最简单的方法当然是使用Python内置方法。也就是说,在sys模块中,有一个名为getsizeof()的函数。当我们想要检查变量的大小时,使用它非常方便。
import sys
x = [1, 2, 3, 4, 5]
print("Size of list:", sys.getsizeof(x), "bytes")
for item in x:
print("Size of list item:", sys.getsizeof(item), "bytes")
上述代码中的列表x的大小是120,而每个项的整数值是28个字节。为啥加起来和与列表大小不一致?
为什么列表的大小不等于其所有项大小的总和?
getsizeof()函数只测量对象本身,而不是它所引用或包含的对象。因此,104个字节只表示列表的“容器”大小。换句话说,它不包括其中的项。容器还包括指针,这些指针是维护对象列表的所有“开销”。
这120 个字节甚至不包括列表中的数值元素。列表的总大小应该是120 + 28*5 = 260字节。换句话说,当我们使用sys.getsizeof()函数来获取对象的内存大小时,如果对象间接引用了其他对象,它不会给出完整的大小。
为什么在Python中一个整数需要28个字节?
我们看下面的整数示例:
import sys
n = 1
size = sys.getsizeof(n)
print(size)
一个简单的整型占用28个字节包括以下组件/字段:
Reference count (8字节)
Type pointer (8字节)
Size (8字节)
Integer value(4字节)
以上所有“组件/字段对于任何Python对象都存在的内存开销,并导致一个简单整数占用28字节。
Reference Count
“引用计数”(“Reference Count”)是Python用于垃圾回收的。也就是说,每当对象被某个东西引用时,它的引用计数就会加1。当引用计数变为0时,Python的垃圾回收过程会释放内存。这意味着当前系统进程无法使用该对象,因为没有对它的引用。
import sys
a = [1, 2, 3]
b = a
c = a
print('sys.getrefcount(a):',sys.getrefcount(a))
del b
print('sys.getrefcount(a):',sys.getrefcount(a))
del c
print('sys.getrefcount(a):',sys.getrefcount(a))
del a
在上述代码中,我们创建了一个列表并将其赋给变量a。然后,我们定义了两个其他变量b和c来引用它。通过sys.getrefcount()函数检查变量a的引用计数,应该是3。由于该函数在尝试访问引用计数时会创建一个临时引用,因此输出的引用计数会变成4。
接着,我们删除了变量b,变量a的引用计数减少了1。再删除变量c后,变量a的引用计数再次减少了1。最后,我们删除了变量a。实际上,此时它的引用计数应该是0。但由于我们的程序已经删除了对它的所有引用,因此无法再访问它的引用。变量a的引用计数为0,垃圾回收机制会意识到可以释放变量a分配的内存。
Type Pointer
与大多数使用静态类型的编程语言(例如C/C++和Java)不同,Python以其动态类型而闻名。基本上,Python允许对象在运行时灵活地更改其类型。请看下面的代码。
my_var = 1
print(f"my_var is an integer: {my_var}, type: {type(my_var)}")
my_var = "Hello, world!"
print(f"my_var is now a string: '{my_var}', type: {type(my_var)}")
my_var = [1, 2, 3, 4, 5]
print(f"my_var is now a list: {my_var}, type: {type(my_var)}")
my_var = {"key": "value"}
print(f"my_var is now a dictionary: {my_var}, type: {type(my_var)}")
def my_function():
return "I am a function"
my_var = my_function
print(f"my_var is now a function: {my_var}, type: {type(my_var)}")
在上面的代码中,我们多次重新定义了变量my_var,并且每次类型都不同。在Python中,这完全没问题。然而,在大多数其他编程语言中,比如Java,我们通常必须用固定类型定义一个变量。
因此,为了拥抱这种灵活性,Python必须使用另外8个字节来存储这个“类型指针”,以确保对象具有可以在运行时更改的动态类型。换句话说,这个“类型指针”可以指向其他类型。
Size Fields
同样,这也是因为Python的动态特性。Python中许多类型变量的精度和长度都是动态的。因此,Python需要这个“开销”来存储这些信息。
这个“大小字段”将以整数形式表示精度。由于Python中的任意精度,变量的“大小”可能需要适应该变量的增长,以支持更大的值或长度。
Integer Value
这28个字节必须包括数字本身。默认情况下,Python为整数值分配了4个字节。在64位系统中,我相信大多数人都在使用,这意味着我们可以拥有的数字介于-2³⁰+1和2³⁰-1之间。请看下面的示例。
n = 2**30-1
size = sys.getsizeof(n)
print(f"Size of the integer {n}: {size} bytes",type(n))
n = 2**30
size = sys.getsizeof(n)
print(f"Size of the integer {n}: {size} bytes",type(n))
在上面的示例中,当我们尝试使用数字 2³⁰ 时,对象的总大小变为了 32 字节。这是因为 4 字节不足以存储这个整数值,因此 Python 给它另外分配了 4 字节。
以上示例简单示例展示了Python内存机制,解释了为何简单的整型需要28字节的内存。了解了基本对象的内存结构,说明了为实现Python的灵活性和动态特性而引入的复杂对象内存结构,需要牺牲内存。