09-元类
Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置:
oop_demo/app/infra/singleton.py测试验证:cd oop_demo && uv run pytest -k metaclass -v标记:⭐选读 — 对中级读者偏深,实际项目中很少需要自定义元类
概念铺垫
为什么需要元类?
问题场景
你在开发图书馆框架,需要某些配置类在整个系统中只存在一个实例(单例模式)。
不用元类的传统写法:
class LibraryConfig:
_instance: LibraryConfig | None = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
self.max_borrow_days = 14
self.max_books_per_member = 5
self.overdue_fine_per_day = 0.5问题:
__new__+ 类属性_instance的样板代码- 每个单例类都要复制一遍
__init__会被多次调用(每次LibraryConfig()都执行)
用元类实现:
class SingletonMeta(type):
_instances: dict[type, object] = {}
def __call__(cls, *args: object, **kwargs: object) -> object:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class LibraryConfig(metaclass=SingletonMeta):
def __init__(self) -> None:
self.max_borrow_days = 14
self.max_books_per_member = 5
self.overdue_fine_per_day = 0.5元类的价值:在类创建和实例化的过程中注入控制逻辑,让多个类共享同一套行为模式。
生活类比
元类就像"工厂的工厂" — 如果类是产品的模具(决定蛋糕的形状),元类就是制造模具的机器。你不用每次都手工雕刻模具,而是配置好机器,让它按统一规则生产所有模具。SingletonMeta 就是这样一台机器:它确保任何用它制造的"模具"(类),生产出来的"蛋糕"(实例)永远只有一个。
L1 理解层:会用
核心原理
type 是类的类
在 Python 中,一切都是对象,包括类本身:
class BookItem:
pass
type(BookItem) # <class 'type'>
type(int) # <class 'type'>
type(str) # <class 'type'>
type(type) # <class 'type'> — type 是自己的实例三层对象关系:
元类 (type)
│ 创建
↓
类对象 (BookItem, Member, LibraryConfig)
│ 创建
↓
实例对象 (book1, member1, config1)type(name, bases, namespace) 可以动态创建类:
Person = type(
"Person",
(object,),
{"name": "", "age": 0},
)
p = Person()
p.name = "张三"自定义元类:SingletonMeta
# oop_demo/app/infra/singleton.py
class SingletonMeta(type):
_instances: dict[type, object] = {}
def __call__(cls, *args: object, **kwargs: object) -> object:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]工作原理:
LibraryConfig()
↓
Python 调用 type(LibraryConfig).__call__(LibraryConfig, *args, **kwargs)
↓
SingletonMeta.__call__ 拦截
↓
检查缓存: LibraryConfig in _instances?
├── 否 → super().__call__() → 创建实例 → 存入缓存 → 返回
└── 是 → 直接返回缓存实例__call__ vs __new__ vs __init__:
| 方法 | 所属层 | 何时调用 | 职责 |
|---|---|---|---|
__call__ | 元类 | cls() 实例化时 | 控制实例创建过程 |
__new__ | 类 | 创建实例时 | 分配内存,返回实例对象 |
__init__ | 类 | __new__ 返回后 | 初始化实例属性 |
元类的 __call__ 在类的 __new__ 之前执行,因此可以完全控制是否创建新实例。
类创建流程
class MyClass(Base, metaclass=MyMeta):
attr = 100
执行步骤:
1. 准备命名空间 → namespace = {"attr": 100, "__module__": "..."}
2. 确定元类 → MyMeta(metaclass= 指定或继承)
3. __prepare__ → MyMeta.__prepare__() 返回命名空间(可选)
4. 执行类体 → 填充 namespace
5. __new__ → MyMeta.__new__(name, bases, namespace) 创建类对象
6. __init__ → MyMeta.__init__(name, bases, namespace) 初始化类对象
7. 返回类对象 → MyClass = 创建好的类大多数元类只需要重写 __new__ 或 __call__,无需触碰 __prepare__。
贯穿实战
运行 demo_ch09 查看完整演示:
cd oop_demo && uv run python -m app
# 选择 9. 元类(metaclass)或用测试验证:
cd oop_demo && uv run pytest -k metaclass -v测试覆盖:
test_singleton_meta_same_instance—LibraryConfig()两次返回同一对象test_singleton_meta_custom_class— 自定义类用SingletonMeta也是单例test_library_config_default_values— 配置默认值正确test_library_config_mutation_persists— 修改单例后全局可见
元类 vs init_subclass
| 维度 | 元类 | __init_subclass__ |
|---|---|---|
| 介入时机 | 类创建过程中 | 类创建完成后 |
| 能力 | 修改 namespace、完全控制创建 | 只能修改已创建的类 |
| 复杂度 | 高(需要理解 __new__、__call__、MRO) | 低(普通方法定义) |
| 适用场景 | 框架级控制(单例、ORM、自动属性注入) | 简单定制(插件注册、子类验证) |
| 语法 | class X(metaclass=Meta) | 在父类中定义 __init_subclass__ |
| Python 版本 | 3.0+ | 3.6+ |
选择建议: 能用 __init_subclass__ 就不用元类。元类是最后的手段。
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
优先用 __init_subclass__ | 90% 的场景足够 | 插件注册、子类验证 |
| 元类保持简单 | 复杂元类难以调试 | 只做一件事 |
| 元类名以 Meta 结尾 | 社区惯例 | SingletonMeta |
| 缓存用类属性存储 | 元类实例是共享的 | SingletonMeta._instances |
用 __call__ 控制实例化 | 在 __new__ 之前拦截 | 单例、工厂 |
| 避免在应用代码中写元类 | 过度设计,增加认知负担 | 框架代码才考虑 |
反模式:不要这样做
# ❌ 用全局变量实现单例 —— 线程不安全,可被外部修改
_config = None
def get_config():
global _config
if _config is None:
_config = {"max_borrow": 5}
return _config
# ✅ 用元类实现单例 —— CPython GIL 保护,外部无法绕过
class LibraryConfig(metaclass=SingletonMeta):
max_borrow_days: int = 14
max_books_per_member: int = 5# ❌ 元类过于复杂 —— 做了太多事情
class OverloadedMeta(type):
def __new__(mcs, name, bases, namespace):
# 注册 + 验证 + 修改属性 + 动态方法 ...
# 难以理解和调试
pass
# ✅ 元类只做一件事 —— 单例
class SingletonMeta(type):
_instances: dict[type, object] = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]# ❌ 用元类替代简单的类装饰器
class AutoReprMeta(type):
def __new__(mcs, name, bases, namespace):
namespace['__repr__'] = lambda self: f"{name}(...)"
return super().__new__(mcs, name, bases, namespace)
# ✅ 用类装饰器更简单
def auto_repr(cls):
def __repr__(self):
return f"{cls.__name__}(...)"
cls.__repr__ = __repr__
return cls适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 单例模式 | 元类 | 干净,外部无法绕过 |
| 插件注册 | __init_subclass__ | 更简单,够用 |
| ORM 模型定义 | 元类 | Django/Peewee 的典型用法 |
| 自动属性注入 | 类装饰器 | 比元类更简洁 |
| 子类验证 | __init_subclass__ | 正好的抽象级别 |
L3 专家层:深入
Python 如何实现:元类创建类的完整内幕
type.__new__ vs class 关键字
当你写 class MyClass(Base): ... 时,CPython 实际执行:
class MyClass(Base, metaclass=MyMeta):
attr = 100
def method(self): pass
CPython 内部执行步骤:
┌──────────────────────────────────────────────────┐
│ │
│ 1. 确定元类 (metaclass resolution) │
│ - 检查 class 语句中的 metaclass= 参数 │
│ - 否则检查基类的 type(最具体的共同元类) │
│ - 否则用默认的 type │
│ │
│ 2. metaclass.__prepare__(name, bases, **kw) │
│ → 返回一个映射对象(namespace) │
│ → 默认是普通 dict │
│ → 可以返回 OrderedDict 来保留字段顺序 │
│ │
│ 3. 执行类体 (exec body in namespace) │
│ → attr = 100 │
│ → def method(self): pass │
│ → __qualname__ = 'MyClass' │
│ → __module__ = '__main__' │
│ │
│ 4. metaclass.__new__(metacls, name, │
│ bases, namespace, **kw) │
│ → 创建真正的类对象 │
│ → type.__new__ 负责设置类型结构 │
│ → 用户可以在这里修改 namespace │
│ │
│ 5. metaclass.__init__(cls, name, │
│ bases, namespace, **kw) │
│ → 初始化类对象(类似 __init__) │
│ → 此时 __new__ 已返回 cls │
│ │
│ 6. 返回类对象 MyClass │
│ │
└──────────────────────────────────────────────────┘# 验证:追踪类创建全过程
class TracingMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
print(f"1. __prepare__({name}, {bases})")
return {}
def __new__(mcs, name, bases, namespace, **kwargs):
print(f"2. __new__({name}, {bases}), namespace keys: {list(namespace.keys())}")
return super().__new__(mcs, name, bases, namespace)
def __init__(cls, name, bases, namespace, **kwargs):
print(f"3. __init__({name}, {bases})")
super().__init__(name, bases, namespace)
class Demo(metaclass=TracingMeta):
x = 42
def greet(self): return "hello"
# 输出:
# 1. __prepare__(Demo, ())
# 2. __new__(Demo, ()), namespace keys: ['__module__', '__qualname__', 'x', 'greet']
# 3. __init__(Demo, ())__init_subclass__ 钩子在类创建流程中的位置:
类创建流程(包含 __init_subclass__):
┌──────────────────────────────────────────────────┐
│ │
│ metaclass.__prepare__() │
│ ↓ │
│ 执行类体 │
│ ↓ │
│ metaclass.__new__() │
│ ↓ │
│ metaclass.__init__() │
│ ↓ │
│ cls.__init_subclass__() ← 在所有基类上调用 │
│ ↓ 类已创建完成之后! │
│ 返回类对象 │
│ │
│ 关键:__init_subclass__ 是类创建完成后 │
│ 由 type.__init_subclass__ 调用的钩子 │
│ 而元类在创建过程中就能介入 │
└──────────────────────────────────────────────────┘# 验证:__init_subclass__ 在元类之后执行
class InitSubMeta(type):
def __init__(cls, name, bases, namespace):
print(f"Meta.__init__: {name}")
super().__init__(name, bases, namespace)
class Base(metaclass=InitSubMeta):
def __init_subclass__(cls, **kwargs):
print(f"Base.__init_subclass__: {cls.__name__}")
class Child(Base):
pass
# Meta.__init__: Base
# Meta.__init__: Child ← 元类先介入
# Base.__init_subclass__: Child ← 钩子在元类之后性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 类定义(含元类) | O(类体大小) | 元类的 __new__/__init__ 在类定义时执行一次 |
__call__ 拦截实例化 | O(1) | 元类 __call__ 替代默认实例化流程 |
type.__call__ 默认实例化 | O(1) + __init__ | __new__ + __init__ 开销 |
| 元类 MRO 查找 | O(1) | 元类自身也有 MRO(都继承自 type) |
| 自定义元类继承 | 额外一层函数调用 | super().__call__() 或 super().__new__() |
# 验证:元类 __call__ 的性能开销
import timeit
class Meta(type):
def __call__(cls, *args, **kwargs):
return super().__call__(*args, **kwargs)
class Normal: pass
class WithMeta(metaclass=Meta): pass
# 两者实例化性能几乎相同
print(timeit.timeit("Normal()", globals={'Normal': Normal}, number=1000000))
print(timeit.timeit("WithMeta()", globals={'WithMeta': WithMeta}, number=1000000))
# 差异 < 5%,元类的额外 __call__ 实现只有一次函数调用开销知识关联
元类知识关联:
┌───────────────┐
│ type(object) │
│ 元类的元类 │
└───────┬───────┘
│
↓
┌───────────────┐
│ 第 9 章:元类 │
│ metaclass │
│ type.__new__ │
└───────┬───────┘
│
┌────────────┼────────────┐
↓ ↓ ↓
┌───────────┐ ┌───────────┐ ┌───────────┐
│ __new__ │ │ __init__ │ │ __call__ │
│ 创建类 │ │ 初始化类 │ │ 实例化对象 │
└───────────┘ └───────────┘ └───────────┘
│
↓
┌───────────────┐
│ 第10章: │
│ __init_ │
│ subclass__ │
│ (更简单的替代)│
└───────────────┘
│
↓
类创建全流程:
__prepare__ → 执行类体 → __new__ → __init__ → __init_subclass__
↑ ↑ ↑ ↑ ↑
准备字典 填充属性 创建类 初始化类 子类钩子自检清单
type 是什么?
type是 Python 的默认元类,所有类的类型都是type;type本身也是type的实例
元类的
__call__和类的__new__有什么区别?- 元类的
__call__在cls()时调用,在类的__new__之前执行;它决定是否创建新实例以及何时创建
- 元类的
SingletonMeta 如何实现单例?
- 在
__call__中维护_instances缓存,首次调用时创建实例并缓存,后续调用直接返回缓存
- 在
为什么实际项目中很少需要自定义元类?
__init_subclass__、类装饰器、描述符等更简单的方案已覆盖大多数需求;元类增加复杂度和认知负担
元类继承时会发生什么?
- 子类继承父类的元类;多继承时如果基类有不同元类,需要创建组合元类(继承所有基类元类)来解决冲突
本章能力清单
学完本章后,你可以:
- [ ] 理解
type是所有类的元类 - [ ] 阅读和理解自定义元类的代码
- [ ] 用元类实现单例模式
- [ ] 在元类和
__init_subclass__之间做正确选择 - [ ] 描述 Python 类创建的完整流程