主题
05 面向对象编程
一句话:类把状态和行为绑在一起,特殊方法让对象接入语言语法;实例方法收
self,类方法收cls,静态方法不收隐式参数,三者职责不能混。
适用:CPython 3.11+。dataclass 需要 3.7+。
为什么需要
零散变量和参数会让调用方承担所有约束;类把状态和「必须同时修改」的规则收进方法。继承用于表达可替换接口,多态让调用方只依赖行为;但继承层次过深会把耦合藏起来,所以工程上优先组合。特殊方法解决另一个问题:让自定义对象像内建类型一样支持 +、len()、下标和 with。
核心机制
类属性、实例属性与属性查找
python
class Model:
version = "v1" # 类属性:所有实例共享
def __init__(self, name):
self.name = name # 实例属性:每个实例独立
m = Model("mlp")
print(m.name, m.version) # mlp v1
m.version = "v2" # 实例上新建属性,不改类属性
print(m.version, Model.version) # v2 v1属性查找先找实例 __dict__,再沿类与 MRO 找。类属性是共享默认值;可变类属性(如 registry = [])也是共享的,想让每个实例独立,必须在 __init__ 里赋值。
三种方法:self、cls 与静态方法
python
class Dataset:
total = 0
def __init__(self, name):
self.name = name
type(self).total += 1 # 用实际类,避免子类共用基类计数
def describe(self): # 实例方法:第一个参数 self
return self.name
@classmethod
def from_name(cls, name): # 类方法:第一个参数 cls,收到的是类
return cls(name)
@staticmethod
def valid_size(n): # 静态方法:没有隐式参数
return n > 0
d = Dataset.from_name("train")
print(d.describe(), Dataset.valid_size(10), Dataset.total) # train True 1| 类型 | 第一个参数 | 绑定到 | 能做什么 | 典型用途 |
|---|---|---|---|---|
| 实例方法 | self | 实例 | 访问实例属性、类属性 | 常规行为 |
| 类方法 | cls | 类 | 访问类属性;cls(...) 构造 | 备选构造函数、类级状态 |
| 静态方法 | 无 | 无 | 只能用显式传入的参数 | 与类相关的纯工具函数 |
- 实例方法通过实例调用,
m.f(x)等价于Model.f(m, x);通过类调用时必须显式传实例。 - 类方法通过类或实例都能调用,但第一个参数始终是类;子类调用
Sub.from_name()时cls是Sub。它没有self,不能直接访问某个实例的属性。 - 静态方法既不收
self也不收cls,放在类里只是为了命名空间。
继承、MRO 与组合
python
class Base:
def predict(self, x): raise NotImplementedError
class Tree(Base):
def predict(self, x): return "tree"
class Net(Base):
def predict(self, x): return "net"
print(Tree().predict(1), Net().predict(1)) # tree net多态靠「有同名方法」即可,不要求共同基类(鸭子类型)。多继承按 C3 线性化确定 MRO,super() 沿 MRO 走,不是简单「找父类」。工程上优先组合:把能力做成独立对象持有,而不是堆继承。确需多继承时,mixin 只放行为、不放状态,并用 D.__mro__ 验证顺序。
特殊方法:让对象接入语言
python
class Vector:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Vector({self.x}, {self.y})"
def __add__(self, o): return Vector(self.x + o.x, self.y + o.y)
v = Vector(1, 2)
print(v + v) # Vector(2, 4)__repr__ 面向调试,__str__ 面向用户;__len__、__getitem__ 分别接入 len() 和下标;__enter__/__exit__ 支持 with(06)。__eq__ 定义后 __hash__ 会被设为 None,对象不再可哈希,需要放进 set/dict 时要同时实现 __hash__。
property 与 dataclass
python
from dataclasses import dataclass, field
class Temperature:
def __init__(self, c): self._c = c
@property
def celsius(self): return self._c
@dataclass
class Config:
name: str = "bert"
tags: list[str] = field(default_factory=list) # 可变默认值必须用 field
print(Temperature(25).celsius, Config(tags=["nlp"]))property 把校验逻辑藏进属性访问,调用方仍写 t.celsius;dataclass 自动生成 __init__、__repr__、__eq__,适合配置和 DTO。可变默认值必须用 field(default_factory=...),原因和 04 的可变默认参数相同。
最小上手
保存为 oop.py 后运行,预期输出 train:100 0 True:
python
from dataclasses import dataclass, field
@dataclass
class Dataset:
name: str
size: int
tags: list[str] = field(default_factory=list)
def describe(self): return f"{self.name}:{self.size}"
@classmethod
def from_name(cls, name): return cls(name, 100)
@staticmethod
def valid(size): return size > 0
d = Dataset.from_name("train")
print(d.describe(), len(d.tags), Dataset.valid(10))生产动作
- 状态与构造:实例状态放
__init__,类级常量放类属性;可变类属性要么避免,要么明确它就是共享状态;需要多种构造方式时用@classmethod。 - 继承:优先组合;多继承只用于 mixin,职责单一;
super().__init__()不要漏。 - 排障三段:
- 现象:改了
a的属性,b也变 → 命令:print(a is b)、检查类属性是否可变 → 判断:是否共享了类级可变对象。 - 现象:子类计数把基类也算进去 → 命令:
print(type(self), cls)→ 判断:是否硬编码了基类名。 - 现象:
TypeError: unhashable type→ 命令:检查__eq__与__hash__→ 判断:对象放进 set/dict 前需要可哈希。
- 现象:改了
边界
- 类不是模块化的唯一手段;无状态工具函数放模块级更简单。多继承和元类在业务代码里很少需要。
dataclass不做运行时类型校验;需要校验用 Pydantic v2(09)。
讲清楚
类是状态和行为的封装:实例方法收 self、绑定具体对象;类方法收 cls、绑定到类,适合备选构造函数和类级状态;静态方法没有隐式参数,只是挂在类命名空间下的工具函数。三者混淆的典型后果是:在类方法里访问实例属性、在静态方法里访问类变量、子类计数写死基类名。继承表达可替换行为,多态靠鸭子类型,但工程上优先组合。特殊方法让对象支持 +、len()、下标和 with,property 与 dataclass 则把校验和样板代码收进声明。
适用 CPython 3.11+ | 初稿 2026-08,2026-10 压缩并订正三种方法