Add Day_6: 面向对象(三) descriptor复刻CharField

This commit is contained in:
2026-08-18 00:22:04 +08:00
parent 2832c2772d
commit 05aa279c76
2 changed files with 163 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# 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 的"基础 → 函数 → 面向对象"闭环就完成了。
+88
View File
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""
Day_6 参考解答
=============
主题:面向对象(三)—— descriptor 复刻 models.CharField(进阶)。
建议:先自己写,写不出来再看;看完后关掉文件自己默写一遍。
"""
_NOT_SET = object() # 哨兵值:表示"字段还没赋过值"
class CharField:
def __init__(self, max_length=255):
self.max_length = max_length
self.name: str | None = None # 字段对应的属性名,由 ModelBase.__init_subclass__ 填入
def __get__(self, instance, owner):
if instance is None:
return self # 通过类访问(Article.title):返回描述符本身
return instance.__dict__.get(self.name, _NOT_SET)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError(f"{self.name} 必须是字符串")
if len(value) > self.max_length:
raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")
instance.__dict__[self.name] = value # 存到实例自己的字典里
class EmailField(CharField):
def __set__(self, instance, value):
if "@" not in value or "." not in value:
raise ValueError(f"{self.name} 不是合法的邮箱格式")
super().__set__(instance, value) # 通过校验后再走父类存储
class IntegerField:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.name: str | None = None
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name, _NOT_SET)
def __set__(self, instance, value):
# bool 是 int 的子类,所以要显式排除 True/False
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"{self.name} 必须是整数")
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} 不能小于 {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} 不能大于 {self.max_value}")
instance.__dict__[self.name] = value
class ModelBase:
# 子类定义完成后自动调用:把每个 Field 对象和它的属性名绑定
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
for attr_name, attr_value in cls.__dict__.items():
if isinstance(attr_value, (CharField, IntegerField)):
attr_value.name = attr_name
# 题目描述里想定义哪个类都可以(Article/Post 皆可),机制完全相同
class Post(ModelBase):
title = CharField(max_length=50)
content = CharField(max_length=1000)
author_email = EmailField(max_length=100)
views = IntegerField(min_value=0)
if __name__ == '__main__':
a = Post()
a.title = "Django 入门"
a.content = "跟着 Day_6 慢慢理解框架"
a.author_email = "zs@163.com"
a.views = 10
print(a.title, a.views, a.author_email) # Django 入门 10 zs@163.com
print(Post.title) # 类访问 => CharField 对象本身
# 以下三条都应该抛 ValueError,逐行取消注释验证
# a.title = "x" * 100
# a.views = -5
# a.author_email = "not-an-email"