本文基于 CPython 实现讨论引用计数和对象释放。其他 Python 解释器的具体实现可能不同。
学习 Python 或其他面向对象语言时,我们经常听到这样的说法:
“创建一个
cat对象,然后用del销毁它。”
这种表达便于入门,却隐藏了许多重要细节。
严格来说,变量并不等于对象,del 也不一定是在“销毁对象”。如果不能区分名称、引用和对象,后续理解浅拷贝、深拷贝、可变对象、循环引用和垃圾回收时就很容易产生混乱。
本文从下面这行代码出发:
cat = Cat("白色")
从 Python 语言语义和 CPython 实现两个层面,分析类、对象、变量以及垃圾回收之间的关系。
一、类、对象与变量分别是什么?
1. 类:创建对象的规则,同时它自己也是对象
类描述了一类对象所具有的属性和行为。
class Cat:
def __init__(self, color):
self.color = color
def meow(self):
print("喵")
这里的 Cat 定义了:
- 实例如何初始化;
- 实例可以保存哪些属性;
- 实例可以执行哪些方法。
可以把类类比成建筑图纸,但这个比喻并不完全严谨。
在 Python 中,类本身也是对象:
print(type(Cat))
输出结果为:
<class 'type'>
因此,类并不是一份完全不占内存的抽象说明。创建类时,Python 同样需要保存类名、方法、属性和继承关系等信息。
2. 对象:运行期间存在的数据实体
执行下面的代码:
cat = Cat("白色")
会创建一个 Cat 类的实例。这个实例保存着属于自己的状态:
print(cat.color)
输出:
白色
如果再创建一个实例:
cat_2 = Cat("黑色")
那么 cat 和 cat_2 指向两个不同的对象:
print(cat is cat_2)
输出:
False
它们可以共享类中定义的方法,但各自拥有独立的实例属性。
3. 变量:绑定到对象的名称
在 Python 中,更准确的说法不是“变量里面装着对象”,而是:
名称绑定到了对象。
执行:
cat = Cat("白色")
可以理解为完成了两个主要步骤:
Cat("白色")创建一个新的实例对象;- 名称
cat与这个对象建立绑定关系。
可以用下面的示意图表示:
名称空间 Python 对象
cat ───────────────────────▶ Cat 实例
color = "白色"
我们通过名称 cat 找到对象,然后访问对象的属性和方法:
cat.color
cat.meow()
需要注意的是,将 Python 变量简单描述成“栈中保存的物理内存地址”并不严谨。
“栈中引用、堆中对象”可以作为理解高级语言内存模型的入门类比,但 Python 语言规范并没有要求名称必须存放在栈中。局部变量、全局变量、闭包变量和对象属性的保存方式并不完全相同。
因此,更通用、准确的表达是:
Python 名称存在于某个名称空间中,并绑定到相应的对象。
二、cat = Cat("白色") 到底发生了什么?
先看完整代码:
class Cat:
def __new__(cls, color):
print("1. 分配并创建实例")
return super().__new__(cls)
def __init__(self, color):
print("2. 初始化实例")
self.color = color
cat = Cat("白色")
运行结果:
1. 分配并创建实例
2. 初始化实例
从概念上看,这个过程可以分成以下几步。
第一步:查找名称 Cat
解释器首先在当前作用域中查找 Cat,找到对应的类对象。
第二步:调用类对象
执行 Cat("白色") 本质上是在调用 Cat 类。在常见情况下,这会依次涉及:
__new__()创建并返回实例;__init__()初始化该实例。
需要特别注意:
__new__()负责创建实例;__init__()负责初始化实例;__init__()并不真正负责分配对象。
第三步:建立名称绑定
实例创建完成后,赋值语句把名称 cat 绑定到这个实例。此时可以通过 cat 操作该对象。
三、多个变量可以指向同一个对象
执行下面的代码:
cat = Cat("白色")
another_cat = cat
这不会创建第二只猫,而是让两个名称绑定到同一个对象:
cat ───────────────┐
├────▶ Cat 实例
another_cat ───────┘ color = "白色"
可以通过 is 验证对象身份:
print(cat is another_cat)
输出:
True
因此,通过其中一个名称修改对象,另一个名称也能观察到变化:
another_cat.color = "黑色"
print(cat.color)
输出:
黑色
原因并不是两个变量的数据被自动同步,而是它们从始至终指向同一个对象。这也是理解浅拷贝、可变对象和函数传参的基础。
四、del cat 删除的究竟是什么?
假设存在下面的代码:
cat = Cat("白色")
another_cat = cat
del cat
执行 del cat 后,被删除的是名称 cat 与对象之间的绑定。此时对象仍然可以通过 another_cat 访问:
print(another_cat.color)
输出:
白色
这说明:
del删除名称绑定,并不等价于直接销毁对象。
只有当对象不再拥有任何强引用时,它才可能进入释放流程。
五、CPython 的引用计数
CPython 是目前最常用的 Python 实现。它主要通过引用计数管理对象生命周期。
每个对象都会记录当前有多少个强引用指向自己。可以使用 sys.getrefcount() 进行观察:
import sys
cat = Cat("白色")
print(sys.getrefcount(cat))
实际输出通常会比预期多 1,因为把 cat 传给 getrefcount() 时,函数调用本身会临时增加一次引用。
继续执行:
another_cat = cat
print(sys.getrefcount(cat))
引用计数会相应增加。删除一个名称后,引用计数又会下降:
del another_cat
print(sys.getrefcount(cat))
当对象的引用计数下降到 0 时,CPython 通常会立即进入对象释放流程。
因此,把这一过程描述为“垃圾回收器定期巡逻后才发现对象”并不准确。对于普通的非循环引用对象,CPython 的引用计数通常可以立即处理它们。
六、为什么只有引用计数还不够?
引用计数无法独立解决循环引用问题。
class Node:
def __init__(self):
self.next = None
node_a = Node()
node_b = Node()
node_a.next = node_b
node_b.next = node_a
此时对象之间形成了循环:
node_a 对象 ─────▶ node_b 对象
▲ │
└─────────────────┘
即使删除外部名称:
del node_a
del node_b
两个对象内部仍然互相引用。为了解决这种问题,CPython 还提供了循环垃圾回收机制,用来寻找已经无法从程序访问、却因为互相引用而没有归零的对象。
因此,CPython 的内存管理可以概括为:
引用计数:处理大多数普通对象
循环垃圾回收:补充处理循环引用
二者并不是互相替代的关系。
七、__del__() 并不等于 del
这两个名称非常相似,但作用完全不同。
del
del 是 Python 语句,用来删除名称、容器元素或对象属性:
del cat
del numbers[0]
del user.name
__del__()
__del__() 是对象的终结器。当对象即将被回收时,解释器可能调用它:
class Cat:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"{self.name} 对象即将被回收")
cat = Cat("小白")
del cat
在 CPython 的简单场景中,可能立即输出:
小白 对象即将被回收
但不应该把 __del__() 当作可靠的资源管理工具。原因包括:
- 对象具体何时被回收可能与解释器实现有关;
- 循环引用会让对象生命周期更加复杂;
- 解释器退出时,全局对象可能已经被部分清理;
__del__()中出现异常时不容易正确处理;- 对象甚至可能在
__del__()中重新建立引用。
因此,不应依赖 __del__() 关闭文件、数据库连接或网络连接。
八、资源释放应该使用上下文管理器
管理文件等外部资源时,推荐使用 with:
with open("example.txt", "r", encoding="utf-8") as file:
content = file.read()
离开 with 代码块后,即使发生异常,文件也会被正确关闭。
对于自定义资源,可以实现上下文管理协议:
class Connection:
def __enter__(self):
print("建立连接")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("关闭连接")
with Connection() as connection:
print("使用连接")
输出:
建立连接
使用连接
关闭连接
这比依赖 __del__() 更明确、更安全,也更容易维护。
九、对象身份与内存地址
Python 提供了 id() 函数,用来返回对象在当前生命周期中的唯一标识:
cat = Cat("白色")
print(id(cat))
在 CPython 中,id() 通常与对象当前的内存地址有关,但 Python 语言规范只保证:
一个对象在其生命周期内拥有唯一且不变的
id。
对象被释放后,这个标识可能被新对象重复使用。因此,不应该把 id() 当作永久编号。
判断两个名称是否指向同一对象时,应使用 is;比较两个对象的值是否相等时,通常使用 ==:
print(object_a is object_b)
print(object_a == object_b)
is:是否为同一个对象;==:对象的值是否相等。
十、完整实验:观察对象生命周期
可以通过下面的代码观察名称绑定和对象回收:
import sys
class Cat:
def __init__(self, name):
self.name = name
print(f"创建对象:{self.name}")
def __del__(self):
print(f"回收对象:{self.name}")
cat = Cat("小白")
print("第一次引用计数:", sys.getrefcount(cat))
another_cat = cat
print("第二次引用计数:", sys.getrefcount(cat))
del cat
print("删除 cat 后,对象仍然存在")
print("another_cat.name =", another_cat.name)
del another_cat
print("最后一个名称绑定已删除")
可能得到类似结果:
创建对象:小白
第一次引用计数:2
第二次引用计数:3
删除 cat 后,对象仍然存在
another_cat.name = 小白
回收对象:小白
最后一个名称绑定已删除
这个实验说明:
- 创建实例后,名称
cat绑定到对象; another_cat = cat不会复制对象;del cat只删除一个名称绑定;- 只要还存在其他强引用,对象就不会被释放;
- 最后一个强引用消失后,CPython 通常立即释放对象。
十一、总结
理解 Python 对象模型时,可以记住下面几句话:
1. 类也是对象
类不仅用于描述实例,它本身也由 Python 的对象系统管理。
2. 变量是名称绑定
Python 变量不是存放完整对象的盒子,而是名称空间中绑定到对象的名称。
3. 赋值通常不会复制对象
b = a
通常意味着让 b 和 a 指向同一个对象,而不是自动复制一份数据。
4. del 删除的是绑定关系
del a
不代表一定立即销毁对象。只有在不存在其他有效引用时,对象才可能被释放。
5. CPython 主要使用引用计数
普通对象通常在引用计数归零时立即释放;循环垃圾回收用于补充处理循环引用。
6. 不要依赖 __del__() 管理重要资源
文件、锁、数据库连接和网络连接等资源,应使用上下文管理器或显式清理逻辑。
结语
面向对象语言帮助我们隐藏了大量内存管理细节,但这些细节并没有消失。
当我们从“创建变量”和“删除对象”这样的表层说法继续向下探索,就会发现真正发生的是:
创建对象
↓
建立名称绑定
↓
增加或减少引用
↓
引用计数归零
↓
释放对象占用的资源
掌握对象、名称、引用和生命周期之间的关系,不仅能帮助我们理解 Python,也能为学习数据结构、操作系统、编译原理和垃圾回收算法建立更扎实的基础。
真正理解一门语言,不只是记住它的语法,更要理解语法背后的运行机制。