上一篇验证了元类的机制本身。这一篇看一个真实框架怎么重度依赖它——Django 的每一个 Model 类,它的元类都是 ModelBase。本机真实拦截发现一个反直觉的结果:title = CharField(...) 这行代码执行完之后,类属性 title 已经不是那个 CharField 对象了——它被替换成了别的东西。这一篇顺着真实源码,把这个替换过程和它背后的真实用途(用一次真实多打的 SQL 查询证明)都挖了出来。
本机真实配置了一个最小化的 Django 环境(Django 6.0.7),定义一个最简单的 Model。
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
class Meta:
app_label = '__main__'
print(type(Article))
print(type(Article).__mro__)
<class 'django.db.models.base.ModelBase'>
(<class 'django.db.models.base.ModelBase'>, <class 'type'>, <class 'object'>)
印证了上一篇的结论:Article 这个类,是 ModelBase 的实例,而 ModelBase 本身是 type 的子类——一个真正在生产环境重度使用的元类。
按直觉,Article.title 应该就是那个 CharField(max_length=200) 对象。实测不是。
print(Article.__dict__['title'])
print(type(Article.__dict__['title']))
print()
for f in Article._meta.fields:
print(f' {f.name!r}: {type(f).__name__}')
<django.db.models.query_utils.DeferredAttribute object at 0x10b715220>
<class 'django.db.models.query_utils.DeferredAttribute'>
'id': BigAutoField
'title': CharField
'body': TextField
两个意外发现:第一,Article.__dict__['title'] 是一个 DeferredAttribute,真正的 CharField 对象藏到了 Article._meta.fields 里;第二,_meta.fields 里多出来一个从没声明过的 id 字段。这两件事都是 ModelBase 干的。
ModelBase.__new__ 拿到的 attrs,就是上一篇讲过的 __build_class__ 第三步"执行类体"之后的那个命名空间——这一刻 title 还真的是 CharField 实例。
django/db/models/base.py · django @ 6.0.7, L97 class ModelBase(type): def __new__(cls, name, bases, attrs, **kwargs): ... attr_meta = attrs.pop("Meta", None) # 内部 class Meta 被摘出去 contributable_attrs = {} for obj_name, obj in attrs.items(): if _has_contribute_to_class(obj): # Field 实例都有这个方法 contributable_attrs[obj_name] = obj else: new_attrs[obj_name] = obj new_class = super_new(cls, name, bases, new_attrs, **kwargs) # 先造一个"光身"的类 ... for obj_name, obj in contributable_attrs.items(): new_class.add_to_class(obj_name, obj) # 逐个把字段"贡献"给类
四步翻译成大白话:①拿到类体执行完的命名空间,这时候 title 还是原始的 CharField;②把"有 contribute_to_class 方法的东西"(所有 Field 实例)单独摘出来,顺手把内部 class Meta 也摘出去——这解释了为什么 hasattr(Article, 'Meta') 是 False;③用剩下的"普通"属性先造出一个不带任何字段的空壳类;④对每个被摘出来的字段,调用 add_to_class,这一步才是真正的字段安装。
django/db/models/base.py · django @ 6.0.7, L391 def add_to_class(cls, name, value): if _has_contribute_to_class(value): value.contribute_to_class(cls, name) else: setattr(cls, name, value)
真正的替换动作,发生在 Field.contribute_to_class 里。
django/db/models/fields/__init__.py · django @ 6.0.7, L944 def contribute_to_class(self, cls, name, private_only=False): self.set_attributes_from_name(name) self.model = cls cls._meta.add_field(self, private=private_only) # 真正的 Field 对象注册进 _meta if self.column: setattr(cls, self.attname, self.descriptor_class(self)) # 类属性被替换成描述符
self.descriptor_class 默认就是 DeferredAttribute(定义在 Field 类上:descriptor_class = DeferredAttribute)。这一行 setattr 就是 Article.title 变成 DeferredAttribute 的真正发生地——原始的 CharField 对象只活在 _meta.fields 里,从没真的成为过类属性。
django/db/models/query_utils.py · django @ 6.0.7, L243 class DeferredAttribute: """A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.""" def __get__(self, instance, cls=None): if instance is None: return self data = instance.__dict__ field_name = self.field.attname if field_name not in data: ... instance.refresh_from_db(fields=[field_name]) # 真的会发一次新查询 return data[field_name]
这是一个真正的描述符(实现了 __get__)——类访问(Article.title)拿到的是描述符本身,实例访问(article.title)才会走 __get__ 逻辑,返回实例 __dict__ 里真正的值。名字里的"Deferred"不是随便起的:如果这个字段的值还没被加载(比如用了 .only()/.defer() 排除了它),第一次访问会触发 refresh_from_db,真的再发一次数据库查询。
本机真实建表、插入一行数据,用 .only('title') 故意排除 body 字段,监控真实发出的 SQL。
a.title 访问不产生任何新查询(数据已经在 .only() 那次查询里加载了);a.body 访问真实触发了第二次 SQL,专门去数据库补这一个字段。调用方完全看不出这中间发生了什么——a.body 用起来和普通属性一模一样,这正是描述符协议存在的意义:把"要不要查数据库"这种复杂逻辑,藏在一次看起来平平无奇的属性访问背后。
没有声明主键的话,谁来决定加一个 id?
django/db/models/options.py · django @ 6.0.7, L309 if self.pk is None: if self.parents: ... else: pk_class = self._get_default_pk_class() auto = pk_class(verbose_name="ID", primary_key=True, auto_created=True) model.add_to_class("id", auto) # 走的是和普通字段完全一样的 add_to_class
_meta._prepare() 阶段,如果扫描完所有字段发现没有主键,就用 settings.DEFAULT_AUTO_FIELD(本机默认配的 BigAutoField)造一个,再调用同一个 add_to_class 把它装上去——自动字段和手写字段走的是完全相同的机制,没有任何特殊通道。这也是为什么 _meta.fields 里 id 排在最前面:它是最先被 add_to_class 的。
复现 Article(title, body) 从类体执行完毕到最终类对象定型的完整过程。
数据来自独立验证过的 Python 模拟,复现了真实的四步流程和自动主键注入顺序;最终 _meta.fields 的顺序(id 在最前)和类 __dict__ 里每个字段对应的描述符,都用独立回放算法核对过。