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 里定义一个用户模型只需要三行:
class User(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField()
看起来像魔法,其实核心就是"类 + 类属性"。今天我们不用 Django,用纯 Python 复刻 User 类。
要求:
- 类属性
all_users = [](模拟数据库表,用来存所有创建出来的用户) __init__(self, name, email, age):把三个参数存成实例属性,并把self追加进all_users- 实例方法
get_full_name():返回用户:<name>(<email>) - 魔法方法
__str__(self):返回self.name(Django admin 后台显示的就是它!) - 魔法方法
__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 里,子类只需要写自己的字段。
今天我们自己写这个父类。
要求:
- 定义基类
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...>
- 类属性
- 定义
Article(BaseModel):__init__接收title, content, author存成实例属性, 再用time模块记录created_at(形如2026-08-18 10:30:00) - 定义
Comment(BaseModel):__init__接收content, author - 全流程走一遍:创建文章和评论 → save → 遍历
BaseModel.objects打印 →to_dict()→ delete - 验证继承关系:
print(issubclass(Article, BaseModel))应为True,print(isinstance(a, BaseModel))应为True
进阶(思考 + 动手):如果要求"Article 保存前先打印一行日志",而 save() 写在父类里,
怎么办?(提示:在子类里重写 save(),先打印,再 super().save()。试试看输出顺序。)
出题意图:Django 的 Model 就是"通用逻辑放父类,子类只写自己的字段"。
理解了继承 + super(),你就理解了为什么 Django 模型"不写 save 却能保存"。
题目三(进阶):类属性当"字段声明"——复刻 models.CharField
背景:Django 里这样声明字段:
class Article(models.Model):
title = models.CharField(max_length=50)
content = models.TextField()
title 是类属性,但每个实例的 title 值又各自不同——这是怎么做到的?
答案是 descriptor(描述符)协议:类属性如果是实现了 __get__ / __set__ 的对象,
那么对实例属性的一切读写都会先经过它。Django 的 Field 就是这种对象。
要求:
-
定义字段类型类(都实现
__get__和__set__):CharField(max_length):__set__里校验"必须是字符串、长度 ≤ max_length",否则抛ValueErrorEmailField:继承CharField,额外校验必须含@和.IntegerField(min_value, max_value):校验"必须是 int(注意True不算)、在范围内"
值的存储要写到实例自己的
__dict__里;还没赋过值时返回一个哨兵对象 (用一个模块级_NOT_SET = object())。__get__里instance is None表示 "通过类访问",此时应返回描述符本身(这样Article.title看到的是 Field 对象, 这正是 Django 里Article.title的行为)。 -
写一个基类
ModelBase,用__init_subclass__(在子类定义完成后自动调用) 遍历子类的cls.__dict__,把每个 Field 对象的.name填上它的属性名。 这样描述符才知道自己对应哪个属性。 -
定义
Article(ModelBase),用类属性声明字段:class Article(ModelBase): title = CharField(max_length=50) content = CharField(max_length=1000) author_email = EmailField(max_length=100) views = IntegerField(min_value=0) -
测试:
- 正常赋值、读取,一切正常
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 时会遇到的机制。