# Day_6 · 面向对象(三):复刻 models.CharField(进阶) > 这是整个系列最难的一题,做不出来很正常——卡住就回头把 Day_4 / Day_5 再过一遍。 > Django 里这样声明字段: > > ```python > class Article(models.Model): > title = models.CharField(max_length=50) > ``` > > `title` 是**类属性**,但每个实例的 `title` 值又各自不同——这是怎么做到的? > 答案是 **descriptor(描述符)协议**:类属性如果是实现了 `__get__` / `__set__` 的对象, > 那么对实例属性的一切读写都会先经过它。Django 的 Field 就是这种对象。 ## 今日知识点 → 以后在 Django 里对应什么 | 今天练的 | 以后在 Django 里会看到 | | --- | --- | | descriptor(`__get__` / `__set__`) | Django Field 的核心机制 | | 类属性声明字段 | `models.CharField(max_length=50)` 的真相 | | `__init_subclass__` 元编程 | 框架"自动注册"类信息的通用手段 | --- ## 题目(必做):类属性当"字段声明"——复刻 models.CharField **要求**: 1. 定义字段类型类(都实现 `__get__` 和 `__set__`): - `CharField(max_length)`:`__set__` 里校验"必须是字符串、长度 ≤ max_length",否则抛 `ValueError` - `EmailField`:继承 `CharField`,额外校验必须含 `@` 和 `.` - `IntegerField(min_value, max_value)`:校验"必须是 int(注意 `True` 不算)、在范围内" 值的存储要写到**实例自己的** `__dict__` 里;还没赋过值时返回一个哨兵对象 (用一个模块级 `_NOT_SET = object()`)。`__get__` 里 `instance is None` 表示 "通过类访问",此时应返回描述符本身(这样 `Article.title` 看到的是 Field 对象, 这正是 Django 里 `Article.title` 的行为)。 2. 写一个基类 `ModelBase`,用 `__init_subclass__`(在子类定义完成后自动调用) 遍历子类的 `cls.__dict__`,把每个 Field 对象的 `.name` 填上它的属性名。 这样描述符才知道自己对应哪个属性。 3. 定义 `Article(ModelBase)`,用类属性声明字段: ```python class Article(ModelBase): title = CharField(max_length=50) content = CharField(max_length=1000) author_email = EmailField(max_length=100) views = IntegerField(min_value=0) ``` 4. 测试: - 正常赋值、读取,一切正常 - `a.title = "x" * 100` → 抛 `ValueError`(超长) - `a.views = -5` → 抛 `ValueError`(越界) - `a.author_email = "not-an-email"` → 抛 `ValueError`(格式错) - `print(Article.title)` → 打印出的是 `CharField` 对象本身 **出题意图**:这就是 Django Field 的核心机制。做完这一题,你再去看 `models.CharField(max_length=50)` 时,看到的就不是魔法,而是你今天亲手写过的代码。 --- ## 完成标准 - [ ] 三种 Field 的校验全部生效 - [ ] 类访问(`Article.title`)返回描述符本身 完成后打开 `参考解答.py` 对照。 > 进度提示:Day_6 是整个系列里最难的一题,卡住很正常。 > 它对应的正是后面写真正 Django Model 时会遇到的机制。 > 这一关过了,Day_2~Day_6 的"基础 → 函数 → 面向对象"闭环就完成了。