跳转到内容
新建笔记

Python 类、实例与协作继承

类把对象的状态和相关操作放在一起。实例属性记录每个对象自己的数据;类属性可被实例共同查到;方法则通过绑定把实例或类传给函数。掌握这几种关系后,继承、属性约定和 super() 的行为才容易解释。

本文以 Python 3.11 为基线,每段代码都是可以独立运行的程序。示例中的 assert 用于核对结果,不要使用会禁用断言的 -O。

一个完整的类从哪里开始

跳转到“一个完整的类从哪里开始”

类定义使用半角冒号和一致的缩进。__init__ 初始化已经创建的实例,通常不显式返回结果;self 是约定的参数名,不是关键字。调用 record.describe() 时,绑定方法会提供实例参数;调用 Record.describe(record) 时则由代码显式提供。

class Record:
category = "measurement"
def __init__(self, name, *samples, **metadata):
self.name = name
self.samples = list(samples)
self.metadata = dict(metadata)
def describe(self):
return f"{self.name}: {len(self.samples)} samples"
first = Record("voltage", 3.2, 3.3, unit="V")
second = Record("current", 0.5, unit="A")
assert first.describe() == "voltage: 2 samples"
assert Record.describe(first) == first.describe()
assert first.describe.__self__ is first
assert first.describe.__func__ is Record.describe
assert first.samples == [3.2, 3.3]
assert first.metadata == {"unit": "V"}
first.samples.append(3.4)
assert second.samples == [0.5]
assert first.category == second.category == Record.category
first.category = "special"
assert first.category == "special"
assert second.category == Record.category == "measurement"
del first.category
assert first.category == "measurement"
print("instance and binding checks passed")

这里 *samples 收集额外位置参数,**metadata 收集关键字参数;原文中的 **args, **kwargs 不能声明两个任意关键字收集参数。代码也明确初始化了之后要使用的属性,避免 attribute、attribute1、value 各指不同名字。

访问 first.category 可以查到类上的值;给普通实例的这个名字赋值后,实例自己的属性会遮蔽类属性。删除这个实例属性后,又可以查到类上的值。属性描述器可能改变读写行为,因此这是普通属性场景的说明,不能拿来推断所有 property 的赋值规则。类与方法

类属性中的可变对象确实会共享

跳转到“类属性中的可变对象确实会共享”

希望每个实例各自拥有一个列表时,在 __init__ 中创建。若把列表直接放在类体中,并通过实例访问它后调用 append,修改的是被共同引用的那个列表。

class SharedLog:
entries = []
left = SharedLog()
right = SharedLog()
left.entries.append("shared")
assert left.entries is right.entries is SharedLog.entries
assert right.entries == ["shared"]
left.entries = ["local"]
assert left.entries == ["local"]
assert right.entries == ["shared"]
class OwnLog:
def __init__(self):
self.entries = []
first = OwnLog()
second = OwnLog()
first.entries.append("first only")
assert first.entries == ["first only"] and second.entries == []
print("class-state sharing checks passed")

不要把“修改共享对象”和“给实例重新绑定属性”混成一件事。共享缓存、注册表可能有意使用类属性;每个对象的历史记录通常更适合实例属性。

实例方法、类方法、静态方法和 property

跳转到“实例方法、类方法、静态方法和 property”
写法自动提供的参数常见用途
类体中的普通函数,通过实例访问该实例,通常命名为 self操作实例状态
@classmethod实际调用涉及的类,通常命名为 cls保留子类类型的替代构造方式
@staticmethod没有额外绑定参数与类有关、但不依赖实例或类状态的函数
@property读取属性时调用 getter用属性语法表达计算结果或受控访问

下面让类方法通过 cls(...) 创建对象,观察继承后仍然返回子类实例。

class Sample:
def __init__(self, value):
self.value = float(value)
@classmethod
def from_text(cls, text):
return cls(float(text))
@staticmethod
def is_nonnegative(value):
return value >= 0
@property
def doubled(self):
return self.value * 2
class Voltage(Sample):
pass
item = Voltage.from_text("3.5")
assert type(item) is Voltage
assert item.doubled == 7.0
assert Sample.is_nonnegative(1) and not item.is_nonnegative(-1)
try:
item.doubled = 0
except AttributeError:
pass
else:
raise AssertionError("read-only property accepted assignment")
item.value = 4
assert item.doubled == 8
print("method-kind and property checks passed")

只提供 getter 的 property 拒绝对该属性名直接赋值,并不自动使整个实例不可变。上例的 value 仍然可以修改。需要约束值时,可另外实现 setter 或验证方法。

_name 通常表示非公开接口,是调用约定。类定义中的 __name 会触发名称改写,用来降低基类与子类的名字冲突;它不是不可访问、不可修改的安全边界。以双下划线结尾的特殊名称,如 __init__,也不能按同样方式推断为“私有”。

class Parent:
def __init__(self):
self.public_value = "public"
self._internal_value = "internal"
self.__value = "parent"
def parent_value(self):
return self.__value
def __describe(self):
return "parent method"
def describe(self):
return self.__describe()
class Child(Parent):
def __init__(self):
super().__init__()
self.__value = "child"
def child_value(self):
return self.__value
item = Child()
assert item.public_value == "public"
assert item._internal_value == "internal"
assert item.parent_value() == "parent" and item.child_value() == "child"
assert item.describe() == "parent method"
assert "_Parent__value" in vars(item) and "_Child__value" in vars(item)
try:
getattr(item, "__value")
except AttributeError:
pass
else:
raise AssertionError("a string name was unexpectedly mangled")
item._Parent__value = "changed"
assert item.parent_value() == "changed"
assert item._Parent__describe() == "parent method"
assert item.child_value() == "child"
print("name-mangling checks passed")

这段代码特意展示了外部访问改写名称的可能性,用于纠正“只能在类内使用、不能修改”的说法;常规调用方仍应使用公开接口。改写针对类定义中的标识符,getattr(item, "__value") 的字符串不会自动替换成 _Parent__value。类名开头的下划线还会影响改写前缀,规则见 名称改写。

super 的起点是类,绑定的是实例或类

跳转到“super 的起点是类,绑定的是实例或类”

在普通实例方法中,零参数 super() 通常对应 super(当前定义方法的类, self)。显式形式的第一个参数是类,不是 self。它创建代理,沿运行时方法解析顺序 MRO,从指定类之后继续查找属性。

先看单继承:子类可以转交经过变换的参数,再补充自己的处理。

class Parent:
label = "parent class attribute"
def __init__(self):
self.instance_only = "created in parent init"
def foo(self, value):
return [f"parent:{value}"]
class Child(Parent):
def __init__(self):
super().__init__()
def foo(self, value):
return super().foo(value * 2) + [f"child:{value}"]
def inherited_label(self):
return super().label
item = Child()
assert item.foo(3) == ["parent:6", "child:3"]
assert item.inherited_label() == "parent class attribute"
assert item.instance_only == "created in parent init"
assert super(Child, item).foo(4) == ["parent:4"]
try:
super(Child, item).instance_only
except AttributeError:
pass
else:
raise AssertionError("super unexpectedly searched the instance dictionary")
print("single-inheritance super checks passed")

父类初始化给实例添加的数据,通过 self.instance_only 使用。super() 的属性查找不等于对实例字典做一次普通查询,所以不能因为属性在父类初始化中写入,就认定 super().instance_only 一定可用。super

多重继承需要各层协作

跳转到“多重继承需要各层协作”

super() 不会自动调用所有父类,也不能概括为每次都从左到右扫描直接基类。下面的 Diamond 的 MRO 是 Diamond → Left → Right → Root → object;Left 中的 super() 因而会进入 Right。

class Root:
def __init__(self, *, name, **kwargs):
super().__init__(**kwargs)
self.name = name
self.initialized = ["Root"]
def steps(self):
return ["Root"]
class Left(Root):
def __init__(self, *, left, **kwargs):
super().__init__(**kwargs)
self.left = left
self.initialized.append("Left")
def steps(self):
return super().steps() + ["Left"]
class Right(Root):
def __init__(self, *, right, **kwargs):
super().__init__(**kwargs)
self.right = right
self.initialized.append("Right")
def steps(self):
return super().steps() + ["Right"]
class Diamond(Left, Right):
pass
item = Diamond(name="sensor", left=1, right=2)
assert [cls.__name__ for cls in Diamond.__mro__] == [
"Diamond", "Left", "Right", "Root", "object"
]
assert item.initialized == ["Root", "Right", "Left"]
assert item.steps() == ["Root", "Right", "Left"]
assert (item.name, item.left, item.right) == ("sensor", 1, 2)
assert super(Left, item).steps() == ["Root", "Right"]
try:
Diamond(name="sensor", left=1, right=2, typo=3)
except TypeError:
pass # The unconsumed keyword eventually reaches object.__init__.
else:
raise AssertionError("unconsumed constructor argument was ignored")
print("cooperative-MRO checks passed")

调用向后传递,返回结果再逐层拼接,所以结果列表的次序与 MRO 的书写方向不同。这个例子里每层只消费自己的关键字参数,将其余参数继续传递;Root 最终把剩余参数交给 object,从而暴露未消费的拼写错误。若某一层直接返回而不继续调用 super(),链条就在那一层停止。

真实继承体系必须约定兼容的参数和返回值,而不是机械地在每个方法里添加 super()。尤其不要在同一条协作链中又显式调用某个祖先初始化,否则可能重复执行祖先操作。Python 的 C3 方法解析规则见 MRO 说明。对象协议、属性钩子与可调用实例继续见 特殊方法与数据模型。