概述

在 Python 中,一切皆对象,变量存储的是对象的引用。因此研究 Python 的内存模型就是研究 Python 中对象的引用机制的问题。

在介绍 Python 的内存模型之前,先介绍一下一些方便我们做测试的函数和运算

id(object):返回对象的内存地址
==:判断等号两端的值是否相同
is:判断两端的内存地址是否相同

内存模型

先运行下面的代码

x = 1
print(id(x))
x = 2
print(id(x))

可以发现两次 print(x) 的值其实不一样。这说明了 python 中,不是先给 x 开辟内存空间,然后再将值放进去,而是将要进行赋值的值这个对象的地址给 x

运行代码

a = 1
b = a
print(id(a))
print(id(b))

可以发现两次 print 的结果是一样的,因此这个 = 应该是传递了引用

Python 的赋值语句不复制对象,而是创建目标和对象的绑定关系。

python 中还有可变对象和不可变对象的区别:

a = 1
l1 = [1, 2]
b = a
l2 = l1
a = 2
l1[0] = 2

上面代码执行完后,b=2,l2=[2,2],即 a 的改变没有影响到 b,而 l1 的改变影响到了 l2。Python 中 int, float 这些属于不可变的类型,而 list, tuple 这些属于可变的类型。

我的理解是 Python 中的变量类似于 c 中的常指针,他不能修改他指向的地址中的值,但是它可以指向不同的地址
因此从我这个理解出发的话,列表之所以可变,是因为列表中的每个元素其实是一个指针,因此使用 l[0] 时,我们修改了该指针指向的值,因此其他引用了该列表的元素也会做出修改

有时候,对于列表这样的可变类型,我们不希望同步的修改数据。对于自身可变,或包含可变项的集合,有时要生成副本用于改变操作,而不必改变原始对象 [^1]。此时,可以使用拷贝函数,浅层拷贝函数 copy 只复制一层,而深层拷贝函数 deepcopy 则是递归的复制

垃圾回收

每个对象在存在一个引用计数,当引用计数为 0 时会进行垃圾回收。

引用计数增加的情况

  • 一个对象被分配给一个新的名字(例如:a=[1,2]
  • 将其放入一个容器中(例如:c.append(a)

引用计数减少的情况

  • 使用 del 语句对对象别名显式的销毁
  • 对象所在的容器被销毁或从容器中删除对象
  • 引用超出作用域或被重新赋值

引用计数能解决大多数垃圾回收的问题,但是会遇到循环引用的问题。为了解决标记清除和提高效率,可以采用“标记清除”和“分代回收”的方法。其中,前者用于解决循环引用的问题,后者则基于对象的生命周期长短将对象分为不同的代,以提高垃圾回收的效率。[^2]

可以通过 sys.getrefcount 方法查看引用计数

from sys import getrefcount
a = [1, 2, 3]
b = a
del b
print(getrefcount(a))

References

[^1]:copy-浅层和深层赋值操作-Python 3.12.0 文档
[^2]:没白熬夜,终于把Python的内存管理机制搞明白了-知乎