RSS
菜单
全部文章快讯开发科技深度热点

元类与类创建机制(Python 从精通到入门 · 01)

内容摘要

class 不只是一段声明,它是一台由 type 驱动的类工厂。看懂这台机器,你就能亲手造类。

class 不只是一段声明,它是一台由 type 驱动的类工厂。看懂这台机器,你就能亲手造类。

你将学到

  • 为什么说「类也是对象」,以及 type 在这句话里的确切含义
  • 一条 class Foo: ... 背后完整的创建流程(__prepare__ → 命名空间 → 元类 → __init_subclass__)
  • 用元类实现单例、注册表、ORM 风格声明,以及元类 / __init_subclass__ / 类装饰器的取舍

前置知识

读完 序章 即可。本篇是大师篇第一篇,不要求你懂元类,但要求你敢读陌生代码。


一、「类也是对象」和 type 的两副面孔

在 Python 里,「对象」意味着可以被赋值、传参、被 type() 查询类型。类满足全部三条:

class Foo: pass

print(type(Foo))              # 输出: <class 'type'>
print(isinstance(Foo, type))  # 输出: True
print(type(Foo()))            # 输出: <class '__main__.Foo'>

type(Foo()) 是 Foo(实例的类型是类),而 type(Foo) 是 type(类本身是 type 的实例)。「类的类」就是元类(metaclass)。绝大多数类的元类都是 type,int、str、list 也不例外。

type 有两种完全不同的用法:

print(type(42))                 # 用法 1:查类型 -> <class 'int'>

# 用法 2:造类,type(类名, 基类元组, 命名空间字典)
Point = type("Point", (), {
    "dim": 2,
    "describe": lambda self: f"一个 {self.dim}D 点",
})

print(Point().describe())       # 输出: 一个 2D 点

这就是 class 语句的真相:class Point: ... 执行后,等价于调用 type(...) 造出一个类对象,再绑定到名字 Point。

二、一条 class 语句的完整流程

自定义元类可以插入流程的每一步:

class Meta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwds):        # 步骤 1:准备命名空间
        print(f"[1] __prepare__ 为 {name} 准备命名空间")
        return {}                                     # 必须是 mapping

    def __new__(mcs, name, bases, ns, **kwds):        # 步骤 2:创建类对象
        public = [k for k in ns if not k.startswith("__")]
        print(f"[2] __new__ 收集到的类体名字: {public}")
        return super().__new__(mcs, name, bases, ns)

    def __init__(cls, name, bases, ns, **kwds):       # 步骤 3:初始化类对象
        print(f"[3] __init__ 类 {name} 就绪")
        super().__init__(name, bases, ns)


class Base(metaclass=Meta):
    def __init_subclass__(cls, **kwds):               # 步骤 4:子类创建时触发
        print(f"[4] __init_subclass__ 被 {cls.__name__} 触发")
        super().__init_subclass__(**kwds)


print(">>> 开始定义 Child")
class Child(Base):
    x = 1
print(">>> 定义结束")

# 输出:
# [1] __prepare__ 为 Base 准备命名空间
# [2] __new__ 收集到的类体名字: []
# >>> 开始定义 Child
# [1] __prepare__ 为 Child 准备命名空间
# [2] __new__ 收集到的类体名字: ['x']
# [4] __init_subclass__ 被 Child 触发
# [3] __init__ 类 Child 就绪
# >>> 定义结束

顺序要点:

  1. 先找元类:就近取「metaclass= 参数 > 基类的元类 > type」,多个基类元类冲突会报 TypeError。
  2. __prepare__ 先于类体执行,返回一个 mapping(默认就是 dict);类体里的 x = 1、def m(...) 都写进它。
  3. 元类被调用(Meta(name, bases, ns)):__new__ 在 super().__new__ 内部一次性完成「造类 + 对描述符调用 __set_name__ + 触发父类 __init_subclass__」(见 02 - 描述符协议),之后才轮到 __init__。
  4. 类对象绑定到名字 Child。

留意输出里 [4] 出现在 [3] 之前——__init_subclass__ 在 super().__new__ 内部触发,早于元类的 __init__。顺序记不清时,跑一遍就有答案。__init_subclass__ 自动被当作 classmethod,定义时不用写 self。

三、__prepare__ 的独门用途

元类唯一「非它不可」的能力,是接管命名空间。下面让类体里的名字自动大写:

class UpperDict(dict):
    def __setitem__(self, key, value):
        if not (key.startswith("__") and key.endswith("__")):
            key = key.upper()               # 类体里的普通名字自动大写
        super().__setitem__(key, value)

class UpperMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwds):
        return UpperDict()

class Conf(metaclass=UpperMeta):
    timeout = 30
    retries = 3

print(Conf.TIMEOUT, Conf.RETRIES)           # 输出: 30 3
print("timeout" in Conf.__dict__)           # 输出: False

命名空间是可插拔的——这一点 __init_subclass__ 和类装饰器都做不到。类头还能传关键字参数,一路透传到 __prepare__ / __new__,第四节就会用到。

四、实战:单例元类

元类拦截实例化(Foo()),靠的是元类的 __call__:

import threading

class SingletonMeta(type):
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        # 这个方法拦截的是「Foo(...)」这个实例化动作
        if cls not in cls._instances:
            with cls._lock:                       # 双检锁,保证线程安全
                if cls not in cls._instances:
                    cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self, path="/etc/app.conf"):
        print(f"真正构造了一次: {path}")
        self.path = path

a = Config()
b = Config("/tmp/other.conf")     # 第二次构造被跳过
print(a is b, b.path)             # 输出: True /etc/app.conf

上面 真正构造了一次: /etc/app.conf 只会打印一次。关键点:__init__ 只在第一次真正创建时执行,所以 b.path 仍是第一次的值。

五、实战:注册表——元类 vs __init_subclass__

插件注册是最常见的元类需求。先用元类实现(顺便用上类头关键字参数):

class PluginMeta(type):
    registry = {}

    def __new__(mcs, name, bases, ns, **kwds):
        cls = super().__new__(mcs, name, bases, ns)
        if not kwds.get("abstract", False):      # 抽象基类不注册
            PluginMeta.registry[name] = cls
        return cls

class Plugin(metaclass=PluginMeta, abstract=True):
    pass

class CSVParser(Plugin): pass
class JSONParser(Plugin): pass

print(PluginMeta.registry)
# 输出: {'CSVParser': <class '__main__.CSVParser'>, 'JSONParser': <class '__main__.JSONParser'>}

同样的效果,用 __init_subclass__ 更短、更好读,也不需要引入元类:

class Plugin:
    registry = {}

    def __init_subclass__(cls, **kwds):
        super().__init_subclass__(**kwds)
        Plugin.registry[cls.__name__] = cls

class CSVParser(Plugin): ...
class JSONParser(Plugin): ...

print(Plugin.registry)     # 输出与上面完全一致

结论先行:只是「子类创建后做点什么」(注册、加校验、打标记),__init_subclass__ 就够,别上元类。

六、实战:ORM 风格声明(元类 + 描述符)

真正的元类场景是在类创建时收集字段信息,这需要读到完整命名空间:

class Field:
    def __init__(self, column=None):
        self.column = column
    def __set_name__(self, owner, name):     # 类创建完成后自动被调用
        self.name = name
        if self.column is None:
            self.column = name

class ModelMeta(type):
    def __new__(mcs, name, bases, ns):
        cls = super().__new__(mcs, name, bases, ns)
        fields = {}
        for klass in reversed(cls.__mro__):  # reversed 保证子类覆盖父类
            for key, value in vars(klass).items():
                if isinstance(value, Field):
                    fields[key] = value
        cls._fields = fields
        return cls

class Model(metaclass=ModelMeta):
    def save(self):
        cols = ", ".join(f.column for f in self._fields.values())
        return f"INSERT INTO {type(self).__name__.lower()} ({cols}) VALUES (...)"

class User(Model):
    id = Field()                      # 列名自动取属性名 id
    name = Field(column="user_name")  # 显式指定列名

print(list(User._fields))             # 输出: ['id', 'name']
print(User._fields["name"].column)    # 输出: user_name
print(User().save())
# 输出: INSERT INTO user (id, user_name) VALUES (...)

_fields 是类级别的元数据——ORM、序列化框架(Pydantic、DRF Serializer)、表单库都靠这一招在类定义时把结构摸清楚,而不是运行时靠反射硬猜。

七、三者取舍:别拿大炮打蚊子

需求 首选方案
子类创建后做点事(注册、校验、打标记) __init_subclass__
给类追加/替换方法,不需要读类体语义 类装饰器
需要修改命名空间(重复键、注入名字、动态运算) 元类 __prepare__
需要控制类对象本身(拦截实例化、改 __call__/__getattr__) 元类

能用 __init_subclass__ 就别用类装饰器,能用类装饰器就别用元类。

元类有两个真实成本:元类冲突(多个基类元类不一致直接 TypeError),以及心智负担——读代码的人必须知道元类是什么。只有 __prepare__ 和「控制类对象」这两类需求,才值得付这个成本。

常见坑

坑 1:元类冲突。 多继承时各基类元类不一致会直接崩:

# ❌ 两个基类的元类互不相干
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

# ✅ 让元类形成继承关系,取最「派生」的那个
class MetaB2(MetaA): pass      # MetaB2 继承 MetaA
class B2(metaclass=MetaB2): pass
class C2(A, B2): pass
print(type(C2))                # 输出: <class '__main__.MetaB2'>

坑 2:__new__ 忘了把类对象 return 出去。

# ❌ 忘了 return,类名会绑定成 None(不报错,更坑)
class Meta(type):
    def __new__(mcs, name, bases, ns):
        pass
class A(metaclass=Meta): pass
print(A)                       # 输出: None
A()                            # TypeError: 'NoneType' object is not callable

# ✅ 校验归校验,最后一定把类对象交出去
class Meta2(type):
    def __new__(mcs, name, bases, ns):
        return super().__new__(mcs, name, bases, ns)
class A2(metaclass=Meta2): pass
print(A2)                      # 输出: <class '__main__.A2'>

坑 3:把普通方法写进元类,误以为是实例方法。

# ❌ Meta 上定义的 hello,接收的是「类」,不是实例
class Meta(type):
    def hello(cls): return f"hi {cls.__name__}"
class Foo(metaclass=Meta): pass
print(Foo.hello())        # 输出: hi Foo   —— 类上能调用
print(Foo().hello())      # AttributeError: 'Foo' object has no attribute 'hello'

# ✅ 元类方法服务于「类」,实例方法写进类体
class Bar(metaclass=Meta):
    def hello(self): return "hi instance"
print(Bar().hello())      # 输出: hi instance

小结

  • 类也是对象:类的「类」就是元类,默认是 type。
  • 一条 class 语句 = 找元类 → __prepare__ → 执行类体填命名空间 → 元类 __new__(内含 __set_name__ 与父类 __init_subclass__)→ 元类 __init__。
  • type(name, bases, ns) 是手工造类的入口;元类的独有能力是 __prepare__(改命名空间)和拦截实例化(__call__)。
  • 取舍顺序:__init_subclass__ > 类装饰器 > 元类。注册表这类需求优先用 __init_subclass__,别为小事引入元类冲突的风险。

延伸阅读

  • Python 官方文档 Data Model:Metaclasses
  • 《Fluent Python》第 23 章,以及 Django Model、Pydantic 的字段收集源码
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《元类与类创建机制(Python 从精通到入门 · 01)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。