206 lines
7.2 KiB
Python
206 lines
7.2 KiB
Python
# -*- 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
|