元类到底有什么用?
上一篇文章 为什么编程语言离不开类型?,我把类型理解成了这样一句话:
类型,是程序对规则的分类压缩。
值太多,就先按类型分组,让一批值共享同一套表示规则、操作规则和组合规则。
但顺着这条线再往下走,很快又会遇到另一个问题:
如果连“类”本身都开始大量重复,那应该怎么办?
很多元类教程一上来就讲:
__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):给这个类补上一些统一能力
如果还有 Order、Product,你就得继续写:
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()创建出来的对象能做什么
这叫:
对象规则
但当 User、Order、Product 这些类本身也要遵守共同规则时,我们就需要再往上一层:
- 这些类应该怎样被处理
- 这些类应该何时被注册
- 这些类应该自动得到哪些能力
这时管理的就不是对象规则,而是:
类规则
所以更准确地说:
类
→ 管理对象的规则
元类
→ 管理类的规则
这就是为什么我觉得“规则的规则”比“类的类”更容易让人真正理解它。
“类的类”只是在说身份关系。
“规则的规则”才是在说它到底解决什么问题。
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 模型注册
- 序列化字段收集
- 插件自动登记
- 框架级统一注入能力
回到开头那条线,你会发现它和“类型”那篇其实是连着的:
值太多
→ 用类型压缩值的规则
对象太多
→ 用类压缩对象的规则
类太多
→ 用元类压缩类的生产规则
所以元类并不神秘。
它只是把“规则”这件事,又往上提了一层。