Add Day_6: 面向对象(三) descriptor复刻CharField
This commit is contained in:
@@ -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 的"基础 → 函数 → 面向对象"闭环就完成了。
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user