重构: Day_2拆分/Day_3延后为Day_4-6/Django改为进阶任务
This commit is contained in:
-145
@@ -1,145 +0,0 @@
|
||||
# Day_3 · 面向对象 × 手写一个小 ORM
|
||||
|
||||
> Django 最核心的能力是 **ORM(对象关系映射)**:你定义类,Django 帮你管理数据库表。
|
||||
> 打开任何 Django 项目,第一行几乎都是 `class Article(models.Model):`——
|
||||
> 这一行的背后,是整片面向对象的大海。
|
||||
> 今天我们不装 Django,用纯 Python 把它的 Model 一层层拆开看:
|
||||
> 类、实例、继承、魔法方法、property、描述符(descriptor)。
|
||||
> 全部做完,你会第一次"看穿"框架的魔法。
|
||||
|
||||
## 今日知识点 → 以后在 Django 里对应什么
|
||||
|
||||
| 今天练的 | 以后在 Django 里会看到 |
|
||||
| --- | --- |
|
||||
| 类定义 + 类属性 | `class Article(models.Model)`,字段就是类属性 |
|
||||
| `__init__` / 实例方法 | Model 实例的构造、`.save()` / `.delete()` |
|
||||
| `__str__` / `__repr__` 魔法方法 | Admin 后台显示的名称、QuerySet 打印结果 |
|
||||
| 继承与 `super()` | 所有 Model 继承 `models.Model`;自定义 Manager |
|
||||
| `@property`、描述符与封装 | Model 上的派生字段、字段校验(Field 机制) |
|
||||
|
||||
---
|
||||
|
||||
## 题目一(必做):设计一个 User 类
|
||||
|
||||
**背景**:Django 里定义一个用户模型只需要三行:
|
||||
|
||||
```python
|
||||
class User(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
email = models.EmailField()
|
||||
```
|
||||
|
||||
看起来像魔法,其实核心就是"类 + 类属性"。今天我们不用 Django,用纯 Python 复刻 User 类。
|
||||
|
||||
**要求**:
|
||||
|
||||
1. 类属性 `all_users = []`(模拟数据库表,用来存所有创建出来的用户)
|
||||
2. `__init__(self, name, email, age)`:把三个参数存成实例属性,并把 `self` 追加进 `all_users`
|
||||
3. 实例方法 `get_full_name()`:返回 `用户:<name>(<email>)`
|
||||
4. 魔法方法 `__str__(self)`:返回 `self.name`(Django admin 后台显示的就是它!)
|
||||
5. 魔法方法 `__repr__(self)`:返回 `User(name='zhangsan', email='zs@163.com', age=20)`
|
||||
这种"能还原出原对象"的格式
|
||||
|
||||
**验证**:
|
||||
|
||||
- 创建 3 个用户,打印 `User.all_users`
|
||||
- 分别 `print` 每个用户(会触发 `__str__`)
|
||||
- 用 `eval(repr(u1))` 还原出 u1 的信息,确认内容一致
|
||||
|
||||
**思考题(写在注释里)**:`__str__` 和 `__repr__` 到底有什么区别?
|
||||
(提示:一个是"给人看的",一个是"给开发者调试看的"——具体从使用场景和返回值要求去想。)
|
||||
|
||||
---
|
||||
|
||||
## 题目二(必做):继承 —— 复刻 models.Model
|
||||
|
||||
**背景**:Django 里 `Article` 没有写 `__init__`,没有写 `save()`,却能 `.save()`、能查 `objects`。
|
||||
为什么?因为通用能力都在父类 `models.Model` 里,子类只需要写自己的字段。
|
||||
今天我们自己写这个父类。
|
||||
|
||||
**要求**:
|
||||
|
||||
1. 定义基类 `BaseModel`:
|
||||
- 类属性 `objects = []`(模拟数据库表)
|
||||
- `save(self)`:把 `self` 追加进 `objects`,并打印 `[DB] 已保存 <类名>: <str(self)>`
|
||||
- `delete(self)`:把 `self` 从 `objects` 中移除,打印 `[DB] 已删除 <str(self)>`
|
||||
- `to_dict(self)`:把实例的所有属性转成字典(提示:`self.__dict__` 是什么?)
|
||||
- `__str__(self)`:默认返回 `<类名#id>`,比如 `<Article#1405...>`
|
||||
2. 定义 `Article(BaseModel)`:`__init__` 接收 `title, content, author` 存成实例属性,
|
||||
再用 `time` 模块记录 `created_at`(形如 `2026-08-18 10:30:00`)
|
||||
3. 定义 `Comment(BaseModel)`:`__init__` 接收 `content, author`
|
||||
4. 全流程走一遍:创建文章和评论 → save → 遍历 `BaseModel.objects` 打印 → `to_dict()` → delete
|
||||
5. 验证继承关系:`print(issubclass(Article, BaseModel))` 应为 `True`,
|
||||
`print(isinstance(a, BaseModel))` 应为 `True`
|
||||
|
||||
**进阶(思考 + 动手)**:如果要求"Article 保存前先打印一行日志",而 `save()` 写在父类里,
|
||||
怎么办?(提示:在子类里**重写** `save()`,先打印,再 `super().save()`。试试看输出顺序。)
|
||||
|
||||
**出题意图**:Django 的 Model 就是"通用逻辑放父类,子类只写自己的字段"。
|
||||
理解了继承 + `super()`,你就理解了为什么 Django 模型"不写 save 却能保存"。
|
||||
|
||||
---
|
||||
|
||||
## 题目三(进阶):类属性当"字段声明"——复刻 models.CharField
|
||||
|
||||
**背景**:Django 里这样声明字段:
|
||||
|
||||
```python
|
||||
class Article(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
content = models.TextField()
|
||||
```
|
||||
|
||||
`title` 是**类属性**,但每个实例的 `title` 值又各自不同——这是怎么做到的?
|
||||
答案是 **descriptor(描述符)协议**:类属性如果是实现了 `__get__` / `__set__` 的对象,
|
||||
那么对实例属性的一切读写都会先经过它。Django 的 Field 就是这种对象。
|
||||
|
||||
**要求**:
|
||||
|
||||
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)` 时,看到的就不是魔法,而是你今天亲手写过的代码。
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 题目一:User 类全流程 + 想通了 `__str__` / `__repr__` 的区别
|
||||
- [ ] 题目二:BaseModel + Article + Comment 全流程 + 继承验证(进阶则 +1)
|
||||
- [ ] 题目三:三种 Field 的校验全部生效(难度较高,做不出来先把题目二的继承吃透)
|
||||
|
||||
完成后打开 `参考解答.py` 对照。
|
||||
|
||||
> 进度提示:Day_3 的题目三是整个系列里最难的一题,卡住很正常。
|
||||
> 它对应的正是 Day_4 你真正写 Django Model 时会遇到的机制。
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Day_3 参考解答
|
||||
=============
|
||||
用纯 Python 复刻 Django Model 的机制。
|
||||
建议:先自己写,写不出来再看;看完后关掉文件自己默写。
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
# ============================================================
|
||||
# 题目一:设计一个 User 类
|
||||
# ============================================================
|
||||
|
||||
class User:
|
||||
all_users = [] # 类属性:模拟数据库表
|
||||
|
||||
def __init__(self, name, email, age):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.age = age
|
||||
User.all_users.append(self) # 把新用户"存表"
|
||||
|
||||
def get_full_name(self):
|
||||
return f"用户:{self.name}({self.email})"
|
||||
|
||||
def __str__(self):
|
||||
# 给"人"看的:print(user) 时显示,Django admin 列表里显示的就是它
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
# 给"开发者调试"看的:要能还原出原对象,eval(repr(u)) 应得到相同信息
|
||||
return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
u1 = User("zhangsan", "zs@163.com", 20)
|
||||
u2 = User("lisi", "ls@163.com", 21)
|
||||
u3 = User("wangwu", "ww@163.com", 19)
|
||||
|
||||
for u in User.all_users:
|
||||
print(u, "->", repr(u))
|
||||
# zhangsan -> User(name='zhangsan', email='zs@163.com', age=20)
|
||||
# lisi -> User(name='lisi', email='ls@163.com', age=21)
|
||||
# wangwu -> User(name='wangwu', email='ww@163.com', age=19)
|
||||
|
||||
restored = eval(repr(u1)) # __repr__ 的用途:可还原
|
||||
print(restored.name, restored.email) # zhangsan zs@163.com
|
||||
|
||||
# 思考题答案:
|
||||
# __str__ 面向普通用户/展示,__repr__ 面向开发者/调试,且要求无歧义可还原;
|
||||
# 只实现 __repr__ 时,print() 会退而使用 __repr__;反之不会。
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 题目二:继承 —— 复刻 models.Model
|
||||
# ============================================================
|
||||
|
||||
class BaseModel:
|
||||
objects = [] # 类属性:模拟数据库表(所有子类共用)
|
||||
|
||||
def save(self):
|
||||
BaseModel.objects.append(self)
|
||||
print(f"[DB] 已保存 {type(self).__name__}: {self}")
|
||||
|
||||
def delete(self):
|
||||
BaseModel.objects.remove(self)
|
||||
print(f"[DB] 已删除 {self}")
|
||||
|
||||
def to_dict(self):
|
||||
return dict(self.__dict__) # 实例的所有属性 => 字典
|
||||
|
||||
def __str__(self):
|
||||
return f"<{type(self).__name__}#{id(self)}>"
|
||||
|
||||
|
||||
class Article(BaseModel):
|
||||
def __init__(self, title, content, author):
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.author = author
|
||||
self.created_at = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 进阶:重写 save,先打日志,再调用父类逻辑
|
||||
def save(self):
|
||||
print(f"[LOG] 正在保存文章《{self.title}》...")
|
||||
super().save() # 父类的 save 负责真正"入库"
|
||||
|
||||
def __str__(self):
|
||||
return f"<Article: {self.title}>"
|
||||
|
||||
|
||||
class Comment(BaseModel):
|
||||
def __init__(self, content, author):
|
||||
self.content = content
|
||||
self.author = author
|
||||
|
||||
def __str__(self):
|
||||
return f"<Comment: {self.content[:10]}...>"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
a1 = Article("Django 入门", "从 Hello World 开始", "zhangsan")
|
||||
a1.save() # 触发重写后的 save(带日志 + super)
|
||||
c1 = Comment("写得太好了!", "lisi")
|
||||
c1.save()
|
||||
|
||||
for obj in BaseModel.objects:
|
||||
print(obj.to_dict())
|
||||
|
||||
print(issubclass(Article, BaseModel)) # True
|
||||
print(isinstance(a1, BaseModel)) # True
|
||||
|
||||
a1.delete()
|
||||
print(len(BaseModel.objects)) # 1(只剩评论)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 题目三(进阶):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_3 慢慢理解框架"
|
||||
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"
|
||||
|
||||
# 尚未赋值的字段返回哨兵(不是属性不存在,也不是默认值)
|
||||
print(a.__dict__.get("views")) # 已被赋值为 10
|
||||
Reference in New Issue
Block a user