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

xyz 的关系如下:

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

也就是说:

  • userUser 的实例;
  • Usertype 的实例;
  • objecttype 的实例;
  • type 也是 type 的实例。

类是对象,元类也是对象。type 是 Python 中最常见的元类:如果没有显式指定元类,通常由 type 创建类对象。


二、objecttype 的关系

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)

可近似理解为:

  1. 使用 name 作为类名;
  2. 使用 bases 作为父类元组;
  3. 使用 namespace 作为类命名空间;
  4. 创建并返回一个新的类对象。

例如:

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") 实现。不过,实际结果还可能受到以下机制影响:

  1. 数据描述符;
  2. 实例字典;
  3. 非数据描述符;
  4. 类属性;
  5. 基类 MRO;
  6. __getattr__
  7. 特殊方法的隐式查找规则。

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 方法、staticmethodclassmethod 都使用非数据描述符机制;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 参数为 Noneowner 参数为被访问的类。

还存在一个更高层的边界:访问 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")

对于普通类,调用流程大致是:

  1. 元类的 __call__ 被调用;
  2. 元类调用 User.__new__(User, "Alice")
  3. 如果 __new__ 返回的是 User 实例,继续调用 User.__init__(instance, "Alice")
  4. 返回实例。

__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 时:

  1. 实例字典中没有 deposit
  2. Account.__dict__ 中找到函数;
  3. 函数是非数据描述符;
  4. 生成绑定方法;
  5. 隐式传入 account 作为 self

随后:

self.balance += amount

可以拆成近似步骤:

current = self.balance
new_value = current + amount
self.balance = new_value

其中读取和写入都会再次经过 Positive 描述符。

这段代码把多个机制连接起来:

类对象由 type 创建
实例对象由调用类对象创建
实例属性赋值触发数据描述符
实例方法访问触发非数据描述符
属性来源由 MRO 和优先级决定
对象存活由引用关系和垃圾回收机制决定

十六、最终的统一理解

Python 对象模型可以压缩成几条彼此关联的规则:

  1. 一切运行时数据都是对象,对象拥有身份、类型和值。
  2. 类也是对象,普通类通常是 type 的实例。
  3. object 是普通继承体系的根,type 是典型的类创建者
  4. 属性访问是协议调用,不是字典读取
  5. 数据描述符优先于实例字典,实例字典优先于非数据描述符
  6. 函数通过非数据描述符变成绑定方法,property 通过数据描述符实现属性行为
  7. 继承查找依赖 MRO,super() 查找的是 MRO 中的下一个位置
  8. is 比较身份,== 比较值;对象身份不能替代业务相等性
  9. __new__ 负责创建或返回对象,__init__ 负责初始化,__del__ 不是确定性资源管理机制
  10. CPython 通常结合引用计数和循环垃圾回收管理对象生命周期,但释放时机仍应与语言保证区分

当这些规则能够同时用于推导一个表达式的执行过程时,Python 的类、继承、property、绑定方法、ORM 字段和动态属性访问就不再是互不相关的“魔法”,而是同一套对象协议在不同位置的具体表现。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。