Files
DailyChallenge_ProgrammingD…/Day_3/README.md
T

6.8 KiB
Raw Blame History

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 类。

要求:

  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 里这样声明字段:

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),用类属性声明字段:

    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 时会遇到的机制。