Skip to content

09-元类

Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置oop_demo/app/infra/singleton.py测试验证cd oop_demo && uv run pytest -k metaclass -v标记:⭐选读 — 对中级读者偏深,实际项目中很少需要自定义元类


概念铺垫

为什么需要元类?

问题场景

你在开发图书馆框架,需要某些配置类在整个系统中只存在一个实例(单例模式)。

不用元类的传统写法:

python
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() 都执行)

用元类实现:

python
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 中,一切都是对象,包括类本身:

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) 可以动态创建类:

python
Person = type(
    "Person",
    (object,),
    {"name": "", "age": 0},
)
p = Person()
p.name = "张三"

自定义元类:SingletonMeta

python
# 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 查看完整演示:

bash
cd oop_demo && uv run python -m app
# 选择 9. 元类(metaclass)

或用测试验证:

bash
cd oop_demo && uv run pytest -k metaclass -v

测试覆盖:

  • test_singleton_meta_same_instanceLibraryConfig() 两次返回同一对象
  • 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__ 之前拦截单例、工厂
避免在应用代码中写元类过度设计,增加认知负担框架代码才考虑

反模式:不要这样做

python
# ❌ 用全局变量实现单例 —— 线程不安全,可被外部修改
_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
python
# ❌ 元类过于复杂 —— 做了太多事情
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]
python
# ❌ 用元类替代简单的类装饰器
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                           │
│                                                  │
└──────────────────────────────────────────────────┘
python
# 验证:追踪类创建全过程
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__ 调用的钩子       │
│        而元类在创建过程中就能介入                 │
└──────────────────────────────────────────────────┘
python
# 验证:__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__()
python
# 验证:元类 __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__
       ↑              ↑          ↑         ↑             ↑
    准备字典      填充属性    创建类    初始化类     子类钩子

自检清单

  1. type 是什么?

    • type 是 Python 的默认元类,所有类的类型都是 typetype 本身也是 type 的实例
  2. 元类的 __call__ 和类的 __new__ 有什么区别?

    • 元类的 __call__cls() 时调用,在类的 __new__ 之前执行;它决定是否创建新实例以及何时创建
  3. SingletonMeta 如何实现单例?

    • __call__ 中维护 _instances 缓存,首次调用时创建实例并缓存,后续调用直接返回缓存
  4. 为什么实际项目中很少需要自定义元类?

    • __init_subclass__、类装饰器、描述符等更简单的方案已覆盖大多数需求;元类增加复杂度和认知负担
  5. 元类继承时会发生什么?

    • 子类继承父类的元类;多继承时如果基类有不同元类,需要创建组合元类(继承所有基类元类)来解决冲突

本章能力清单

学完本章后,你可以:

  • [ ] 理解 type 是所有类的元类
  • [ ] 阅读和理解自定义元类的代码
  • [ ] 用元类实现单例模式
  • [ ] 在元类和 __init_subclass__ 之间做正确选择
  • [ ] 描述 Python 类创建的完整流程