一切皆对象 — type 与元类、__new__/__init__、C3 线性化、dataclass 与魔术方法协议: 对象行为由协议决定
Python 的对象模型只有一条主规则: 一切皆对象, 行为由协议决定。实例的 type 是类, 类的 type 是 type (元类) — 类自己也是可传递、可修改的对象; 取属性沿"实例 __dict__ → 类的 MRO"查找, 而 len()/下标/加号/in/for 这些语法糖全都翻译成对应的魔术方法 — 你实现协议, 语言就白送你整套能力。
继承体系的核心是 MRO (C3 线性化): super() 的含义是"MRO 里的下一个"而不是"父类", 菱形继承里公共基类只被经过一次。日常工程把机制用在刀刃上: dataclass 定 DTO、__slots__ 省内存、ABC 定契约、Mixin 组合能力 — 能用组合表达的就不动继承, 继承是最强的耦合。
__new__ 造实例 (分配内存, 返回实例, 类方法), __init__ 初始化 (拿到的实例填字段, 不返回); 单例与不可变类型的子类化要在 __new__ 里做文章。
class Single: _inst = None def __new__(cls): if cls._inst is None: # 关键: __new__ 分配并返回实例 cls._inst = super().__new__(cls) return cls._inst # __init__ 只负责填字段, 拿到的正是 __new__ 的返回值
obj.x: 数据描述符 (property) → 实例 __dict__ → 沿 MRO 查类字典; 而赋值 obj.x = v 永远写实例字典 (描述符除外)。
class C: @property def x(self): return self._x # 数据描述符, 优先级最高 c = C(); c._x = 1 c.__dict__["x"] = 99 # 实例字典同名也抢不过 print(c.x) # → 1 描述符 > 实例 __dict__ > 类
class A: ... class B(A): ... class C(A): ... class D(B, C): ... print([k.__name__ for k in D.__mro__]) # → ['D', 'B', 'C', 'A', 'object'] B 声明在前, A 只出现一次
class B: def __init__(self): super().__init__(); print("B") class C: def __init__(self): super().__init__(); print("C") class D(B, C): def __init__(self): super().__init__() # MRO 里的下一个 D() # → 先打印 C 再 B: B 里的 super() 走到 C, 不是父类 object
__init__/__repr__/__eq__; frozen=True 不可变且可哈希; field(default_factory=list) 防可变默认值共享 — DTO/配置对象的首选。
@dataclass(frozen=True) class Conf: name: str tags: list = field(default_factory=list) # 每实例独立 print(Conf("a"), Conf("a") == Conf("a")) # → Conf(name='a', tags=[]) True init/repr/eq 全自动
__dict__: 海量小对象省 ~50% 内存, 属性访问略快; 代价是不能动态加属性 (这是特性也是限制)。
class P: __slots__ = ("x", "y") # 定长槽, 实例无 __dict__ p = P(); p.x = 1 p.z = 2 # → AttributeError: 'P' object has no attribute 'z'
__eq__ 而不定义 __hash__, 类自动变 unhashable, 实例进不了 set/当不了 dict key; 相等对象的哈希必须一致。
class T: def __init__(self, v): self.v = v def __eq__(self, o): return self.v == o.v {T(1)} # → TypeError: unhashable type: 'T' # 对: def __hash__(self): return hash(self.v) 成对实现
class Temp: @property def c(self): return self._c @c.setter def c(self, v): if v < -273: raise ValueError("below absolute zero") self._c = v # 写走 setter, 顺带校验
@abstractmethod 声明接口, 子类没实现就在实例化时抛 TypeError — 把"缺方法"的报错从运行期提到启动期。
class Storage(ABC): @abstractmethod def get(self, key): ... class Redis(Storage): ... # 漏实现 get Redis() # → TypeError: Can't instantiate abstract class Redis
@dataclass class Task: id: str; attempt: int = 0 t = Task("t-91", 3) print([t]) # → [Task(id='t-91', attempt=3)] 容器打印走 repr # 手写可输出 Task(id='t-91', try=3) — 排障字段自己挑
class JSONMixin: # 窄能力: 只做导出 def to_json(self): return json.dumps(self.__dict__) class Report(JSONMixin): def __init__(self, oid): self.oid = oid Report("r-7").to_json() # → '{"oid": "r-7"}' 声明能力而非 is-a
__init_subclass__ 钩子就够了。
class Meta(type): def __new__(mcls, name, bases, ns): print("造类", name) # 类创建时执行一次 return super().__new__(mcls, name, bases, ns) class S(metaclass=Meta): ... # → 造类 S (type 是默认元类)
__len__、[]→__getitem__、+→__add__、in→__contains__; 协议满足即获得语言能力, 这是鸭子类型的根基。
class Bag: def __len__(self): return 2 def __getitem__(self, i): return "ab"[i] b = Bag() print(len(b), b[0], "a" in b, list(b)) # → 2 a True ['a', 'b']
class Engine: def start(self): return "vroom" class Car: # has-a: 组合 + 窄接口 def __init__(self): self.engine = Engine() def start(self): return self.engine.start() Car().start() # → vroom
DTO 在服务边界被几十个函数传递, 谁都能改一笔就是隐患。frozen 让写入当场炸, 可变默认值交给工厂:
from dataclasses import dataclass, field @dataclass(frozen=True, slots=True) # frozen: 下游偷改订单直接炸 class OrderDTO: order_id: str items: tuple[Item, ...] # 不可变集合配 frozen tags: frozenset = frozenset() # 不可变类型才配当默认值 meta: dict = field(default_factory=dict) # 每实例独立 dict # o.items = (...) → FrozenInstanceError — 配置错误当场暴露, 不带病上线
风控回测要同时持有 1000 万个行情点对象, 默认 __dict__ 版直接把 512MB 容器打爆:
class PointDict: # 默认: 每实例背一个 __dict__ def __init__(self, x, y): self.x, self.y = x, y class PointSlots: __slots__ = ("x", "y") # 定长槽, 没有 __dict__ def __init__(self, x, y): self.x, self.y = x, y # tracemalloc 实测 10_000_000 实例: # PointDict ≈ 1.62 GB → PointSlots ≈ 0.76 GB (-53%) # 属性访问快 ~10%; 代价: 不能动态加属性 (海量小对象求之不得)
海量小对象 (网格/词表/回测 bar) 一律 slots; 数量级到不了百万就别为它牺牲灵活性。
全局只要一个的重量级对象, 三种写法各有成本 — 生产默认选最朴素的:
# ① 模块级 (Python 之禅): 模块只执行一次, config = load() 天然单例 # 优点零魔法; 缺点 import 即初始化, 测试要多实例得 hack # ② __new__ 拦截: 需要"延迟到首次使用"时 class ConnHub: _inst = None def __new__(cls): if cls._inst is None: # 首次才真正创建 cls._inst = super().__new__(cls) return cls._inst # ③ metaclass: 所有子类自动单例 — 框架级魔法, 业务别碰 # 生产默认①; 要多环境/易测试 → 工厂 + 显式依赖注入
播放列表类想被 len/切片/for/in 像原生列表一样对待 — 不用继承 list, 实现协议即可:
class Playlist: def __init__(self, *tracks): self._t = list(tracks) def __len__(self): return len(self._t) def __getitem__(self, i): if isinstance(i, slice): # 支持 pl[10:20] 切片 return Playlist(*self._t[i]) return self._t[i] def __iter__(self): return iter(self._t) # 白得的能力: len(pl), pl[3], pl[1:5], for t in pl, "x" in pl, list(pl)
三个业务类都要 JSON 导出与审计日志。老代码从公共基类继承, 基类越长越不敢动; 重构成窄 Mixin:
class JSONMixin: # 能力: 可序列化; 无状态, 不单用 def to_json(self): return json.dumps(self.__dict__, default=str, ensure_ascii=False) class AuditMixin: def audit(self, actor): # 只依赖宿主自带属性 log.info("audit", obj=type(self).__name__, oid=getattr(self, "id", None), actor=actor) class Report(JSONMixin, AuditMixin): # 声明能力而非继承实现 ... # Mixin 排 MRO 最左; 动了能力不影响业务类, 各能力可独立测试
存储层要换 Redis/MySQL 双实现, 新人漏实现一个方法要到运行到那行才炸。抽象基类把契约钉死:
from abc import ABC, abstractmethod class Storage(ABC): @abstractmethod def get(self, key): ... @abstractmethod def put(self, key, val): ... class RedisStorage(Storage): def get(self, key): return self.r.get(key) # 忘了 put → RedisStorage() 当场 TypeError: # Can't instantiate abstract class without put — 启动期报错
给每个 ETL 阶段统一计时打点, 不想在每个函数里复制 try/finally — 做成上下文管理器对象 (机制详见上下文主题):
class timer: # 用法: with timer("etl"): ... def __init__(self, name): self.name = name def __enter__(self): self.t0 = time.perf_counter(); return self def __exit__(self, exc_t, exc_v, tb): ms = (time.perf_counter() - self.t0) * 1000 metrics.histogram("phase.ms", ms, phase=self.name) # 异常也计时 return False # 不吞异常
余额字段要"负数进不来 + 首次访问才查账本"。门面用 property, 内部走私有属性, 一道门收口:
class Account: def __init__(self, aid): self.aid, self._balance = aid, None @property def balance(self): # 惰性: 首次访问才查账本 if self._balance is None: self._balance = self._fetch_from_ledger() return self._balance @balance.setter def balance(self, v): if v < 0: raise ValueError("balance cannot be negative") self._balance = v # 写内部字段, 不触发自身
重试框架打日志全是 "object at 0x7f3a...", 排障时对不上是哪个任务。手写 repr 是性价比最高的投资:
@dataclass class Task: id: str; status: str; attempt: int = 0 def __repr__(self): # 排障字段全带上, 截断防刷屏 return f"Task(id={self.id!r}, status={self.status}, try={self.attempt})" # 日志: [retry] Task(id='t-91', status=failed, try=3) — 一眼定位, 可直接 grep # 默认的 <Task object at 0x7f...> 在排障现场等于什么都没打
缓存/存档里的对象是旧结构, 新代码加了字段, 反序列化就缺 key。用状态钩子做迁移 (序列化细节见序列化主题):
class Profile: def __setstate__(self, state): if "created_at" not in state: # 旧版本存档没有该字段 state["created_at"] = utcnow() # 迁移默认值 if "tags" not in state: state["tags"] = [] self.__dict__.update(state) # 永远兼容旧快照 # dataclass 等价做法: 新字段给 default; __init__ 缺字段时不会炸
class C: items = [], 一个实例 append 全部实例都看得见, 数据莫名串味。正解: 类属性只放不可变常量; 实例级可变状态在 __init__ 里 self.items = []。
# 错: 类属性放可变对象, 全员共享一份 class C: items = [] C().items.append(1); print(C().items) # → [1] 数据串味 # 对: __init__ 里 self.items = [] 每实例独立
def f(x, acc=[]): acc 跨调用累积, 第二次调用带着第一次的残留。正解: acc=None 入参, 函数体内 if acc is None: acc = [] — 与坑 1 是同一个根: 默认值只求值一次。
# 错: def f(x, acc=[]) → 默认值只求值一次, 残留跨调用 def f(x, acc=None): if acc is None: acc = [] # 对: None 哨兵 acc.append(x); return acc f(1); print(f(2)) # → [2] 每次全新
TypeError: unhashable。原因: 定义 __eq__ 会把 __hash__ 置 None。正解: 成对实现 __hash__ = hash((self.a, self.b)) 满足契约, 或 dataclass(frozen=True) 自动生成。
class P: def __init__(self, a): self.a = a def __eq__(self, o): return self.a == o.a {P(1)} # 错: → TypeError: unhashable type: 'P' # 对: def __hash__(self): return hash(self.a) 成对补上
A.__init__(self, ...) 直接指名道姓, A 被初始化两遍或被跳过。正解: 一律 super().__init__(**kwargs) 沿 MRO 走; 父类签名差异用 kwargs 透传。
# 错: 手写 B.__init__(self); C.__init__(self) 指名道姓, # 菱形里 A.__init__ 被跑两遍 (或被跳过) class D(B, C): def __init__(self, **kw): super().__init__(**kw) # 对: 一律 super() 沿 MRO 走, 签名差异用 kwargs 透传
D(B, C) 且 B/C 都有 save, 永远是 B 的赢, 你以为调的是 C 的语义。正解: 明确意图 — 显式 D.save = C.save 或拆成 Mixin; 上线前打印 D.__mro__ 确认顺序。
class B: def save(self): return "B" class C: def save(self): return "C" class D(B, C): ... D().save() # → B 声明在前的 B 赢, 不是你以为的 C # 对: 显式 D.save = C.save 表意, 或拆 Mixin; 查 D.__mro__
tags: list = [] 定义时直接 ValueError: mutable default (旧写法静默共享, 更阴险)。正解: field(default_factory=list); frozenset/tuple 这类不可变值可直接当默认。
# 错: @dataclass class O: tags: list = [] # → ValueError: mutable default ... for field 'tags' @dataclass class Ok: tags: list = field(default_factory=list) # 对
self.balance = v 又进 setter, RecursionError。正解: 内部一律走私有字段 self._balance, property 只做门面; 新代码可用 __set_name__ 描述符避免手滑。
# 错: @balance.setter 里写 self.balance = v # → 又进 setter 自己, RecursionError @balance.setter def balance(self, v): self._balance = v # 对: 写私有字段
__del__, 文件/连接照样泄漏。正解: with/显式 close + atexit 兜底; __del__ 最多在泄漏时打条告警日志。
# 错: 靠 __del__ 关连接 — 退出/引用环时不保证调, 照样泄漏 with open("a.log") as f: ... # 对: with 确定性释放 atexit.register(conn.close) # 对: 进程退出再兜底
self.x = 1 写进实例, 之后改 Cls.x, 这个实例读到的还是旧值, 同一个类两套真相。正解: 类属性当只读常量用 (Cls.x 访问); 会变的状态不放类上。
class C: x = 1 a = C(); a.x = 99 # 写进实例 __dict__ C.x = 100 # 之后改类, a 读到的还是旧值 print(a.x, C().x) # → 99 100 两套真相 # 对: 会变的状态不放类上, 类属性只当常量
__init__ 里 self.load(), 子类覆写 load 且用到 self.conn (还没赋值) → AttributeError。正解: 模板方法用私有名 self._load(); 或两段式 construct + initialize, 文档写清调用顺序。
class Base: def __init__(self): self.conn = connect() self._load() # 对: 私有名, 子类覆写不了 # 错: 改成 self.load() — 子类覆写 load 且用到 self.conn, # 而 conn 还没赋值 → AttributeError
class A: @property def v(self): return self._v # 错: 简单字段也包一层 class B: v = 1 # 对: 裸属性, 热路径零包装 # property 只留给校验/惰性计算/迁移兼容三类场景
TypeError: metaclass conflict。正解: 写一个共同元类 (继承两者) 或改用 __init_subclass__ 钩子 — 现代优先级永远给后者。
class M1(type): ... class M2(type): ... class X(metaclass=M1): ... class Y(metaclass=M2): ... class Z(X, Y): ... # 错: → TypeError: metaclass conflict # 对: class M12(M1, M2): ... Z(X, Y, metaclass=M12)
__weakref__ (weakref 失效)、老式 pickle/动态 Mixin 翻车。正解: 需要 weakref 显式把 "__weakref__" 加进 slots; 与库冲突就退回普通类, 用工厂函数控制字段。
class S: __slots__ = ("a", "__weakref__") # 对: 要 weakref 显式加槽 s = S(); s.b = 1 # 错: → AttributeError (实例没有 __dict__)
@classmethod (dict.fromkeys 就是范本), 纯函数才 staticmethod。
class Conn: @classmethod def from_cfg(cls, path): return cls(parse(path)) # 对: Sub.from_cfg() 拿到 Sub; dict.fromkeys 同款范本 # 错: @staticmethod 里硬编码 return Conn(...) — 子类拿到父类
self.engine.start(); 继承留给真正的类型关系。
# 错: class Car(Engine) — 为复用 20 行, is-a 根本不成立 class Car: # 对: 组合 + 窄接口委托 def __init__(self): self.engine = Engine() def start(self): self.engine.start()
cls.counter += 1 是读-改-写三步, 多线程/asyncio 并发下更新丢失。正解: 加锁或 itertools.count + next(); 多进程下类属性根本不共享, 计数请用 Redis/DB。
class Meter: n = 0 def hit(self): Meter.n += 1 # 错: 读-改-写, 并发丢更新 # 对: with lock: Meter.n += 1 或 next(itertools.count())
@dataclass(frozen=True) 冻结的是属性绑定, 字段里的 list/dict 内容照样被改: o.items.append(x) 不报错。正解: 用 tuple/frozenset/MappingProxyType 装真正不可变的内容。
@dataclass(frozen=True) class O: items: list = field(default_factory=list) o = O(); o.items.append(1) # 冻结的是绑定, 内容照样被改 # 对: items: tuple = () 用不可变容器装内容
__getattribute__ 后所有属性访问都过它, 忘了调 super() 直接 RecursionError, 且性能塌方; __getattr__ 只在正常查找失败后触发。正解: 补缺省用 __getattr__; 确要全量拦截时内部必须 super().__getattribute__(name)。
class A: def __getattribute__(self, name): return super().__getattribute__(name) # 对: 必须委托 # 错: return self.__dict__[name] → 又进自己, RecursionError # 补缺省用 __getattr__: 只在正常查找失败后才触发
super().save() 落到 object 直接 AttributeError; 宿主组合顺序不同, 调到的实现也不同。正解: Mixin 文档写清"宿主链必须提供某方法"; 防御性 getattr(super(), "save", None) 判空。
class JSONMixin: def save(self): super().save() # 单独用 → AttributeError # 对: fn = getattr(super(), "save", None) 判空再调; # 文档写清宿主链必须提供 save, 组合顺序不同结果不同
isinstance 且子类分支在前; 精确匹配才用 type(x) is Cls。
class Base: ... class Sub(Base): ... print(type(Sub()) == Base) # 错: → False 子类被拒之门外 print(isinstance(Sub(), Base)) # 对: → True # if isinstance 分支要把子类写在前面, 否则父类先命中