Python 基础体系 · 第 23/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 类与继承:实例、类属性、组合、覆盖和边界

Python 的类不是只用于“把字段和方法放在一起”的语法容器。类本身是对象,也是创建实例的工厂;实例保存运行时状态;继承改变属性查找路径;组合则通过对象之间的引用表达协作关系;覆盖决定同名行为由谁提供。理解这些机制,必须从 Python 的对象模型和属性查找规则开始,而不能停留在“子类复用父类代码”的描述上。

1. 类、实例与对象:先区分三个层次

Python 中的一切数据都由对象表示。每个对象都有身份、类型和值;对象创建后身份不变,type(obj) 返回对象的类型,类型本身也是一个对象。(docs.python.org)

例如:

class User:
    pass


user = User()

print(type(user) is User)
print(type(User) is type)
print(User.__bases__)
print(User.__mro__)

典型输出:

True
True
(<class 'object'>,)
(<class '__main__.User'>, <class 'object'>)

这里有三层关系:

user ──是一个实例──> User ──是一个实例──> type
                         │
                         └──继承──> object
  • userUser 的实例;
  • Usertype 的实例;
  • User 默认继承 object
  • User.__mro__ 是属性和方法查找时使用的类顺序。

没有显式基类时:

class User:
    pass

等价于:

class User(object):
    pass

类定义本身是可执行语句:Python 先创建一个新的类命名空间,执行类体中的语句,再使用基类和这个命名空间创建类对象。类体中的定义顺序会保留在类的 __dict__ 中。(docs.python.org)

因此,下面的代码在定义类时就会执行:

class Config:
    print("class body is running")
    default_timeout = 10

print("class created")

输出:

class body is running
class created

Config() 才是创建实例的动作:

config = Config()

类和实例不是同一个对象,也不是同一个命名空间:

class User:
    role = "guest"


user = User()

print(User.__dict__["role"])
print(user.__dict__)
print(user.role)

输出:

guest
{}
guest

user.role 能成功,不代表 role 存在于 user.__dict__ 中。它可能来自实例所属的类,甚至来自某个基类。


2. 实例属性:属于某一个对象的状态

实例属性是绑定在具体实例上的属性。它通常在 __init__() 中通过 self.name = value 创建,也可以在实例创建后动态添加。Python 不要求像某些静态语言那样预先声明所有实例字段。(docs.python.org)

class User:
    def __init__(self, name):
        self.name = name
        self.login_count = 0

    def login(self):
        self.login_count += 1


alice = User("Alice")
bob = User("Bob")

alice.login()
alice.login()

print(alice.__dict__)
print(bob.__dict__)

输出:

{'name': 'Alice', 'login_count': 2}
{'name': 'Bob', 'login_count': 0}

alicebob 是两个独立实例,因此:

alice.login_count += 1

只改变 alice 的状态,不会改变 bob.login_count

方法中的 self 只是约定俗成的参数名,不是关键字。下面的代码语法上有效:

class Counter:
    def __init__(this):
        this.value = 0

    def increase(this):
        this.value += 1

但工程代码应使用 self,因为读者、调试工具和静态检查工具通常都按这个约定理解实例方法。(docs.python.org)

__new__()__init__() 和实例生命周期

实例化并不只是简单地调用 __init__()。概念上可以分为:

  1. 调用类的 __new__() 创建实例;
  2. 如果返回的对象是该类的实例,再调用 __init__() 初始化它;
  3. 实例在程序中被引用和使用;
  4. 当对象不再被引用时,最终可能被回收。
class User:
    def __new__(cls, name):
        print("1. __new__", cls.__name__)
        return super().__new__(cls)

    def __init__(self, name):
        print("2. __init__", name)
        self.name = name


user = User("Alice")

输出:

1. __new__ User
2. __init__ Alice

__new__() 负责“得到哪个对象”,__init__() 负责“初始化已经得到的对象”。如果 __new__() 返回的不是目标类实例,Python 不会继续调用该类的 __init__()

class Factory:
    def __new__(cls):
        return object()

    def __init__(self):
        print("不会执行")


value = Factory()
print(type(value))

输出类似:

<class 'object'>

这也是继承内置不可变类型时常见的边界:不可变对象的核心值通常必须在 __new__() 阶段构造,而不能依赖 __init__() 修改。


3. 类属性:共享的查找来源,不等于共享的实例字段

类属性是定义在类命名空间中的属性。实例访问某个名称时,如果实例自身没有该名称,Python 会继续从实例所属类及其基类中查找。因此,类属性可以为实例提供共享值或默认值。(docs.python.org)

class User:
    default_role = "guest"

    def __init__(self, name):
        self.name = name


alice = User("Alice")
bob = User("Bob")

print(alice.default_role)
print(bob.default_role)

查找过程大致是:

alice.default_role
    │
    ├─ 查找 alice.__dict__["default_role"]
    ├─ 未找到
    ├─ 查找 User.__dict__["default_role"]
    └─ 找到 "guest"

但“读取类属性”与“通过实例赋值”是两件不同的事:

class User:
    role = "guest"


alice = User()
bob = User()

alice.role = "admin"

print(alice.role)
print(bob.role)
print(User.role)
print(alice.__dict__)

输出:

admin
guest
guest
{'role': 'admin'}

alice.role = "admin" 创建或修改的是 alice 的实例属性,而不是修改 User.role。它使 alice 的实例属性遮蔽了同名类属性。

如果确实要修改类属性,应明确写出类名:

User.role = "member"

不过,实例属性已经存在时,修改类属性不会影响该实例的同名实例属性:

class User:
    role = "guest"


alice = User()
bob = User()

alice.role = "admin"
User.role = "member"

print(alice.role)  # admin
print(bob.role)    # member

可变类属性的共享陷阱

类属性本身没有问题,危险在于把可变对象作为类属性,并误以为每个实例会自动得到一份副本:

class Dog:
    tricks = []

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)


fido = Dog("Fido")
buddy = Dog("Buddy")

fido.add_trick("roll over")
buddy.add_trick("play dead")

print(fido.tricks)
print(buddy.tricks)

输出:

['roll over', 'play dead']
['roll over', 'play dead']

两个实例没有自己的 tricks,它们都通过属性查找找到同一个类属性列表。官方教程也把这种写法列为典型错误。(docs.python.org)

正确写法是把可变状态放到实例中:

class Dog:
    def __init__(self, name):
        self.name = name
        self.tricks = []

    def add_trick(self, trick):
        self.tricks.append(trick)


fido = Dog("Fido")
buddy = Dog("Buddy")

fido.add_trick("roll over")
buddy.add_trick("play dead")

print(fido.tricks)
print(buddy.tricks)

输出:

['roll over']
['play dead']

可以用身份检查直接诊断共享问题:

print(fido.tricks is buddy.tricks)

对于实例字段,预期结果应为:

False

类属性适合表达什么

类属性适合表达:

  • 所有实例共享的不可变配置;
  • 类型级别的常量;
  • 类级别的注册表;
  • 方法、属性描述器和其他行为定义。

例如:

class HttpRequest:
    allowed_methods = frozenset({"GET", "POST"})
    timeout = 5

但如果类属性是注册表、缓存或统计计数器,就必须明确它的生命周期和并发语义:

class Connection:
    active_count = 0

    def __init__(self):
        Connection.active_count += 1

这里使用 Connection.active_count 是有意的:计数属于类,而不是某个实例。如果写成 self.active_count += 1,读取和赋值的组合可能先读取类属性,再在实例上创建同名属性,从而把计数语义改变。


4. 属性查找:实例、类属性和描述器的真实优先级

简单模型可以记成:

obj.name
    1. obj 的实例属性
    2. obj 的类型及其 MRO 中的类属性
    3. 找不到时调用 __getattr__(如果定义了)

但完整规则还要考虑描述器。对于实例属性访问,数据描述器优先于实例字典;实例字典优先于非数据描述器和普通类属性;找不到后才可能进入 __getattr__()。属性查找的默认链条从实例字典开始,然后查找 type(obj) 及其基类,但描述器可以改变这条默认行为。(docs.python.org)

描述器是定义了 __get__()__set__()__delete__() 中至少一个方法的对象。定义了 __set__()__delete__() 的描述器称为数据描述器;只定义 __get__() 的通常是非数据描述器。(docs.python.org)

class Positive:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        return instance.__dict__.get(self.name, 0)

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("value must be non-negative")
        instance.__dict__[self.name] = value


class Account:
    balance = Positive()

    def __init__(self, balance):
        self.balance = balance

运行:

account = Account(100)
print(account.balance)

try:
    account.balance = -1
except ValueError as exc:
    print(type(exc).__name__, exc)

输出:

100
ValueError value must be non-negative

即使尝试写入:

account.__dict__["balance"] = -1

数据描述器仍然可以在正常的 account.balance 访问中优先介入。property 也是数据描述器,因此实例不能用同名实例字段绕过它。相反,普通方法是非数据描述器,实例字典中的同名属性可以遮蔽它。(docs.python.org)

class Greeter:
    def hello(self):
        return "class method"


greeter = Greeter()
greeter.hello = lambda: "instance function"

print(greeter.hello())

输出:

instance function

这也是一个边界:方法不是不可覆盖的“特殊成员”,它本质上参与属性绑定。生产代码中如果允许实例随意写入同名属性,可能导致某一个实例的行为与同类其他实例不同。

特殊方法还有一个额外边界。显式访问:

obj.__len__()

和隐式调用:

len(obj)

并不完全经过同一套查找路径。Python 为了正确性和性能,隐式特殊方法查找通常绕过实例级的 __getattribute__()。因此,给单个实例设置 __len__,通常不能改变 len(obj) 的结果;特殊方法应定义在类上。(docs.python.org)


5. 继承:扩展的是属性查找路径

继承表示一个类把另一个类作为基类。单继承中,如果子类自身没有某个属性,Python 会沿基类链继续查找:

class Animal:
    category = "animal"

    def speak(self):
        return "some sound"


class Dog(Animal):
    pass


dog = Dog()

print(dog.category)
print(dog.speak())
print(isinstance(dog, Dog))
print(isinstance(dog, Animal))
print(issubclass(Dog, Animal))

输出:

animal
some sound
True
True
True

派生类的实例化没有特殊语法:Dog() 创建的是 Dog 实例;继承影响的是方法和属性的解析。(docs.python.org)

isinstance()issubclass() 用于检查继承关系:

print(type(dog) is Animal)          # False
print(type(dog) is Dog)             # True
print(isinstance(dog, Animal))      # True

type(value) is Base 只接受精确类型;isinstance(value, Base) 会把子类实例也视为满足条件。官方文档明确建议在需要考虑子类时使用 isinstance()。(docs.python.org)

继承不等于复制

子类不会把父类方法复制到自己的 __dict__ 中:

class Animal:
    def speak(self):
        return "sound"


class Dog(Animal):
    pass


print("speak" in Dog.__dict__)
print("speak" in Animal.__dict__)
print(Dog.speak is Animal.speak)

输出:

False
True
True

Dog.speak 访问时沿 MRO 找到了 Animal.speak。因此,运行时修改父类属性可能影响尚未被子类自身遮蔽的行为:

Animal.speak = lambda self: "new sound"
print(Dog().speak())

这体现了 Python 类的动态性:类创建后仍然可以被修改。(docs.python.org)


6. 覆盖:替换行为、扩展行为与属性遮蔽

覆盖是子类在相同属性名下提供新的定义。最常见的是覆盖方法:

class Animal:
    def speak(self):
        return "sound"


class Dog(Animal):
    def speak(self):
        return "woof"


print(Animal().speak())
print(Dog().speak())

输出:

sound
woof

覆盖可能是完全替换,也可能是“先执行父类逻辑,再增加子类逻辑”。

class Parser:
    def parse(self, text):
        return text.strip()


class JsonParser(Parser):
    def parse(self, text):
        cleaned = super().parse(text)
        return cleaned.removeprefix("json:")


print(JsonParser().parse("  json:{ }  "))

输出:

{ }

super() 不是简单的“父类对象”。它表示:从当前类在实例 MRO 中的位置之后继续查找属性。对于 super(C, obj).method,查找会在 obj.__class__.__mro__ 中找到 C 之后的类,并对找到的属性执行绑定。(docs.python.org)

因此,下面两种写法语义不同:

class Base:
    def run(self):
        return ["Base"]


class Child(Base):
    def run(self):
        return super().run() + ["Child"]

以及:

class Child(Base):
    def run(self):
        return Base.run(self) + ["Child"]

在单继承中结果通常相同,但在多继承中,显式写 Base.run(self) 会固定调用某个类;super() 则遵循 MRO,允许后续类继续参与协作调用。

父类方法中的动态分派

父类方法调用 self.some_method() 时,查找对象仍然是当前实例,而不是“锁定父类”。因此,父类方法可能调用到子类覆盖的方法:

class Base:
    def run(self):
        return "Base -> " + self.step()

    def step(self):
        return "base step"


class Child(Base):
    def step(self):
        return "child step"


print(Child().run())

输出:

Base -> child step

这说明 Python 的实例方法通常是动态分派的:父类方法不拥有对子类覆盖行为的豁免权。官方教程特别指出,基类方法调用同一对象上的另一个方法时,最终可能调用到派生类的覆盖版本。(docs.python.org)

这既是扩展点,也是故障来源。例如父类初始化过程中调用一个可被子类覆盖的方法:

class Base:
    def __init__(self):
        self.prepare()

    def prepare(self):
        self.ready = True


class Child(Base):
    def prepare(self):
        print(self.required)  # 此时可能尚未初始化
        self.ready = True
Child()

可能失败:

AttributeError: 'Child' object has no attribute 'required'

原因不是继承“失效”,而是父类构造阶段发生了动态分派,子类方法在子类状态尚未准备好时被调用。


7. __init__() 的继承边界

如果子类没有定义 __init__(),它会继承父类的初始化方法:

class User:
    def __init__(self, name):
        self.name = name


class Admin(User):
    pass


admin = Admin("Alice")
print(admin.name)

但只要子类定义了自己的 __init__(),父类初始化就不会自动执行:

class Admin(User):
    def __init__(self, name, level):
        self.level = level


admin = Admin("Alice", 10)
print(admin.__dict__)

输出:

{'level': 10}

此时 name 没有创建。若子类需要父类负责的状态,必须显式调用:

class Admin(User):
    def __init__(self, name, level):
        super().__init__(name)
        self.level = level

这不是“父类构造函数自动链式调用”的语言规则,而是子类代码主动调用的结果。

一个更隐蔽的错误是参数不协作:

class A:
    def __init__(self, name):
        self.name = name


class B(A):
    def __init__(self, name, level):
        super().__init__(name)
        self.level = level


class C(B):
    def __init__(self, name, level, region):
        super().__init__(name, level)
        self.region = region

单继承下这样可以工作,但如果未来把多个类组合进协作式多继承体系,就需要所有参与者遵守统一的参数传递协议。否则常见失败路径是:

TypeError: __init__() missing 1 required positional argument

或者某个类接收了它没有预期的关键字参数。


8. 组合:通过对象引用表达“拥有”和“协作”

组合不是继承关系,而是一个对象持有另一个对象,并把任务委托给它。

class Engine:
    def start(self):
        return "engine started"


class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

使用:

car = Car(Engine())
print(car.start())

输出:

engine started

这里:

Car ──持有──> Engine
Car ──委托──> Engine.start()

组合与继承表达不同关系:

  • 继承:Dog 是一种 Animal
  • 组合:Car 拥有或使用一个 Engine
  • 继承复用的是类型关系和属性查找;
  • 组合复用的是对象协作和明确接口。

组合通常更容易替换实现:

class FakeEngine:
    def start(self):
        return "fake engine"


car = Car(FakeEngine())
print(car.start())

输出:

fake engine

Car 不需要继承 Engine,只要依赖对象提供所需的 start() 方法即可。这种设计利用的是 Python 的行为兼容性,而不是名义上的父子类型关系。

组合并不会自动复制状态

class Engine:
    def __init__(self):
        self.started = False

    def start(self):
        self.started = True


class Car:
    def __init__(self, engine):
        self.engine = engine
engine = Engine()
car = Car(engine)

car.engine.start()
print(engine.started)

输出:

True

car.engine 和变量 engine 指向同一个对象。组合建立的是引用关系,不是复制关系。若多个对象共享同一个组件,就必须考虑共享状态、并发访问和组件生命周期。

组合和继承的选择边界

当子类必须满足父类所承诺的接口,并且“替代父类实例”在语义上成立时,继承才有较强理由:

def render(item):
    print(item.render())

MarkdownDocumentDocument 的一种,可以考虑继承。

但如果对象只是需要一个可替换组件:

class Report:
    def __init__(self, formatter):
        self.formatter = formatter

    def export(self, data):
        return self.formatter.format(data)

那么组合通常更直接。Report 并不是 Formatter,它只是使用 Formatter


9. 多继承与 MRO:覆盖顺序不是“简单的从左到右”

Python 支持多个基类:

class A:
    def run(self):
        return ["A"]


class B(A):
    def run(self):
        return super().run() + ["B"]


class C(A):
    def run(self):
        return super().run() + ["C"]


class D(B, C):
    def run(self):
        return super().run() + ["D"]

查看顺序:

print([cls.__name__ for cls in D.__mro__])
print(D().run())

输出:

['D', 'B', 'C', 'A', 'object']
['A', 'C', 'B', 'D']

调用路径是:

D.run
  └─ super() -> B.run
                  └─ super() -> C.run
                                  └─ super() -> A.run
                                                  └─ super() -> object

关键点是:super() 的下一个类不是“当前类的父类列表中的第一个父类”,而是当前类在完整 MRO 中的后继类。

C3 线性化

Python 使用 C3 方法解析顺序。对类 C(B1, B2, ..., Bn),其线性化可以写成:

L[C] = [C] + merge(L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn])

其中:

  • L[X] 表示类 X 的线性化;
  • merge 负责合并多个已经排序的列表;
  • 每一步选择一个“合法头部”;
  • 一个候选类只有在它不出现在其他列表的尾部时,才能被选中;
  • 选中后从相关列表移除,继续下一步。

D(B, C) 为例:

L[A] = [A, object]
L[B] = [B, A, object]
L[C] = [C, A, object]

L[D] = [D] + merge(
    [B, A, object],
    [C, A, object],
    [B, C]
)

逐步合并:

候选 B:
B 不在其他列表的尾部,选中 B
结果:[D, B]

剩余:
[A, object]
[C, A, object]
[C]

候选 A:
A 出现在 [C, A, object] 的尾部,不能选

候选 C:
C 不在其他列表的尾部,选中 C
结果:[D, B, C]

剩余:
[A, object]
[A, object]

继续:

选中 A
结果:[D, B, C, A]

最后选中 object
结果:[D, B, C, A, object]

所以:

D.__mro__

得到:

(D, B, C, A, object)

C3 需要同时保持几个性质:

  1. 子类优先于基类;
  2. 保持基类列表中声明的局部优先顺序;
  3. 不重复访问同一个祖先;
  4. 保持单调性:增加子类不应任意改变已有父类之间的相对顺序。

官方数据模型说明 Python 的基类查找使用 C3 MRO;官方 MRO HOWTO 则给出了 merge 的形式化定义和推导方式。(docs.python.org)

不一致的继承图会在类定义阶段失败

class A:
    pass


class B(A):
    pass


class C(A):
    pass


class Bad(B, A, C):
    pass

这类基类顺序无法同时满足一致的局部优先级,Python 会拒绝创建类,通常抛出:

TypeError: Cannot create a consistent method resolution
order (MRO) for bases ...

故障发生在 class Bad(...) 执行时,而不是创建 Bad() 实例时。诊断多继承问题的第一步通常是查看:

print(D.__mro__)
print(D.mro())
print(D.__bases__)

如果类根本无法创建,就需要回到基类声明,检查是否存在互相冲突的顺序约束。


10. 协作式多继承:每一层都必须“继续传递”

多继承中,推荐的协作式方法通常满足两个条件:

  1. 每个实现都调用 super()
  2. 每个实现都遵守兼容的参数和返回值协议。
class Base:
    def process(self, value):
        return [value]


class AddOne(Base):
    def process(self, value):
        result = super().process(value)
        return result + [value + 1]


class AddTwo(Base):
    def process(self, value):
        result = super().process(value)
        return result + [value + 2]


class Pipeline(AddOne, AddTwo):
    pass


pipeline = Pipeline()
print([cls.__name__ for cls in Pipeline.__mro__])
print(pipeline.process(10))

输出:

['Pipeline', 'AddOne', 'AddTwo', 'Base', 'object']
[10, 12, 11]

调用顺序是:

AddOne.process
  -> AddTwo.process
       -> Base.process

而不是:

AddOne.process
  -> Base.process

如果 AddOne 改成显式调用 Base.process(self, value)AddTwo 就会被绕过:

class AddOne(Base):
    def process(self, value):
        result = Base.process(self, value)
        return result + [value + 1]

这说明直接写父类名和使用 super() 的边界不同:

  • 直接父类调用:固定目标,适合明确绕过协作链的特殊场景;
  • super():遵守 MRO,适合设计为可组合的多继承实现。

super() 也可以用于属性查找,而不只是调用方法;零参数形式由编译器结合当前类和实例提供上下文,但在嵌套函数或生成器表达式中不能误认为仍然处于同一个方法调用环境。(docs.python.org)


11. 类属性覆盖与实例属性遮蔽并不是一回事

“覆盖”至少有三种容易混淆的形式。

11.1 子类覆盖父类类属性

class Animal:
    category = "animal"


class Dog(Animal):
    category = "dog"


print(Dog.category)
print(Dog().__class__.category)

Dog.__dict__ 中有自己的 category,因此查找不会继续到 Animal.category

11.2 实例属性遮蔽类属性

class Animal:
    category = "animal"


dog = Animal()
dog.category = "temporary"

print(dog.category)
print(Animal.category)

这是实例级遮蔽,不是子类覆盖。它只影响 dog,不影响其他实例,也不修改类。

11.3 数据描述器阻止实例遮蔽

class ReadOnly:
    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        return 42

    def __set__(self, instance, value):
        raise AttributeError("read-only")


class Item:
    value = ReadOnly()


item = Item()

try:
    item.value = 100
except AttributeError as exc:
    print(type(exc).__name__, exc)

输出:

AttributeError read-only

因此,不能只看 obj.__dict__ 判断最终的 obj.name 值。描述器可能在实例字典之前或之后介入,具体取决于它是数据描述器还是非数据描述器。(docs.python.org)


12. 继承中的名称改写不是真正的私有

Python 没有不可访问的私有实例变量。以两个下划线开头的名称会执行名称改写,主要目的是避免子类意外覆盖父类内部名称;它不是访问控制机制。(docs.python.org)

class Parent:
    def __init__(self):
        self.__value = "parent"

    def get_parent_value(self):
        return self.__value


class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__value = "child"


child = Child()

print(child.get_parent_value())
print(child.__dict__)

输出:

parent
{'_Parent__value': 'parent', '_Child__value': 'child'}

Parent.__value 被改写为 _Parent__valueChild.__value 被改写为 _Child__value。这避免了简单的同名冲突,但并不能防止有意访问:

print(child._Parent__value)

单下划线名称则只是约定:

class Service:
    def _internal_step(self):
        pass

它表达“非公共 API”,不会阻止访问。


13. 继承关系的边界:不是所有相似对象都应成为子类

继承关系需要稳定的替代语义。若函数声明接收某个基类实例:

def save(document):
    document.render()
    document.write()

那么一个子类如果继承 Document,却不能正常提供 render()write(),就会在运行时破坏调用方预期。

一个典型错误是只为了复用一段代码而继承:

class CsvReport(SomeUnrelatedUtility):
    pass

如果 CsvReport 并不代表 SomeUnrelatedUtility 的一种,那么继承会把不需要的属性、初始化流程和类型关系暴露给调用方。此时组合、普通函数或独立工具对象通常更准确。

另一个边界是过深的继承层次。每增加一级继承,就增加一层属性查找和初始化协作关系;当父类方法依赖可覆盖方法、类属性可以被实例遮蔽、多个基类还要通过 super() 协作时,问题的因果链会变长。代码仍然可能正确,但诊断成本会明显提高。


14. 并发和共享状态:类属性会扩大状态作用域

实例属性通常把状态隔离到一个对象;类属性则把状态扩大到整个类及其实例。若类属性是可变对象,多个线程或任务可能同时读写它:

class Metrics:
    values = []

这不仅是“是否共享”的问题,还涉及:

  • 谁拥有这个列表;
  • 何时创建和清空;
  • 是否允许并发追加;
  • 读者是否能看到中间状态;
  • 测试之间是否会相互污染;
  • 子类是否共享同一个列表,还是重新绑定了自己的列表。

例如:

class Base:
    events = []


class Child(Base):
    pass


print(Base.events is Child.events)

输出:

True

子类没有重新定义 events 时,查找会得到同一个列表。若子类重新绑定:

class SeparateChild(Base):
    events = []

则:

print(Base.events is SeparateChild.events)

输出:

False

这里的 is 检查的是对象身份,而不是内容相等。对象身份、类型和值是 Python 对象模型的三个基本维度;可变对象的别名关系会直接影响程序状态。(docs.python.org)

生产代码中,若状态属于实例,就在 __init__() 中创建;若状态确实属于类,就明确使用类名访问,并为并发访问设计同步或不可变替换策略。后者是工程约束,不是 Python 类属性机制自动提供的保证。


15. 一套可执行的诊断流程

遇到“为什么这个属性来自父类”“为什么两个对象互相影响”“为什么 super() 调到了意外的方法”时,可以按以下顺序建立证据。

第一步:确认对象和类型

print(obj)
print(type(obj))
print(obj.__class__)

不要只凭变量名推断对象类型。

第二步:查看实例字典

print(getattr(obj, "__dict__", None))

如果目标属性不在实例字典中,它可能来自类、基类或描述器。

第三步:检查每一级类字典

name = "value"

for cls in type(obj).__mro__:
    print(cls.__name__, cls.__dict__.get(name, "<missing>"))

这能区分:

  • 实例没有属性;
  • 当前类定义了属性;
  • 某个基类定义了属性;
  • 某一级定义了描述器;
  • 名称根本不存在。

第四步:检查 MRO

print(type(obj).__mro__)

多继承中,不要用“从左到右递归搜索”的简化模型代替真实 MRO。

第五步:检查属性身份

print(obj.value is type(obj).value)

这可以帮助判断实例访问得到的是否是同一个对象,但遇到描述器时要谨慎,因为 obj.valuetype(obj).value 可能是描述器绑定后的不同结果。

第六步:区分显式和隐式特殊方法

如果问题涉及 len(obj)obj + other、迭代或比较,不要只检查实例上的同名属性。特殊方法的隐式查找通常绕过实例级属性访问。(docs.python.org)


16. 组合、继承和覆盖的统一理解

可以把几种机制放在同一张图中:

flowchart TD
    I[实例 obj] --> D[obj.__dict__]
    I --> T[type(obj)]
    T --> M[type(obj).__mro__]
    M --> C1[当前类]
    M --> C2[基类]
    M --> O[object]

    I -->|组合引用| P[协作对象 component]
    P --> API[component 的接口]

    C1 -->|同名定义| OV[覆盖/遮蔽]
    C2 -->|继承提供| IN[继承属性]

关键路径是:

  1. 实例访问属性时,先涉及实例状态;
  2. 如果实例没有直接提供,再沿类型的 MRO 查找;
  3. 描述器可以改变普通字典查找的优先级;
  4. 子类同名定义会覆盖基类定义;
  5. super() 从当前类之后继续沿 MRO 查找;
  6. 组合不改变 Car 的 MRO,而是让 Car 通过字段引用另一个对象;
  7. 可变类属性可能让多个实例共享同一状态。

因此:

  • 实例回答“这份状态属于谁”;
  • 类属性回答“哪些对象通过查找可以共享这个定义或值”;
  • 继承回答“查找失败后沿哪条类型链继续寻找”;
  • 覆盖回答“同名定义优先由谁提供”;
  • 组合回答“一个对象如何把工作交给另一个对象”;
  • 边界出现在共享可变状态、初始化顺序、动态分派、描述器、多继承和特殊方法查找之间。

当这些关系需要表达的是稳定的类型替代,就使用继承;当需要表达的是可替换的协作对象,就使用组合;当需要扩展既有行为时,先判断是完全替换、显式父类调用,还是遵循 MRO 的 super() 协作。理解属性查找和对象身份后,类与继承就不再是孤立的语法特性,而是 Python 对象模型中一组相互连接的运行时规则。


系列导航与关联阅读

官方资料

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