property/classmethod/ORM 字段的共同底层 — 框架"魔法"的全部原料: 装饰器改函数, 描述符改属性访问, metaclass 改类创建
装饰器回答"怎么增强函数", 描述符回答"怎么接管属性的读写"。你在框架里见过的所有"魔法" — Django 的 Model.name 能校验能生成 SQL、property 让方法像字段、@classmethod 自动传 cls — 底层都是同一套协议: 把"读写属性"变成"执行你的代码"。看懂这两件, Python 框架的源码对你就是透明的。
@deco\ndef f(): ... 就是 f = deco(f)。装饰器是"接收可调用并返回可调用"的高阶函数, 类也可以当装饰器(实现 __call__)。 def deco(fn): return lambda: fn() + 1 # 关键: 收函数, 返回新可调用 def f(): return 41 f = deco(f) # 等价于在 f 头上写 @deco f() # → 42
@retry(3) 是三层: 最外层收参数 → 中间层收函数 → 最内层收调用参数。记住"参数越外层越多一层嵌套"。 def retry(times): # 第 1 层: 收参数 def deco(fn): # 第 2 层: 收函数 def wrap(*a): return fn(*a) # 第 3 层: 收调用 return wrap return deco # 关键: @retry(3) 先吃参数再吃函数
obj.x 才会触发 __get__(obj, Owner)。放实例 __dict__ 里、或直接挂在实例上都不会生效。 class D: def __get__(self, obj, owner): return 42 class C: d = D() # 关键: 必须放宿主类的类属性位 C().d # → 42 c = C(); c.d = D(); c.d # → D 对象本身, __get__ 不触发
__set__/__delete__ = data, 优先级高于实例字典(所以 property 拦得住赋值); 只有 __get__ = non-data, 会被同名的实例属性盖住(普通函数就是 non-data, 所以实例能遮蔽方法)。 class Data: def __get__(s, o, t): return "data" def __set__(s, o, v): pass # 关键: 带 __set__ = data c = type("C", (), {"x": Data()})(); c.__dict__["x"] = "inst" c.x # → data, 压过实例字典 # 若删掉 __set__ (non-data): c.x → inst, 被实例属性盖住
class C: def __getattr__(self, k): return "fallback" class P(C): @property # 关键: data 描述符排第 1 def x(self): return 1 p = P(); p.__dict__["x"] = 99 # 实例字典蓄意覆盖 p.x # → 1, property 赢 __dict__ p.y # → fallback, 找不到才兜底
class C: def m(self): return self C.m # → 普通函数, 未绑定 c = C() c.m() is c # → True C.m.__get__(c, C) # 关键: 手动触发描述符 = 绑定方法
class Base: def __init_subclass__(cls, **kw): # 关键: 3.6+ 替代 metaclass assert "name" in cls.__dict__ super().__init_subclass__(**kw) class Sub(Base): name = "s" # 忘写 name → 启动即 AssertionError
def retry(times=3, delay=0.5, exc=(TimeoutError, ConnectionError)): def deco(fn): @functools.wraps(fn) # 保住函数名/签名/文档 def wrapper(*args, **kw): for i in range(times, 0, -1): try: return fn(*args, **kw) except exc as e: if i == 1: raise time.sleep(delay * (times - i + 1)) # 递增退避 return wrapper return deco @retry(times=3) def call_payment(api): ... # 幂等接口才可重试!
注意重试的前提是幂等; 支付下单这类非幂等操作要用幂等键, 而不是裸重试。
配置/模型字段上线前就拦住脏数据, 而不是等线上报错:
class Typed: def __init__(self, typ, default=None): self.typ, self.name = typ, None def __set_name__(self, owner, name): self.name = name # 3.6+: 自动拿字段名 def __set__(self, obj, val): if not isinstance(val, self.typ): raise TypeError(f"{self.name} 需要 {self.typ.__name__}, 得到 {val!r}") obj.__dict__[self.name] = val # 值仍存实例字典, 描述符只做守门 __get__ = None # 简化: 读直接走实例字典 (跳过 __get__) class Order: amount = Typed((int, float)); status = Typed(str) Order().amount = "1" # → TypeError: amount 需要 int/float, 得到 '1'
class Sku: @property def price_with_tax(self): # 读起来像字段, 每次实时算 return round(self.price * (1 + TAX), 2) @functools.cached_property # 3.8+: 算一次缓存进 __dict__ def heavy_report(self): ...
接口序列化时 property 直接当字段输出 (dataclasses.asdict 前先 asdict() 转换), 派生字段不必落库。
Flask 风格 @app.get 的本质: 装饰器把 (path, handler) 登记进注册表并原样返回函数:
ROUTES: dict[str, callable] = {}
def route(path):
def deco(fn):
ROUTES[path] = fn # 注册表登记
@functools.wraps(fn)
def wrapper(*a, **kw):
return fn(*a, **kw) # 原样透传, 也可加前置/后置
return wrapper
return deco
@route("/orders") # 业务函数零侵入接入
def list_orders(): ...
服务对象的懒初始化与共享用描述符最顺: 业务类只见声明, 组装逻辑集中在容器:
class Inject: def __init__(self, cls): self.cls = cls def __set_name__(self, owner, name): self.name = name def __get__(self, obj, owner=None): if obj is None: return self svc = container.resolve(self.cls) # 首次访问时按类型解析 obj.__dict__[self.name] = svc # 缓存进实例, 之后直读 return svc class OrderService: db = Inject(Database) # 声明即注入; 单测时 container.override(Database, FakeDB)
配置字段赋值即校验, 错误在启动第 1 秒爆出而不是运行第 3 天:
class Range: def __init__(self, typ, lo, hi): self.typ, self.lo, self.hi = typ, lo, hi def __set_name__(self, o, name): self.name = name def __set__(self, obj, val): if not isinstance(val, self.typ) or not (self.lo <= val <= self.hi): raise ValueError(f"{self.name} 需 {self.typ.__name__} 且在 [{self.lo},{self.hi}]") obj.__dict__[self.name] = val class AppConfig: port = Range(int, 1, 65535) workers = Range(int, 1, 128) AppConfig().port = 99999 # → ValueError: 启动即失败
报告生成贵且多数请求用不到: cached_property 延迟到首次访问, 失效可精确控制:
class SkuView: def __init__(self, sku): self.sku = sku @functools.cached_property # 算一次缓存进实例 __dict__ def heavy_report(self): return build_expensive_report(self.sku) def invalidate(self): self.__dict__.pop("heavy_report", None) # 删缓存即重新生成
order.user 首次访问触发查询并缓存 —— 看懂描述符就懂 N+1 的成因与解法:
class ForeignKey: def __init__(self, model): self.model = model def __set_name__(self, o, name): self.name = name def __get__(self, obj, owner=None): if obj is None: return self if self.name not in obj._cache: obj._cache[self.name] = self.model.get(obj._fk[self.name]) # 惰性查询! return obj._cache[self.name] # 循环里逐个 order.user = 每次一条 SQL = N+1; 解法: select_related 一次预填 _cache
请求从上往下穿: 先认证(身份)再授权(权限), 顺序即语义:
def login_required(fn): @functools.wraps(fn) def wrapper(request, *a, **kw): if not request.user: raise Unauthorized() return fn(request, *a, **kw) return wrapper def role_required(role): def deco(fn): @functools.wraps(fn) def wrapper(request, *a, **kw): if request.user.role != role: raise Forbidden() return fn(request, *a, **kw) return wrapper return deco @login_required # 外层: 有身份 @role_required("admin") # 内层: 是管理员 def delete_order(request): ...
基类 __init_subclass__(3.6+) 子类定义即注册, 不需要 metaclass:
REGISTRY: dict[str, type] = {}
class Plugin:
name: str
def __init_subclass__(cls, **kw): # 每定义一个子类自动调用
super().__init_subclass__(**kw)
REGISTRY[cls.name] = cls # 注册!无需显式登记
class WechatPay(Plugin): name = "wechat" # import 即生效
class Alipay(Plugin): name = "alipay"
handler = REGISTRY["wechat"](cfg) # 按名实例化, 新增支付方式零改分发代码
def deco(fn): def w(): return fn() return w # 错: w.__name__ → 'w', 排查全乱 def deco(fn): @functools.wraps(fn) # 对: 名字/签名/文档全保住 def w(): return fn() return w
obj.desc = MyDesc() 毫无效果, 协议只在类属性位触发。正解: 声明在 class body 里。 class D: def __get__(self, o, t): return 42 class C: d = D() # 对: 类属性位 → C().d → 42 c = C(); c.d = D() # 错: 覆盖到实例上 c.d # → D 对象本身, 协议不触发
# 错: 连接在 import 时就执行, 没人调用也跑 def deco(fn): DB.connect(); return fn # 对: 副作用收进 wrapper, 首次调用才发生 def deco(fn): @functools.wraps(fn) def w(): DB.connect(); return fn() return w
class C: items = [] 被所有实例共享 append。正解: __init__ 里初始化实例属性; 或用 dataclass field(default_factory=list)。 class Cart: items = [] # 错: 所有实例共享一个 list a, b = Cart(), Cart(); a.items.append(1) b.items # → [1], b 也被污染 class Cart: # 对: 实例级初始化 def __init__(self): self.items = []
class Bad: def __set__(s, o, v): s.v = v # 错: 值存自己身上 def __get__(s, o, t): return s.v # 所有宿主共享一个 v class Good: def __set_name__(s, o, n): s.n = n def __set__(s, o, v): o.__dict__[s.n] = v # 对: 值存宿主
class Meta(type): def __new__(m, n, b, ns): REGISTRY[n] = super().__new__(m, n, b, ns) return REGISTRY[n] # 错: 注册也要上 metaclass # 对: __init_subclass__ 两行等价、零魔法 class Plugin: def __init_subclass__(cls, **kw): REGISTRY[cls.__name__] = cls
@A @B def f 是 f = A(B(f)), 请求先穿 A 再到 B; 认证/缓存/重试的顺序不同结果完全不同(缓存放认证前 = 缓存未鉴权数据)。正解: 注释明确每层职责, 团队固定顺序。 @cache # 错: 缓存了未鉴权的数据 @login_required def view(req): ... @login_required # 对: 先鉴权后缓存 @cache # f = login(cache(view)) def view(req): ...
decorator 库重包。 def deco(fn): def w(*a, **k): return fn(*a, **k) return w # 错: 签名变成 (*a, **k) @deco def add(a, b=1): ... inspect.signature(add) # → (*a, **k), 框架全错位 # 对: wrapper 上加 @functools.wraps(fn) → (a, b=1)
@retry 直接用在支持参数的装饰器上, fn 被当成了 times 参数, 报错晚且难懂。正解: 带参装饰器写好后测试"两种用法"(@retry 与 @retry(3))分别行为正确。 @retry # 错: 没加括号, fn 顶替了 times def call(): ... call() # → TypeError: 缺少参数 'fn' @retry(3) # 对: 工厂先吃参数再吃函数 def call(): ...
a = Typed(); b = Typed() 是两个实例没问题; 但 a = b = Typed() 或工厂函数返回同一个实例时, 两个字段互相覆盖状态。正解: 每个字段 new 一个描述符; 状态存宿主 __dict__ 见坑 5。 shared = Typed() class C: a = shared; b = shared # 错: 两字段共用状态互踩 class C: # 对: 每字段各 new 一个 a = Typed(); b = Typed()
# 错: 子类不会再触发 __set_name__, 沿用父类元数据 class Sub(Base): pass # Base.x 的名字还是 'x' # 对: 在 __init_subclass__ 里对子类命名空间重扫 class Base: def __init_subclass__(cls, **kw): for k, v in vars(cls).items(): if isinstance(v, Typed): v.__set_name__(cls, k)
@x.setter 钩子; 契约变化要显式。 class A: @property def x(self): return self._x class B(A): @x.setter # 错: NameError, 子类没有 x def x(self, v): ... # 对: 子类重写完整 property(getter + setter)
self.method() 上静态/类方法都能调, 但隐式 cls 绑定差异让重构(改方法类型)影响面不可见。正解: 类方法统一 ClassName.method 调用风格, review 时类型一目了然。 class Order: @classmethod def from_dict(cls, d): return cls(**d) Order.from_dict({"id": 1}) # 对: 类名调用, cls 一目了然 Order().from_dict({"id": 1}) # 错: 能跑但 cls 来源含糊
class Proxy: def __getattr__(self, k): # 对: 找不到才兜底 return getattr(self.target, k) class Bad: def __getattribute__(self, k): # 错: 每次都进 return self.target[k] # → RecursionError
@property def ratio(self): return self.a / self.b # 错: b=0 → ZeroDivisionError → 500 @property def ratio(self): return self.a / self.b if self.b else 0.0 # 对: 安全默认
def deco(fn): def w(*a): return fn(*a).result() # 错: 生成器没有 .result return w def deco(fn): def w(*a): return fn(*a) # 对: 只透传返回值 return w
# 错: __set__ 里写 obj.__dict__[key] → AttributeError(no __dict__) class D: def __set_name__(s, o, n): s.n = "_" + n def __get__(s, o, t): return getattr(o, s.n) def __set__(s, o, v): setattr(o, s.n, v) # 对: 值存独立 slot class C: __slots__ = ("_x",); x = D()
class A(metaclass=M1): ... class B(metaclass=M2): ... class C(A, B): ... # 错: TypeError: metaclass conflict class M12(M1, M2): type # 对: 先合并再指定 class C(A, B, metaclass=M12): ...
OrderService.save = patched_save # 错: 补丁散落, 时机敏感 from patches import * # 谁都说不清改了什么 @timed # 对: 定义期声明横切逻辑 def save(self): ... # 可 grep 可单测; 补丁只留测试/应急
@feature_flag("x") # 错: 三层装饰器+描述符+metaclass @register @validated class Task(Foundation): ... # 新人接手即瘫痪 class Task(Base): # 对: 一层足矣 + docstring """普通类, 魔法可 grep 可调试"""