首页>Python>正文

Python元编程四件套:装饰器、描述符、元类与动态属性

Feng 37 阅读 Python

重构旧项目那阵子,最烦的不是业务逻辑本身,而是散落在几十个类里的日志埋点和权限判断。复制粘贴到第三遍的时候我决定换个打法——用元编程把这些横切逻辑收拢到一处。改完之后代码量掉了大半,后面加新模型也不用再惦记着补样板。下面这几招是真正在项目里跑过的,不是教科书演示。

装饰器不止能包函数,还能改造整个类

函数装饰器大家都会写,但类装饰器在批量注入行为时更省事。比如给所有数据模型统一挂一个 get_or_create,逐个类手写既啰嗦又容易漏。类装饰器一次搞定:

def auto_get_or_create(cls):
 def get_or_create(self, kwargs):
 obj = cls.objects.filter(kwargs).first()
 if not obj:
 obj = cls(**kwargs)
 obj.save()
 return obj
 cls.get_or_create = get_or_create
 return cls

在类头上加一行 @auto_get_or_create 就能用。这里有个容易混淆的点:类装饰器是在类对象已经构建完成之后才动手改它,而元类介入的是类创建过程本身。前者适合”事后打补丁”,后者适合”出生即定型”。选错了手段,代码会别扭。

描述符做惰性缓存,顺手避开两个坑

描述符是元编程里最值得花时间啃的机制。我做过一个计算密集的数据管道,属性每次读取都要判断缓存是否失效,用普通 property 写出一堆重复样板。换成描述符后清爽很多:

class CachedProperty:
 def __init__(self, func):
 self.func = func
 self.name = func.__name__
 def __get__(self, instance, owner):
 if instance is None:
 return self
 if self.name not in instance.__dict__:
 instance.__dict__[self.name] = self.func(instance)
 return instance.__dict__[self.name]

第一次访问触发计算,结果塞进实例字典,后续直接命中。数据管道跑下来快了约三倍。

但这段代码上线前得想清楚两件事。其一,如果宿主类定义了 slots,实例根本没有 dict,赋值那行会直接抛 AttributeError。要么去掉 slots,要么把缓存挪到 WeakKeyDictionary 里按实例索引。其二,CachedProperty 只实现了 get,属于非数据描述符,优先级低于实例属性——一旦有人手动给同名属性赋值,缓存就被静默覆盖,且不会失效。需要强一致的话,补一个 set 让它变成数据描述符,或者在 set 里清缓存。

Python元编程四件套:装饰器、描述符、元类与动态属性

元类负责”出生登记”,注册路由最典型

元类常被扣上过度设计的帽子,但在框架层做自动注册确实好用。写微服务框架时,所有继承 BaseService 的类都该把公开方法登记进路由表,靠人工维护迟早出错。元类在类定义那一刻就能完成收集:

class ServiceMeta(type):
 def __new__(cls, name, bases, dct):
 new_class = super().__new__(cls, name, bases, dct)
 if hasattr(new_class, '__rpc_methods__'):
 for attr in dct:
 if callable(dct[attr]) and not attr.startswith('_'):
 ServiceMeta._register_method(new_class, attr, dct[attr])
 return new_class

团队新人写服务类,什么都不用配就自动接入。注意遍历的是 dct 而非 dir(new_class),前者只含本类新定义的成员,不会把父类方法重复注册一遍。

属性校验交给 set_name 和 init_subclass

数据校验如果每个字段手写断言,代码会迅速膨胀。用描述符拦截赋值,再配合 init_subclass 让子类自动获得校验能力,业务层就干净了:

class ValidatedAttribute:
 def __set_name__(self, owner, name):
 self.name = name
 def __set__(self, instance, value):
 if not isinstance(value, (int, float)):
 raise TypeError(f'{self.name} must be numeric')
 instance.__dict__[self.name] = value

set_name 是 Python 3.6 引入的钩子,类创建时由解释器自动调用,把属性名回传给描述符,省去了手动传名字的麻烦。低于 3.6 的版本没有这个机制,只能退回在 init 里显式赋值。另外 set_name 抛出的异常会被包装成 RuntimeError 向上冒泡,调试时别被这层包装迷惑。

动态生成类:能用 type() 就别用 exec

配置系统里根据 YAML 自动生成数据类,很多人第一反应是拼字符串丢给 exec。能跑,但命名空间污染和安全边界都难控。更稳的做法是 type() 三参形式或 types.new_class:

fields = {'host': str, 'port': int}
Config = type('Config', (), {k: None for k in fields})

需要更复杂的构建流程(比如处理 prepare)时用 types.new_class,它把元类解析、命名空间准备这些细节都封装好了。exec 不是不能用,但只该出现在输入完全可信、且确实没有替代路径的场景。把外部配置直接喂给 exec,等于把执行权交出去。

收尾

元编程的价值不在炫技,而在于把重复的、机械的、容易漏的代码收进运行时机制里。装饰器管横切注入,描述符管属性访问,元类管类创建期的登记,动态构建管批量生成。每样都有明确的适用边界,越界使用就是给自己埋雷。从小处动手,先挑一个重复最多的模块试,跑通了再铺开。

ID:7106

Feng
这位作者很神秘,还没有填写简介。