From 05aa279c7606043475c0ccf87956875cec1252bc Mon Sep 17 00:00:00 2001 From: lkjing2021345 <1908499668@qq.com> Date: Tue, 18 Aug 2026 00:22:04 +0800 Subject: [PATCH] =?UTF-8?q?Add=20Day=5F6:=20=E9=9D=A2=E5=90=91=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1(=E4=B8=89)=20descriptor=E5=A4=8D=E5=88=BBCharField?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Day_6/README.md | 75 ++++++++++++++++++++++++++++++++++++++++ Day_6/参考解答.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 Day_6/README.md create mode 100644 Day_6/参考解答.py diff --git a/Day_6/README.md b/Day_6/README.md new file mode 100644 index 0000000..1e279b8 --- /dev/null +++ b/Day_6/README.md @@ -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 的"基础 → 函数 → 面向对象"闭环就完成了。 diff --git a/Day_6/参考解答.py b/Day_6/参考解答.py new file mode 100644 index 0000000..2254dcc --- /dev/null +++ b/Day_6/参考解答.py @@ -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"