Python 基础体系 · 第 22/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 对象模型:type、object、属性查找、身份与生命周期
Python 程序中的数据、函数、类、模块,最终都以对象的形式存在。理解对象模型,不能只把类看成“创建实例的模板”:类本身也是对象,类的行为由另一个对象——元类——控制;属性访问也不是简单的字典读取,而是一套包含描述符、实例字典、方法解析顺序和动态钩子的查找算法。
本文以 Python 3.14 的语言语义为主,并在涉及内存管理时区分 Python 规范、常见实现和 CPython 细节。
一、先建立对象模型的基本坐标系
1.1 每个对象都有身份、类型和值
Python 数据模型为对象定义了三个基本维度:
- 身份 identity:对象在其生命周期内的唯一标识;
- 类型 type:对象属于什么类型,以及支持哪些操作;
- 值 value:对象当前表示的数据。
例如:
x = [1, 2]
y = x
z = [1, 2]
print(x is y) # True
print(x is z) # False
print(x == z) # True
print(type(x)) # <class 'list'>
print(id(x) == id(y)) # True
x、y 和 z 的关系如下:
x ─┐
├──> 同一个 list 对象:[1, 2]
y ─┘
z ───> 另一个 list 对象:[1, 2]
因此:
x is y比较的是身份;x == z比较的是值;x is z比较的是身份,所以结果为False;id(x)返回一个代表身份的整数。
对象被创建后,其身份和类型不会改变。对 CPython 而言,id(x) 通常就是对象所在的内存地址;但“id 等于地址”是 CPython 实现细节,不是所有 Python 实现都必须采用的方式。(docs.python.org)
1.2 变量不是对象,而是对象的引用
下面的赋值不会复制列表:
items = []
alias = items
alias.append("Python")
print(items) # ['Python']
print(items is alias) # True
赋值过程可以抽象为:
执行 items = []
1. 创建一个 list 对象
2. 创建名字 items
3. 让 items 指向这个 list 对象
执行 alias = items
1. 读取 items 当前指向的对象
2. 创建名字 alias
3. 让 alias 也指向同一个对象
因此,变量名只是命名空间中的绑定关系。对象是否存活,取决于是否仍然存在指向它的引用,以及是否存在循环引用等情况。
1.3 类型本身也是对象
type() 返回一个对象的类型:
class User:
pass
user = User()
print(type(user)) # <class '__main__.User'>
print(type(User)) # <class 'type'>
print(type(object)) # <class 'type'>
print(type(type)) # <class 'type'>
这里有三层关系:
user ───实例───> User
User ───实例───> type
object ─实例───> type
type ──实例───> type
也就是说:
user是User的实例;User是type的实例;object是type的实例;type也是type的实例。
类是对象,元类也是对象。type 是 Python 中最常见的元类:如果没有显式指定元类,通常由 type 创建类对象。
二、object 与 type 的关系
2.1 object 是普通类层次的根
object 是 Python 类继承体系中的根类。没有显式父类的类,默认继承自 object:
class User:
pass
print(User.__bases__) # (<class 'object'>,)
print(User.__mro__)
# (<class '__main__.User'>, <class 'object'>)
因此:
isinstance(User(), object) # True
issubclass(User, object) # True
object 提供了所有普通对象都能使用的基础行为,例如默认的身份、字符串表示、属性访问和比较相关机制。
但必须区分两个概念:
object是实例继承关系中的根;type是类对象创建关系中的典型元类。
2.2 type 是类对象的类型
可以把 type 理解为“创建类对象的类”:
User = type("User", (), {})
user = User()
print(User) # <class '__main__.User'>
print(type(User)) # <class 'type'>
print(type(user)) # <class '__main__.User'>
print(User.__bases__) # (<class 'object'>,)
三参数形式的 type:
type(name, bases, namespace)
可近似理解为:
- 使用
name作为类名; - 使用
bases作为父类元组; - 使用
namespace作为类命名空间; - 创建并返回一个新的类对象。
例如:
def greet(self):
return f"hello, {self.name}"
User = type(
"User",
(),
{
"name": "anonymous",
"greet": greet,
},
)
user = User()
print(user.greet()) # hello, anonymous
这与下面的类定义在核心结果上相近:
class User:
name = "anonymous"
def greet(self):
return f"hello, {self.name}"
类语句并不是简单地把代码文本转换成字典。类体会在一个新的命名空间中执行,随后由元类根据类名、基类和命名空间创建类对象。普通情况下,这个元类就是 type。
2.3 type 既是 object 的子类,又是自身的实例
下面四个表达式容易混淆:
print(issubclass(type, object)) # True
print(issubclass(object, type)) # False
print(isinstance(object, type)) # True
print(isinstance(type, type)) # True
解释如下:
继承关系:
type ───继承───> object
实例关系:
object ───实例───> type
type ───实例───> type
继承关系和实例关系是两条不同的边,不能混为一谈。
还可以观察它们的 MRO:
print(object.__mro__)
# (<class 'object'>,)
print(type.__mro__)
# (<class 'type'>, <class 'object'>)
type 继承自 object,而 object 自身没有普通 Python 基类。但 object 作为一个类对象,仍然是 type 的实例。
这形成了一个经过精心设计的闭环:
object 是 type 的实例
type 是 type 的实例
type 继承 object
这个闭环避免了“类对象必须由另一个无限向上的类创建”的问题。
三、类对象、实例对象与命名空间
3.1 类对象包含类命名空间
类通常有一个由字典实现的命名空间:
class Account:
currency = "CNY"
def __init__(self, balance):
self.balance = balance
print(Account.__dict__["currency"]) # CNY
print(Account.__dict__["__init__"]) # <function Account.__init__ ...>
Account.__dict__ 是一个映射代理对象,不能直接把它当作普通可写字典使用:
print(type(Account.__dict__))
# <class 'mappingproxy'>
类属性通常存放在类命名空间中:
Account.__dict__
├── currency -> "CNY"
└── __init__ -> function object
函数也是对象,因此类命名空间中的 __init__ 并不是特殊的“源码入口”,而是一个函数对象。它之所以在实例访问时变成绑定方法,是因为函数实现了描述符协议。
3.2 实例通常拥有自己的命名空间
class Account:
currency = "CNY"
def __init__(self, balance):
self.balance = balance
a = Account(100)
b = Account(200)
print(a.__dict__) # {'balance': 100}
print(b.__dict__) # {'balance': 200}
实例字典中只记录实例自己的属性:
a.__dict__ -> {"balance": 100}
b.__dict__ -> {"balance": 200}
Account.__dict__ -> {
"currency": "CNY",
"__init__": function,
}
访问 a.currency 时,currency 不在 a.__dict__ 中,但可以从 Account 及其父类中找到,因此仍然能够访问。
实例属性赋值默认更新实例字典,而不是类字典:
a.currency = "USD"
print(a.currency) # USD
print(b.currency) # CNY
print(Account.currency) # CNY
print(a.__dict__) # {'balance': 100, 'currency': 'USD'}
这里的 a.currency 是实例属性;Account.currency 是类属性。它们同名,但存储位置不同。
四、属性查找:点号不是简单的字典访问
属性访问 obj.name 的默认行为由 obj.__getattribute__("name") 实现。不过,实际结果还可能受到以下机制影响:
- 数据描述符;
- 实例字典;
- 非数据描述符;
- 类属性;
- 基类 MRO;
__getattr__;- 特殊方法的隐式查找规则。
4.1 实例属性查找的优先级
对普通实例 obj 查找 obj.name 时,可以使用下面的等价模型理解:
def conceptual_getattribute(obj, name):
cls = type(obj)
cls_var = find_in_mro(cls, name)
if is_data_descriptor(cls_var):
return cls_var.__get__(obj, cls)
if hasattr(obj, "__dict__") and name in obj.__dict__:
return obj.__dict__[name]
if hasattr(cls_var, "__get__"):
return cls_var.__get__(obj, cls)
if cls_var is not MISSING:
return cls_var
raise AttributeError(name)
其中:
def find_in_mro(cls, name):
for base in cls.__mro__:
if name in base.__dict__:
return base.__dict__[name]
return MISSING
这个模型不是 CPython 的逐行源码,而是对语言层属性访问规则的抽象。关键优先级是:
数据描述符
>
实例字典
>
非数据描述符
>
普通类属性
类属性搜索按照 type(obj).__mro__ 进行,因此继承关系会直接影响属性结果。
4.2 数据描述符会压过实例字典
数据描述符是定义了 __set__ 或 __delete__ 的描述符。最典型的例子是 property:
class User:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
user = User("Alice")
user.__dict__["name"] = "Injected"
print(user.name) # Alice
虽然 user.__dict__ 中存在 "name",但 property 是数据描述符,因此 user.name 会调用属性的 getter,而不是返回实例字典中的值。
可以手动观察类字典中的对象:
print(type(User.__dict__["name"]))
# <class 'property'>
这个行为是 property 能够实现封装的根本原因:实例不能通过同名实例属性绕过 property 的 getter 和 setter。描述符协议规定,数据描述符优先级高于实例字典。(docs.python.org)
4.3 非数据描述符可以被实例属性覆盖
只定义 __get__ 的描述符是非数据描述符。普通函数属于这一类,因此方法可以被单个实例覆盖:
class User:
def greet(self):
return "class method"
user = User()
print(user.greet()) # class method
user.greet = lambda: "instance attribute"
print(user.greet()) # instance attribute
print(User().greet()) # class method
第一次执行 user.greet() 时:
1. User.__dict__["greet"] 是函数对象
2. 函数对象实现 __get__
3. 生成绑定方法
4. 绑定方法的 __self__ 是 user
5. 调用绑定方法,相当于 greet(user)
赋值后:
user.__dict__["greet"] -> lambda
由于函数是非数据描述符,实例字典优先级高于它,所以后续访问直接得到 lambda。
Python 方法、staticmethod 和 classmethod 都使用非数据描述符机制;property 则是数据描述符。(docs.python.org)
4.4 函数如何变成绑定方法
class User:
def greet(self, suffix):
return f"hello {self.name}{suffix}"
def __init__(self, name):
self.name = name
user = User("Alice")
bound = user.greet
raw = User.__dict__["greet"]
print(bound.__self__ is user) # True
print(bound.__func__ is raw) # True
print(bound("!")) # hello Alice!
可以近似写成:
bound = User.__dict__["greet"].__get__(user, User)
而从类访问时:
unbound_like = User.__dict__["greet"].__get__(None, User)
在 Python 3 中,类访问得到的通常仍是函数对象;实例访问才会生成绑定方法。描述符协议的实例绑定形式是:
type(user).__dict__["greet"].__get__(user, type(user))
类绑定形式是:
User.__dict__["greet"].__get__(None, User)
这正是“方法绑定”的机制,而不是编译器给每个实例复制了一份方法。(docs.python.org)
五、描述符:属性查找算法中的行为对象
5.1 描述符的定义
如果一个对象定义了以下任意方法:
__get__
__set__
__delete__
当它被放入某个类的类字典后,Python 就可能在属性访问、赋值或删除时调用这些方法。这样的对象称为描述符。
一个最小的只读描述符:
class LoggedAttribute:
def __init__(self, default):
self.default = default
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
if instance is None:
return self
print(f"读取 {self.name}")
return instance.__dict__.get(self.name, self.default)
class User:
name = LoggedAttribute("anonymous")
user = User()
print(user.name)
# 读取 name
# anonymous
user.__dict__["name"] = "Alice"
print(user.name)
# 读取 name
# Alice
当通过类访问时:
print(User.name)
# 返回 LoggedAttribute 对象本身
因为类访问的描述符调用形式是:
User.__dict__["name"].__get__(None, User)
描述符通常存放在类字典中,而不是实例字典中。CPython 的 C 层也使用描述符对象暴露某些由 C getter/setter 实现的属性。(docs.python.org)
5.2 数据描述符与非数据描述符
定义可以形式化为:
def is_data_descriptor(value):
return (
hasattr(type(value), "__set__")
or hasattr(type(value), "__delete__")
)
注意检查的是描述符类型是否提供这些方法,而不是描述符实例是否恰好拥有某个普通属性。
示例:
class NonData:
def __get__(self, instance, owner=None):
return "from descriptor"
class Data:
def __get__(self, instance, owner=None):
return "from descriptor"
def __set__(self, instance, value):
instance.__dict__["_value"] = value
class Demo:
non_data = NonData()
data = Data()
demo = Demo()
demo.__dict__["non_data"] = "from instance"
demo.__dict__["data"] = "from instance"
print(demo.non_data) # from instance
print(demo.data) # from descriptor
原因是:
non_data:
实例字典优先于非数据描述符
data:
数据描述符优先于实例字典
这条优先级是 property、ORM 字段、懒加载属性和许多框架魔法的基础。
5.3 __getattribute__ 与 __getattr__ 的区别
__getattribute__ 会参与每一次实例属性访问:
class Trace:
def __getattribute__(self, name):
print(f"getattribute: {name}")
return object.__getattribute__(self, name)
def __getattr__(self, name):
print(f"getattr: {name}")
return f"<missing: {name}>"
value = 42
obj = Trace()
print(obj.value)
# getattribute: value
# 42
print(obj.missing)
# getattribute: missing
# getattr: missing
# <missing: missing>
调用过程是:
obj.missing
│
├─ 调用 Trace.__getattribute__(obj, "missing")
│
├─ 默认查找失败,抛出 AttributeError
│
└─ 由点号访问机制调用 Trace.__getattr__(obj, "missing")
__getattr__ 只在默认查找抛出 AttributeError 时调用。它不是每次访问都会运行的兜底函数。
重写 __getattribute__ 时,必须使用基类实现读取内部属性,否则会无限递归:
class Broken:
def __getattribute__(self, name):
return self.__dict__[name] # 访问 self.__dict__ 又会触发自身
class Correct:
def __getattribute__(self, name):
namespace = object.__getattribute__(self, "__dict__")
if name in namespace:
return namespace[name]
return object.__getattribute__(self, name)
更常见的写法是:
class Correct:
def __getattribute__(self, name):
print("lookup:", name)
return object.__getattribute__(self, name)
object.__getattribute__ 本身不会自动调用 __getattr__;__getattr__ 是点号访问和 getattr() 在捕获 AttributeError 后触发的额外逻辑。(docs.python.org)
5.4 特殊方法查找可能绕过实例属性
下面的代码不会让 len(obj) 使用实例中的 __len__:
class Box:
pass
box = Box()
box.__len__ = lambda: 10
print(box.__len__()) # 10
print(len(box)) # TypeError: object of type 'Box' has no len()
显式访问 box.__len__ 使用了普通实例属性查找;而 len(box) 属于语言语法对应的隐式特殊方法调用,通常直接从类型上查找 __len__,不会先读取实例字典。
因此,如果需要让对象支持 len(),必须在类中定义:
class Box:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
box = Box([1, 2, 3])
print(len(box)) # 3
这一规则同样影响 +、==、迭代、上下文管理器等协议。仅给单个实例动态添加特殊方法,通常不能改变这些隐式操作。(docs.python.org)
六、MRO:属性查找如何穿过继承树
6.1 MRO 是线性的类搜索顺序
对于:
class A:
value = "A"
class B(A):
pass
print(B.__mro__)
# (<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
访问 B().value 时,类属性搜索顺序为:
B -> A -> object
__mro__ 是一个元组,描述了方法解析顺序。多继承时,Python 使用 C3 线性化算法生成它。
class A:
def name(self):
return "A"
class B(A):
def name(self):
return "B"
class C(A):
def name(self):
return "C"
class D(B, C):
pass
print(D.__mro__)
# (D, B, C, A, object)
print(D().name()) # B
D().name() 先检查 D,再检查 B,所以得到 B.name。
6.2 菱形继承中的 super()
class A:
def run(self):
print("A")
class B(A):
def run(self):
print("B")
super().run()
class C(A):
def run(self):
print("C")
super().run()
class D(B, C):
def run(self):
print("D")
super().run()
D().run()
输出:
D
B
C
A
关键不是“super() 调用父类”,而是:
super() 按当前对象类型的 MRO,寻找当前类之后的下一个类
对于 D.run,下一个是 B;对于 B.run,在 D.__mro__ 中 B 后面是 C;对于 C.run,后面是 A。
因此,协作式多继承要求每个实现都正确调用 super()。如果某个类直接写死父类名称:
A.run(self)
就会绕过 MRO 的协作链,可能导致重复调用或跳过其他分支。
七、type.__getattribute__:访问类属性时又是一套查找
前面的实例查找针对 obj.name。当访问 Class.name 时,访问者是一个类对象,实际参与查找的是元类的属性访问逻辑。
例如:
class Demo:
value = 10
print(Demo.value)
可以近似理解为:
type.__getattribute__(Demo, "value")
它会沿着 Demo.__mro__ 查找 value,如果找到描述符,则调用:
Demo.__dict__["value"].__get__(None, Demo)
这解释了:
class Descriptor:
def __get__(self, instance, owner=None):
print("instance:", instance)
print("owner:", owner)
return 123
class Demo:
value = Descriptor()
print(Demo.value)
# instance: None
# owner: <class '__main__.Demo'>
# 123
类访问时,描述符的 instance 参数为 None,owner 参数为被访问的类。
还存在一个更高层的边界:访问 C.name 时,元类本身的属性也可能参与处理。因此,如果元类定义了同名的数据描述符,它可能优先于 C 的普通类属性。这是“实例查找”和“类查找”不能简单套用同一条直觉的原因。
八、身份、相等性与对象复用
8.1 is 与 == 解决不同问题
is 比较两个引用是否指向同一个对象:
a = []
b = a
c = []
print(a is b) # True
print(a is c) # False
== 调用对象的相等性协议,比较对象代表的值:
print(a == c) # True
对于自定义类,如果没有重写 __eq__,默认相等性通常与身份相关:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # False
print(p1 is p2) # False
如果重写:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
print(p1 is p2) # False
8.2 不要用 is 判断普通值
下面的代码可能“看起来能工作”,但不应依赖:
a = 1000
b = 1000
print(a is b) # 不应据此编写逻辑
对于不可变对象,解释器可能复用具有相同值的对象;具体是否复用属于实现行为。对可变对象,两个分别创建的空列表则保证是不同对象:
a = []
b = []
print(a is b) # False
x = y = []
print(x is y) # True
is 适合判断单例对象,例如:
result = None
if result is None:
print("没有结果")
对象身份只在对象存活期间有意义。对象销毁后,CPython 可能把其内存位置重新用于另一个对象,因此不能长期保存 id() 并把它当作永久对象编号。
九、对象生命周期:从创建到销毁
一个普通实例的生命周期,可以拆成以下阶段:
调用类对象
│
▼
元类分配对象
│
▼
__new__ 创建或返回实例
│
▼
__init__ 初始化实例
│
▼
对象被引用、使用、修改
│
▼
引用减少,可能变得不可达
│
├──无循环引用:通常可释放
│
└──存在循环引用:由循环垃圾回收器处理
│
▼
最终化与释放
9.1 __new__ 负责创建,__init__ 负责初始化
class User:
def __new__(cls, name):
print("__new__", cls, name)
instance = super().__new__(cls)
return instance
def __init__(self, name):
print("__init__", name)
self.name = name
user = User("Alice")
典型输出:
__new__ <class '__main__.User'> Alice
__init__ Alice
调用 User("Alice") 时,类对象本身可近似看成执行:
type(User).__call__(User, "Alice")
对于普通类,调用流程大致是:
- 元类的
__call__被调用; - 元类调用
User.__new__(User, "Alice"); - 如果
__new__返回的是User实例,继续调用User.__init__(instance, "Alice"); - 返回实例。
__new__ 的返回值决定后续行为。如果它返回的不是当前类实例,通常不会调用当前类的 __init__:
class Factory:
def __new__(cls):
return object()
def __init__(self):
print("不会执行")
value = Factory()
print(type(value)) # <class 'object'>
__new__ 常用于不可变类型的定制创建、实例缓存和单例控制;__init__ 只负责对已经创建的对象进行初始化,不负责分配对象内存。
9.2 __init__ 不是构造器的全部
下面的代码中,__new__ 返回了一个已有对象:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
print("初始化")
a = Singleton()
b = Singleton()
print(a is b) # True
输出中的“初始化”可能出现两次,因为每次调用类对象时,若 __new__ 返回的是当前类实例,调用流程仍可能执行 __init__。
这说明:
对象身份复用 != 初始化逻辑只执行一次
如果需要真正的一次性初始化,必须额外维护状态。
9.3 __del__ 是最终化钩子,不是可靠资源管理器
class Tracer:
def __del__(self):
print("对象最终化")
obj = Tracer()
del obj
在常见的 CPython 场景中,del obj 使对象不再被引用后,可能很快看到输出。但这不是 del 语句直接“释放对象”的保证:
del obj删除的是名字绑定;- 如果还有其他引用,对象仍然存活;
- 循环引用可能延迟处理;
- 解释器关闭时,模块全局状态可能已经部分销毁;
__del__中的异常不会正常传播给调用者。
资源应该使用确定性的上下文管理协议:
class Resource:
def __enter__(self):
print("acquire")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("release")
return False
with Resource():
print("work")
输出:
acquire
work
release
__exit__ 会在退出 with 块时执行,包括块内抛出异常的情况,因此更适合文件、锁、数据库事务等资源管理。
十、CPython 中的引用计数与循环垃圾回收
10.1 引用计数处理直接不可达对象
在传统 CPython 构建中,对象通常维护引用计数。可以用 sys.getrefcount() 观察,但该函数调用本身会临时增加一个引用:
import sys
value = []
print(sys.getrefcount(value))
因此输出通常比直觉多 1,且具体结果可能受解释器内部临时引用影响。
当引用计数降为零时,CPython 通常会立即释放对象:
class Tracer:
def __del__(self):
print("finalized")
obj = Tracer()
alias = obj
del obj
print("still alive")
del alias
print("after delete")
典型输出:
still alive
finalized
after delete
顺序说明:
obj = Tracer() -> 引用计数至少为 1
alias = obj -> 引用计数增加
del obj -> 引用计数减少,但对象仍存活
del alias -> 引用计数降为 0,通常立即最终化
这是 CPython 的常见行为,不应直接推广为所有 Python 实现的语言保证。
10.2 循环引用不能单靠引用计数解决
import gc
import weakref
class Node:
pass
a = Node()
b = Node()
a.other = b
b.other = a
ref = weakref.ref(a)
del a
del b
print(ref() is None) # 可能仍为 False
gc.collect()
print(ref() is None) # True
删除名字后,两个节点仍然互相引用:
a 对象 -> b 对象
b 对象 -> a 对象
外部虽然已经没有引用,但每个对象的引用计数仍不为零。循环垃圾回收器负责发现这种“从程序根不可达,但内部互相引用”的对象组,并尝试回收它们。
Python 的垃圾回收由引用计数和循环垃圾回收器共同完成;gc 模块提供了启用、禁用、主动收集和调试接口。(docs.python.org)
10.3 weakref 不会保持对象存活
弱引用指向对象,但不增加对象的强引用计数:
import weakref
class User:
pass
user = User()
reference = weakref.ref(user)
print(reference() is user) # True
del user
print(reference()) # None
这适合实现缓存、观察者注册表和对象索引,避免容器本身阻止对象释放。
但不是所有对象都支持弱引用。是否支持弱引用取决于对象类型的实现。例如某些使用 __slots__ 的类若想支持弱引用,需要显式包含 __weakref__:
class User:
__slots__ = ("name", "__weakref__")
def __init__(self, name):
self.name = name
十一、生命周期中的引用关系与内存泄漏边界
11.1 闭包会延长对象生命周期
def make_reader():
data = bytearray(10_000_000)
def read():
return len(data)
return read
reader = make_reader()
make_reader() 返回后,局部名字 data 已经离开栈帧,但内部函数 read 的闭包仍然引用它:
reader
└──> function read
└──> closure cell
└──> bytearray
因此,大对象是否释放不能只看创建它的函数是否返回,还要检查闭包、缓存、全局变量、任务队列和异常对象等间接引用。
11.2 异常回溯也可能暂时保留对象
异常对象可能持有 traceback,traceback 又可能引用栈帧和局部变量:
def work():
large_data = bytearray(10_000_000)
raise RuntimeError("failed")
try:
work()
except RuntimeError as error:
saved = error
如果长期保存 saved,就可能间接保留异常发生时的栈帧及其局部对象。生产代码中如果需要保存错误信息,通常应提取必要字段,而不是无期限保存完整异常链和 traceback 对象。
这不是“Python 永远内存泄漏”,而是对象仍然可达,因此垃圾回收器不能释放它。
十二、Python 3.14 与 CPython 生命周期的实现边界
Python 3.14 同时存在不同构建模式。传统构建中,CPython 常通过引用计数快速回收直接不可达对象;而自由线程构建为了减少多线程共享引用计数的竞争,可能采用延迟引用计数、每线程引用计数等机制,使某些对象的释放晚于引用计数看似归零的时刻。(docs.python.org)
因此,下列代码不应作为跨实现或跨构建的时间保证:
del obj
# 这里立刻执行完 __del__ 吗?
# 不能把它当作通用语言保证。
可以依赖的层次应区分为:
语言层:
对象有身份、类型和值
is 比较身份
with 通过上下文管理协议确定性地清理资源
CPython 常见行为:
引用计数归零时通常快速释放
id 通常对应内存地址
具体构建或实现细节:
自由线程构建的引用计数策略
内存分配器何时将内存归还给操作系统
小整数、字符串等对象是否被缓存或复用
尤其不要使用对象析构时机来关闭文件、释放锁或提交事务;这些动作应放入 with、显式 close() 或明确的事务控制流程中。
十三、如何诊断“属性到底从哪里来”
13.1 使用 vars() 和 __dict__
class Parent:
value = "parent"
class Child(Parent):
pass
obj = Child()
print(vars(obj)) # {}
print(vars(Child)) # 类命名空间
print(vars(Parent)) # 父类命名空间
print(obj.value) # parent
vars(obj) 在对象具有 __dict__ 时,通常等价于读取 obj.__dict__。但没有 __dict__ 的对象不能使用这种方式:
class Compact:
__slots__ = ("value",)
obj = Compact()
obj.value = 1
print(obj.value) # 1
# print(obj.__dict__) # AttributeError
__slots__ 并不改变对象模型,只是改变实例属性的存储方式,并可能禁止动态添加未声明的属性。
13.2 使用 inspect.getattr_static 避免触发动态访问
普通的 getattr(obj, name) 会触发描述符、__getattribute__ 和 __getattr__。诊断属性来源时,可以使用:
import inspect
class User:
@property
def name(self):
return "computed"
user = User()
print(getattr(user, "name"))
# computed
print(inspect.getattr_static(user, "name"))
# <property object at ...>
inspect.getattr_static() 返回静态发现的成员,不触发描述符协议、__getattr__ 或 __getattribute__,因此适合检查“类字典里实际放的是什么”。但它也可能返回描述符对象,而不是最终动态值。(docs.python.org)
13.3 逐层排查属性冲突
遇到“属性值不符合预期”时,可以按以下顺序验证:
name = "value"
cls = type(obj)
print("instance:", vars(obj) if hasattr(obj, "__dict__") else None)
for base in cls.__mro__:
if name in base.__dict__:
raw = base.__dict__[name]
print("found in:", base)
print("raw value:", raw)
print("is data descriptor:", hasattr(type(raw), "__set__")
or hasattr(type(raw), "__delete__"))
break
else:
print("not found in class MRO")
这个过程能区分:
- 实例字典覆盖了普通类属性;
- property 或其他数据描述符压过了实例字典;
- 方法实际来自哪个父类;
__getattr__是否只是最后的动态兜底;- 当前对象是否根本没有
__dict__。
十四、几个必须避免的错误直觉
错误一:类是模板,只有实例才是对象
错误。类、函数、模块、描述符、元类都是对象。类对象可以被赋值、传参、存储,也可以通过 type() 查看其类型。
def accept(value):
print(type(value))
accept(int) # <class 'type'>
accept(lambda: 1) # <class 'function'>
错误二:obj.attr 等价于 obj.__dict__["attr"]
错误。这个表达式可能触发:
- 数据描述符;
- 非数据描述符;
- 类和基类查找;
__getattribute__;__getattr__。
甚至对象可能没有 __dict__,但仍然支持属性访问,例如使用 __slots__ 的实例。
错误三:实例属性永远覆盖类属性
错误。实例属性只能覆盖普通类属性和非数据描述符,不能覆盖数据描述符:
class Demo:
@property
def value(self):
return "property"
obj = Demo()
obj.__dict__["value"] = "instance"
print(obj.value) # property
错误四:del name 会销毁对象
错误。它只删除一个名字绑定:
a = []
b = a
del a
print(b) # []
只有当对象不再被任何强引用保持,并且满足具体实现的回收条件时,才可能进入释放流程。
错误五:id() 可以作为永久主键
错误。对象释放后,身份值可能被后续对象复用。业务主键应由应用层生成和管理,而不是使用进程内对象的 id()。
错误六:给实例添加 __len__ 就能让 len() 生效
错误。特殊方法的隐式查找通常直接访问类型,而不是实例字典。必须把特殊方法定义在类上。
十五、把整个对象模型串起来
考虑下面的代码:
class Positive:
def __get__(self, instance, owner=None):
if instance is None:
return self
return instance.__dict__["_value"]
def __set__(self, instance, value):
if value <= 0:
raise ValueError("must be positive")
instance.__dict__["_value"] = value
class Account:
balance = Positive()
def __init__(self, balance):
self.balance = balance
def deposit(self, amount):
self.balance += amount
account = Account(100)
account.deposit(50)
print(account.balance) # 150
完整执行路径如下。
第一步:创建 Positive 描述符对象
Positive()
得到一个普通对象,并把它放入 Account 的类命名空间:
Account.__dict__["balance"] -> Positive descriptor
因为 Positive 定义了 __get__ 和 __set__,它是数据描述符。
第二步:调用 Account(100)
调用类对象会进入元类控制的实例化流程,随后执行:
Account.__new__(Account, 100)
Account.__init__(account, 100)
第三步:执行 self.balance = balance
赋值不是简单执行:
self.__dict__["balance"] = balance
由于 Account.__dict__["balance"] 是数据描述符,赋值会调用:
Positive.__set__(account, 100)
描述符把值存入:
account.__dict__["_value"] = 100
第四步:执行 account.balance
访问时,数据描述符优先级高于实例字典,因此调用:
Positive.__get__(account, Account)
返回:
account.__dict__["_value"]
第五步:执行 account.deposit(50)
访问 account.deposit 时:
- 实例字典中没有
deposit; - 在
Account.__dict__中找到函数; - 函数是非数据描述符;
- 生成绑定方法;
- 隐式传入
account作为self。
随后:
self.balance += amount
可以拆成近似步骤:
current = self.balance
new_value = current + amount
self.balance = new_value
其中读取和写入都会再次经过 Positive 描述符。
这段代码把多个机制连接起来:
类对象由 type 创建
实例对象由调用类对象创建
实例属性赋值触发数据描述符
实例方法访问触发非数据描述符
属性来源由 MRO 和优先级决定
对象存活由引用关系和垃圾回收机制决定
十六、最终的统一理解
Python 对象模型可以压缩成几条彼此关联的规则:
- 一切运行时数据都是对象,对象拥有身份、类型和值。
- 类也是对象,普通类通常是
type的实例。 object是普通继承体系的根,type是典型的类创建者。- 属性访问是协议调用,不是字典读取。
- 数据描述符优先于实例字典,实例字典优先于非数据描述符。
- 函数通过非数据描述符变成绑定方法,property 通过数据描述符实现属性行为。
- 继承查找依赖 MRO,
super()查找的是 MRO 中的下一个位置。 is比较身份,==比较值;对象身份不能替代业务相等性。__new__负责创建或返回对象,__init__负责初始化,__del__不是确定性资源管理机制。- CPython 通常结合引用计数和循环垃圾回收管理对象生命周期,但释放时机仍应与语言保证区分。
当这些规则能够同时用于推导一个表达式的执行过程时,Python 的类、继承、property、绑定方法、ORM 字段和动态属性访问就不再是互不相关的“魔法”,而是同一套对象协议在不同位置的具体表现。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python 导入系统:搜索路径、缓存、Finder、Loader 与循环依赖
- 下一篇:Python 类与继承:实例、类属性、组合、覆盖和边界
- 延伸:Python 描述符与 property:属性访问、绑定方法和 ORM 基础
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论