元类到底有什么用?

0 阅读6分钟

元类到底有什么用?

上一篇文章 为什么编程语言离不开类型?,我把类型理解成了这样一句话:

类型,是程序对规则的分类压缩。

值太多,就先按类型分组,让一批值共享同一套表示规则、操作规则和组合规则。

但顺着这条线再往下走,很快又会遇到另一个问题:

如果连“类”本身都开始大量重复,那应该怎么办?

很多元类教程一上来就讲:

  • __new__
  • __init__
  • __call__
  • type

这些东西当然没错。

但如果一开始就从这里学,最后很容易变成:

我知道元类很底层,但我还是不知道它到底有什么用。

我后来真正想明白它,是从一个更简单的角度:

元类的本质,就是规则的规则。

1. 类解决的是对象重复,元类解决的是类重复

类为什么存在?

因为对象太多,不能一个个单独写规则。

例如:

user1 = User("alice")
user2 = User("bob")
user3 = User("carol")

如果没有 User 类,每个用户对象都得单独说明:

  • 有哪些属性
  • 怎样初始化
  • 能执行哪些操作

所以我们写:

class User:
    def __init__(self, name):
        self.name = name

    def save(self):
        print(f"save {self.name}")

这一步很好理解:

对象太多
→ 用类压缩对象的共同规则

但项目继续变大以后,新的重复又会冒出来。

系统里不只有 User 这一个类,而是会出现:

class User:
    ...

class Order:
    ...

class Product:
    ...

这时候重复的已经不再是对象,而是:

  • 每个模型类都要被处理一遍
  • 每个模型类都要被注册
  • 每个模型类都要被自动补上一些能力

也就是说,开始重复的是“类的生产规则”。

这就是元类出现的位置。

2. 最实用的理解方式:先假装没有元类

如果先不谈元类,很多框架真正想做的事情,其实不过就是下面这几句:

class User:
    table = "users"
    fields = ["id", "name"]


process_model(User)  # 处理这个类,读出它的配置
register_model(User)  # 把这个类注册到框架里
add_query_methods(User)  # 给这个类补上一些统一能力

这三句的意思很直白:

  • process_model(User):处理这个类,读出它的配置
  • register_model(User):把这个类注册到框架里
  • add_query_methods(User):给这个类补上一些统一能力

如果还有 OrderProduct,你就得继续写:

class Order:
    table = "orders"
    fields = ["id", "amount"]


process_model(Order)
register_model(Order)
add_query_methods(Order)

问题一下就清楚了:

不是对象重复了,而是“定义完一个类以后,还要再做同样三件事”重复了。

元类本质上干的,就是把这几句重复动作收进去。

也就是把:

class User:
    ...

process_model(User)
register_model(User)
add_query_methods(User)

收成:

class User(Model):
    ...

然后在类创建出来的那一刻,自动完成后面的处理。

所以元类最实用的一句翻译其实是:

元类就是把“类定义完以后要重复做的那几步”搬进类创建流程里。

3. 为什么说元类是“规则的规则”

类已经是一层规则了。

例如 User 类规定了:

  • User 的实例应该长什么样
  • User() 创建出来的对象能做什么

这叫:

对象规则

但当 UserOrderProduct 这些类本身也要遵守共同规则时,我们就需要再往上一层:

  • 这些类应该怎样被处理
  • 这些类应该何时被注册
  • 这些类应该自动得到哪些能力

这时管理的就不是对象规则,而是:

类规则

所以更准确地说:

类
→ 管理对象的规则

元类
→ 管理类的规则

这就是为什么我觉得“规则的规则”比“类的类”更容易让人真正理解它。

“类的类”只是在说身份关系。

“规则的规则”才是在说它到底解决什么问题。

4. 一个最小的元类实现

下面先写一个最小版本,把刚才那三句真的做出来。

MODEL_REGISTRY = {}


def process_model(cls):
    cls.table_name = cls.__name__.lower()


def register_model(cls):
    MODEL_REGISTRY[cls.__name__] = cls


def add_query_methods(cls):
    def all_objects(inner_cls):
        print(f"query from {inner_cls.table_name}")

    cls.all = classmethod(all_objects)

如果不用元类,那么你得手工调用:

class User:
    pass


process_model(User)
register_model(User)
add_query_methods(User)

现在把它改成元类:

MODEL_REGISTRY = {}


def process_model(cls):
    cls.table_name = cls.__name__.lower()


def register_model(cls):
    MODEL_REGISTRY[cls.__name__] = cls


def add_query_methods(cls):
    def all_objects(inner_cls):
        print(f"query from {inner_cls.table_name}")

    cls.all = classmethod(all_objects)


class ModelMeta(type):
    def __new__(mcls, name, bases, attrs):
        cls = super().__new__(mcls, name, bases, attrs)

        if name != "Model":
            process_model(cls)
            register_model(cls)
            add_query_methods(cls)

        return cls


class Model(metaclass=ModelMeta):
    pass


class User(Model):
    pass


class Order(Model):
    pass


print(User.table_name)
print(Order.table_name)
User.all()
print(MODEL_REGISTRY)

输出类似:

user
order
query from user
{'User': <class '__main__.User'>, 'Order': <class '__main__.Order'>}

这里最关键的不是 __new__ 这个名字。

最关键的是你要看到:

以前:
定义完类
→ 手工 process / register / add methods

现在:
定义类时
→ 元类自动 process / register / add methods

这就是元类最小、也是最实用的价值。

5. ORM 为什么特别喜欢元类

ORM 是理解元类最典型的例子。

我们经常会写:

class User(Model):
    id = IntegerField()
    name = StringField()

表面上看,这只是定义了一个普通类。

但 ORM 真正在意的是:

  • 这个模型叫什么
  • 它有哪些字段
  • 字段类型是什么
  • 应该映射到哪张表

也就是说,ORM 看到的不是一个普通类体,而是一份声明。

于是框架会在 User 这个类真正创建出来时,统一完成这些事:

  • 扫描字段
  • 建立字段映射
  • 注册模型
  • 增加查询接口

所以元类并不是一个神秘技巧。

它只是特别适合这种场景:

你希望用户写一个类, 但框架拿到这个类以后,还要自动做一整套统一处理。

6. 所以元类到底有什么用

如果把这件事说到最短,其实就是一句话:

元类让“定义一个类”不再只是写一个类,而是顺便触发一套框架规则。

所以它最适合的场景,从来都不是“炫技”,而是:

  • ORM 模型注册
  • 序列化字段收集
  • 插件自动登记
  • 框架级统一注入能力

回到开头那条线,你会发现它和“类型”那篇其实是连着的:

值太多
→ 用类型压缩值的规则

对象太多
→ 用类压缩对象的规则

类太多
→ 用元类压缩类的生产规则

所以元类并不神秘。

它只是把“规则”这件事,又往上提了一层。