类把对象的状态和相关操作放在一起。实例属性记录每个对象自己的数据;类属性可被实例共同查到;方法则通过绑定把实例或类传给函数。掌握这几种关系后,继承、属性约定和 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 firstassert first.describe.__func__ is Record.describeassert 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.categoryfirst.category = "special"assert first.category == "special"assert second.category == Record.category == "measurement"del first.categoryassert 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.entriesassert 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 Voltageassert item.doubled == 7.0assert Sample.is_nonnegative(1) and not item.is_nonnegative(-1)try: item.doubled = 0except AttributeError: passelse: raise AssertionError("read-only property accepted assignment")item.value = 4assert item.doubled == 8print("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: passelse: 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_onlyexcept AttributeError: passelse: 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 说明。对象协议、属性钩子与可调用实例继续见 特殊方法与数据模型。