Python深入浅出分析元类
前言「元类metaclass是用来创建类的类」——这句话几乎每篇讲元类的文章都会说但它只给出了定义没回答真正关键的问题元类在整条对象链条的哪一层它到底能管什么、管不了什么大多数把元类用错的人问题不在语法而在这里。常见的两个极端误解一是把元类当成「万能钩子」觉得它能像__setattr__那样管每个实例的属性二是把它当成「高级装饰器」能用却说不清使用时机。其实元类只管「类被创建」这一件事一旦类建好了它就不再多看一眼实例层面的行为全归描述符和__setattr__/__getattribute__管。边界划清之后什么时候该用元类、什么时候不该用就自然清楚了。本文从对象链条讲起再拆解一次类定义在解释器里到底走了哪几步然后给出元类的能力清单和三种「改类」手段的取舍最后用一个从类型注解自动生成 property 的实战收尾。结论以 Python 官方语言参考的数据模型章节为准。一、三层链条实例、类、元类Python 里对象是分层的。拿一个自定义类举例# 适用于 Python 3.0class User:passu User()三层关系是u是实例它的类型是Usertype(u) is User。User本身也是一个对象它的类型是元类。默认情况下type(User) is type。type又是对象它的类型仍是type——链条在这里闭环。配套的两个方向要分清「类型是什么」问type(x)「继承自谁」问x.__bases__。User继承自object因为没写基类但它的类型是typetype继承自object类型是它自己。用一张表固定下来对象type(x)x.__bases__u实例User不适用实例无__bases__User类type(object,)typetype(object,)objecttype()「元类」就是这张表里第三行的角色类的类型。定义一个类时指定metaclassMeta就是把这个类的类型从type换成Meta。二、一次类定义走了哪几步语言参考把过程拆成五步按顺序是解析 MRO 条目确定合适的元类准备类命名空间调用元类的__prepare__执行类体类里的赋值、方法定义都写进这个命名空间创建类对象调用元类本身。其中第 5 步用的是metaclass(name, bases, namespace, **kwds)这个调用——也就是说元类被当函数调用走的是元类的__call__默认即type.__call__进而走到元类的__new__和__init__。而元类本身又是type的实例所以它的__call__来自type。这条链路解释了为什么在元类里能同时拦到「建类」和「建实例」两个时机。第 3 步有个容易忽略的细节__prepare__返回的命名空间在用完之后会被复制到一个新的有序映射再包成只读代理作为类的__dict__。所以别把那个中间映射存起来当类的__dict__用二者不是同一个对象。type(name, bases, dict)这个三参数形式就是第 5 步的「手工版」。官方文档明确说这三参数形式是类语句的动态等价物dict 参数「可能会被复制或包装」后再成为__dict__——同样不要依赖同一性。三、元类能做什么、不能做什么把能力边界列成清单能做的都在「类被创建」这个时机修改类的命名空间或类体执行期的映射__prepare__在建类前后增删属性、校验约束__new__/__init__拦截「类被调用」这件事__call__从而统一控制实例的产出比如单例、对象池让类支持自定义的isinstance/issubclass语义在元类上定义__instancecheck__/__subclasscheck__这正是abc.ABCMeta的实现方式。不能做的管单个实例的属性读写——那是__setattr__、__getattribute__和描述符协议的职责阻止某个子类被定义——元类无法「拒绝继承」要限制得靠__init_subclass__里主动raise影响已经建好的类的历史行为——类一旦建完元类的__new__/__init__不会再执行。isinstance那条要特别说清__instancecheck__和__subclasscheck__是在元类上查的不能定义成普通类里的类方法。官方原文说得很直接这两个方法在类的「类型即元类」上查找与实际类无关。这是把isinstance和「鸭子类型」区分开的关键——abc.ABCMeta正是靠它实现了「虚拟子类」。四、三种「改类」手段怎么挑手段运行时机适合代价类装饰器类对象已建好后加方法、注册、改写属性每个类都要写deco会漏__init_subclass__子类被创建时定义在父类3.6子类注册、子类校验只对子类生效父类自身不触发元类类创建的整个过程校验、换命名空间、拦截类的调用、改isinstance语义影响所有使用者耦合重、可读性差选择顺序建议从简到繁装饰器能解决就用装饰器需要在「每个子类出现时自动执行」就用__init_subclass__只有需要控制类命名空间、类的调用或自定义isinstance语义时才动用元类。Python 社区有一句流传很广的建议如果你还在犹豫要不要用元类那通常就是不该用。实战从类型注解自动生成 property需求类里声明name: str这样的注解后自动得到一组读写该属性的 property实际数据存在带下划线的私有名里。# 适用于 Python 3.0注解读取部分见下方版本说明def _make_field(storage_name):def getter(self):return getattr(self, storage_name, None)def setter(self, value):setattr(self, storage_name, value)return property(getter, setter)class Fields(type):def __new__(mcls, name, bases, namespace):cls super().__new__(mcls, name, bases, namespace)for attr in namespace.get(__annotations__, {}):setattr(cls, attr, _make_field(_ attr))return clsclass Person(metaclassFields):name: strage: intdef __init__(self, name, age):self.name name # 走到 property 的 setterself.age agep Person(Alice, 30)print(p.name, p.age) # Alice 30print(p._name) # Alice ← 真实数据在这里关键点__annotations__只包含本类体里的注解namespace.get读的就是它setattr(cls, attr, property(...))在类建好后补上数据描述符于是self.name ...会走 setter 而不是直接写实例字典。版本说明在 Python 3.13 及更早类命名空间里的__annotations__就是普通字典上面的读法直接可用。Python 3.14 起注解改为延迟求值PEP 649 / PEP 749官方对「读取__annotations__的代码」给出的建议是改用annotationlib.get_annotations()并选择合适格式dataclasses模块就是这么改的。如果你要写跨版本代码优先用annotationlib的公开 API别依赖__annotations__的未文档化细节。常见坑点以为元类__new__每造一个实例就执行一次❌ 在元类__new__里写「给这个实例加属性」的逻辑。 → 它只在建类时执行一次实例级行为它根本管不到。 ✅ 建类逻辑放元类__new__实例逻辑放实例__init__/__setattr__/ 描述符。在元类__new__里改namespace指望影响已经建好的类❌ns2 super().__new__(...); namespace[x] 1此时类已经建好。 → 命名空间早已被复制成只读代理改它对类无效还可能TypeError。 ✅ 要补属性就在super().__new__之后用setattr(cls, ...)。依赖三参数type()里的 dict 会成为类__dict__❌d {...}; C type(C, (), d); assert C.__dict__ is d→ 官方说明该 dict「可能被复制或包装」不保证是同一个对象实际是只读代理。 ✅ 只把 dict 当「输入」用C.__dict__[k]读回来。把__instancecheck__写成类里的普通方法/类方法❌class Box: classmethod def __instancecheck__(cls, obj): ...→ 官方明确说它在元类上查找写在类里不会生效。 ✅ 把__instancecheck__/__subclasscheck__定义在元类上或直接用abc.ABCMeta。以为元类能阻止别人继承❌ 在__new__里检查bases然后return None来「禁止」子类。 → 并不会真正阻止反而让类绑定成None后面各种TypeError。 ✅ 确实要限制就在__init_subclass__里判断并raise TypeError把话说清楚。给metaclass配了额外关键字却没接收❌class A(Base, tag1, metaclassM):而M.__new__签名是(mcls, name, bases, ns)。 → 多余关键字无处安放构造报错。 ✅ 元类相关方法统一加**kwargs并透传给super()。拿元类去替描述符❌ 想在元类里写def __get__让实例读属性时被拦截。 → 元类的属性访问发生在「类」上实例读属性根本不经过元类。 ✅ 要拦实例属性读写用描述符__get__/__set__或__getattribute__。沿用 Python 2 的老习惯❌ 写class A(object): __metaclass__ MPython 3 里__metaclass__已失效等于没写。 ✅class A(metaclassM):。另外注意 Python 2.7 已于 2020 年 1 月 1 日停止维护Python 3 里所有类都是新式类class A:与class A(object):等价旧式类的概念已不存在。总结问题答案元类在哪一层类之上的那一层即「类的类型」什么时候触发类被创建时__prepare__/__new__/__init__类被调用时__call__能改实例行为吗不能实例层面归描述符与__setattr__等管能改isinstance吗能但要写在元类上__instancecheck__该优先用什么装饰器 →__init_subclass__→ 最后才是元类元类的难点从来不是「怎么写出一个元类」而是「判断这里到底该不该用元类」。把三层链条、五步创建过程、能力边界这三件事记住你就能在读到框架源码时一眼看出它在做什么也能在自己写代码时避开拿元类去干实例层面工作的错。