使用元类
普通类用于创建实例,元类(metaclass)用于创建类。就像实例有自己的类型一样,类对象也有自己的类型:
class User:
pass
user = User()
print(type(user) is User) # True
print(type(User) is type) # True,普通类默认由 type 创建
大多数程序只需要定义普通类,不需要直接编写元类。元类适合在“创建类的过程本身”需要统一规则时使用,例如校验所有子类必须声明某个字段、自动注册插件、收集字段定义或改变类对象的行为。它功能强,也会增加阅读、调试和继承的复杂度。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 理解类对象、
type和元类之间的关系; - 解释类语句大致如何创建类对象;
- 使用
__new__()、__init__()和__call__()定制类创建或实例化; - 编写简单的注册型或校验型元类;
- 识别元类冲突,并优先考虑
__init_subclass__()、类装饰器或普通工厂。
先看一个完整例子:自动注册插件
插件系统通常需要知道“有哪些插件可以使用”。下面的元类会在每个插件子类创建时检查 key,并把它放进注册表:
class PluginMeta(type):
registry: dict[str, type["Plugin"]] = {}
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if namespace.get("_is_plugin_base", False):
return cls
key = namespace.get("key")
if not isinstance(key, str) or not key:
raise TypeError(f"插件 {name} 必须声明非空的 key")
if key in mcls.registry:
other = mcls.registry[key].__name__
raise ValueError(f"插件 key {key!r} 已被 {other} 使用")
mcls.registry[key] = cls
return cls
class Plugin(metaclass=PluginMeta):
_is_plugin_base = True
class JsonPlugin(Plugin):
key = "json"
def run(self, data: object) -> str:
return f"JSON: {data}"
class TextPlugin(Plugin):
key = "text"
def run(self, data: object) -> str:
return str(data)
plugin_class = PluginMeta.registry["json"]
print(plugin_class().run({"ok": True})) # JSON: {'ok': True}
print(PluginMeta.registry.keys()) # dict_keys(['json', 'text'])
定义 JsonPlugin 的类语句执行完后,元类的 __new__() 立即运行,因此注册发生在导入模块时,而不是第一次创建插件实例时。这个特性很适合插件发现,但也意味着错误会在导入阶段暴露,注册逻辑必须保持清晰、可测试且没有不必要的外部副作用。
类对象和 type
type() 的两种用途
type(value) 返回对象的类型;传入三个参数时,type(name, bases, namespace) 会动态创建一个类:
def greet(self) -> str:
return f"你好,我是{self.name}。"
DynamicUser = type(
"DynamicUser",
(),
{"name": "小林", "greet": greet},
)
user = DynamicUser()
print(user.greet()) # 你好,我是小林。
三个参数分别是类名、父类元组和类命名空间。普通类语句更容易阅读;type() 适合代码生成、动态模型或框架内部,不应为了少写几行代码而替代普通 class。
type 本身也是对象
type 是最常见的元类,并且它自身也是一个类型对象:
print(type(int) is type) # True
print(type(type) is type) # True
可以把 type 理解为 Python 默认的“类工厂”:普通 class 语句默认把它作为元类。自定义元类通常继承 type,以保留标准类创建行为,再增加少量规则。
类语句大致做了什么
下面的过程不是精确的解释器源码,但足以建立使用元类所需的模型:
- 确定类名、父类和元类;
- 调用元类的
__prepare__()获取类命名空间; - 执行类体,把方法、属性和注解写入命名空间;
- 调用元类的
__new__()创建类对象;类创建过程中还会触发描述器的__set_name__()和父类的__init_subclass__(); - 调用元类的
__init__()初始化已经创建的类对象。
可以用一个日志元类观察关键步骤:
class TraceMeta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
print(f"prepare: {name}")
return {}
def __new__(mcls, name, bases, namespace, **kwargs):
print(f"new: {name}, fields={list(namespace)}")
return super().__new__(mcls, name, bases, namespace, **kwargs)
def __init__(cls, name, bases, namespace, **kwargs):
print(f"init: {name}")
super().__init__(name, bases, namespace, **kwargs)
class Example(metaclass=TraceMeta):
value = 1
实际输出还可能包含编译器生成的 __module__、__qualname__ 等名称。元类不应依赖命名空间恰好只有用户写出的字段。
元类的三个主要钩子
__new__():创建类对象
元类的 __new__() 接收类名、父类和类命名空间,返回新类。需要检查或改写类定义时,通常在调用 super().__new__() 前后完成:
class RequireIdMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
if name != "Entity" and "id" not in namespace:
raise TypeError(f"{name} 必须声明 id 字段")
return super().__new__(mcls, name, bases, namespace, **kwargs)
class Entity(metaclass=RequireIdMeta):
pass
class User(Entity):
id = 0
这里通过类名排除了基类本身。在实际框架中,使用明确的标记属性或抽象基类通常比依赖特定类名更稳健。
__init__():初始化已经创建的类
元类的 __init__() 处理的是类对象,不是类的实例:
class Meta(type):
def __init__(cls, name, bases, namespace, **kwargs):
super().__init__(name, bases, namespace, **kwargs)
cls.class_label = name.lower()
class Service(metaclass=Meta):
pass
print(Service.class_label) # service
__new__() 决定返回哪个类对象,__init__() 可以在这个对象已经存在后继续设置元数据。大多数元类把主要校验和转换放在 __new__(),并让 __init__() 只完成简单初始化。
__call__():拦截类的实例化
表达式 User(...) 实际上是在调用 type(User) 的 __call__()。默认的 type.__call__() 大致会依次执行实例类的 __new__() 和 __init__():
class LoggingMeta(type):
def __call__(cls, *args, **kwargs):
print(f"正在创建 {cls.__name__}")
instance = super().__call__(*args, **kwargs)
print(f"创建完成 {instance!r}")
return instance
class Token(metaclass=LoggingMeta):
def __init__(self, value: str):
self.value = value
def __repr__(self) -> str:
return f"Token({self.value!r})"
token = Token("abc")
覆盖 __call__() 可以做实例缓存、统一日志或参数预处理,但它会影响该元类所有类的实例化。若只是想改变一个类的构造行为,优先写普通的 __new__()、__init__() 或工厂函数。
用元类校验和收集字段
创建类时尽早失败
框架常常希望在模块导入时发现无效的类定义,而不是等到运行某个请求才失败:
class ModelMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
fields = {
key: value
for key, value in namespace.items()
if not key.startswith("_") and not callable(value)
}
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
cls._fields = fields
return cls
class Model(metaclass=ModelMeta):
pass
class UserModel(Model):
name = ""
age = 0
print(UserModel._fields) # {'name': '', 'age': 0}
上面的示例只是演示机制。真实模型框架通常使用描述器、注解或专门的 Field 对象收集字段,而不是把所有非可调用类属性都当成字段;否则类常量、描述器和内部配置也可能被误收集。
保留 __classcell__
类体中使用零参数 super() 或 __class__ 时,编译器可能会在命名空间中放入 __classcell__。元类应把完整命名空间交给 super().__new__(),不要随意过滤或重建一个丢失该键的字典:
class SafeMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
# 可以检查 namespace,但保留所有键交给 type.__new__
return super().__new__(mcls, name, bases, namespace, **kwargs)
如果手动丢弃 __classcell__,类体中的 super() 可能在运行时报告 __class__ 未设置等错误。这是编写元类时容易忽略的底层细节。
__init_subclass__():很多时候更好的选择
如果目标只是约束或注册某个类的直接、间接子类,不一定需要自定义元类。父类的 __init_subclass__() 可以在子类创建后运行:
class Plugin:
registry: dict[str, type["Plugin"]] = {}
def __init_subclass__(cls, *, key: str, **kwargs):
super().__init_subclass__(**kwargs)
if key in cls.registry:
raise ValueError(f"重复的插件 key:{key}")
cls.registry[key] = cls
cls.key = key
class JsonPlugin(Plugin, key="json"):
pass
print(Plugin.registry["json"] is JsonPlugin) # True
这个版本没有新建元类,规则直接放在拥有该协议的基类里,通常更容易阅读、继承和调试。选择 __init_subclass__() 的典型条件是:
- 规则只与一个基类体系有关;
- 不需要改变元类的
__call__()或__prepare__(); - 不需要协调多个独立框架的类创建行为。
__init_subclass__() 本身也应调用 super().__init_subclass__(**kwargs),以便多个基类的钩子协作。
类装饰器和普通工厂
类装饰器适合局部改造
类装饰器接收一个已经创建好的类并返回它或替代类:
def add_label(cls):
cls.label = cls.__name__.lower()
return cls
@add_label
class Report:
pass
print(Report.label) # report
如果只需要给几个类增加属性、包装方法或注册到表中,装饰器比影响整棵继承树的元类更局部。装饰器发生在元类创建类之后,不能替代需要参与类命名空间准备或元类继承关系的场景。
工厂函数适合动态选择类
如果需求是根据配置返回不同的类或实例,使用工厂表达意图更直接:
class CsvExporter:
def export(self, data) -> str:
return "csv"
class JsonExporter:
def export(self, data) -> str:
return "json"
def make_exporter(format_name: str):
exporters = {"csv": CsvExporter, "json": JsonExporter}
try:
return exporters[format_name]()
except KeyError as error:
raise ValueError(f"不支持的格式:{format_name}") from error
不要用元类隐藏一个本来只需要普通字典和工厂函数的选择过程。
元类冲突
两个元类必须兼容
如果多个父类的元类互不兼容,子类无法确定应由哪个元类创建:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
# class C(A, B):
# pass
# TypeError:metaclass conflict
解决冲突通常需要创建一个同时继承 MetaA 和 MetaB 的组合元类,并确保两个元类的钩子都使用协作式 super():
class CombinedMeta(MetaA, MetaB):
pass
class C(A, B, metaclass=CombinedMeta):
pass
如果第三方库已经带有复杂元类,组合元类可能并不安全。此时优先使用库提供的扩展点、类装饰器或组合对象,不要强行重写其元类。
内置元类的例子
Python 标准库和常用框架已经在使用元类:
ABCMeta用于抽象基类,跟踪抽象方法并阻止不完整的类实例化;EnumMeta管理枚举成员的创建、别名和不可重新绑定行为;- ORM、数据校验和依赖注入框架可能用元类收集字段或注册模型。
使用这些类型时,通常应继承框架提供的基类或元类,而不是替换其元类。先查阅框架文档,确认它的类创建钩子、继承约束和序列化规则。
元类与实例类的边界
容易混淆的一点是:元类里的方法作用于类对象,普通类里的方法作用于实例:
class Meta(type):
def describe(cls) -> str:
return f"类名:{cls.__name__}"
class User(metaclass=Meta):
def greet(self) -> str:
return "你好"
print(User.describe()) # 元类方法,作用于 User 这个类对象
print(User().greet()) # 普通实例方法,作用于 User 实例
如果某个行为应该由每个实例调用,放进普通类;如果行为是类对象本身的能力,才考虑类方法或元类方法。很多看似需要元类的需求,其实一个 @classmethod 就够了。
常见误区
把元类当作单例工具
元类可以通过 __call__() 缓存实例,但单例会带来全局状态、测试隔离和并发问题。需要共享资源时,优先使用显式缓存、依赖注入或模块级对象;只有生命周期和唯一性确实属于类型语义时,才考虑在元类层面实现。
在元类中做昂贵的外部操作
元类代码通常在模块导入时运行。连接数据库、读远程配置或注册网络服务会让“导入模块”产生不可预期的副作用,降低启动速度并增加测试难度。元类应主要做本地、确定性的结构检查;需要外部资源时,提供显式初始化步骤。
忽略继承和命名空间
只检查当前类命名空间可能漏掉继承字段;盲目把所有类属性当作字段又会收集到内部常量。设计元类规则时,要明确“只检查直接定义”“合并父类定义”还是“以描述器/注解为准”,并为覆盖、重复和继承场景编写测试。
忘记转发关键字参数
元类、__init_subclass__() 和其他元类钩子可能收到类定义中的关键字参数。协作式实现应消费自己认识的参数,再通过 super() 转发其余参数;不支持的参数应尽早给出清晰异常。
什么时候应该使用元类
可以按以下顺序判断:
- 需求是否发生在“创建类时”,而不是实例运行时?不是,优先使用普通方法、
property或实例装饰器。 - 是否只是注册或检查子类?先尝试
__init_subclass__()。 - 是否只需要给少数类增加属性或包装方法?先尝试类装饰器。
- 是否只是根据配置选择实现?使用工厂函数或注册表。
- 是否需要参与类命名空间准备、改变类对象行为、统一多个类体系的创建过程?这才是元类的合理候选。
- 是否能写出清晰的 MRO、继承和测试说明?如果不能,先简化设计。
小结
- 实例由类创建,类由元类创建;普通类默认使用
type作为元类; type(name, bases, namespace)可以动态创建类,但普通class通常更易读;- 元类的
__prepare__()、__new__()、__init__()和__call__()分别参与命名空间准备、类创建、类初始化和实例化; - 元类适合做创建时校验、字段收集和插件注册,但要保留完整命名空间并使用协作式
super(); __init_subclass__()、类装饰器和工厂函数通常更局部、更容易维护,应优先尝试;- 多重继承可能带来元类冲突,第三方框架的元类不能随意替换;
- 避免在元类中制造隐藏的全局状态、导入副作用和不透明的实例缓存。