Compare commits
2 Commits
16e8724fcd
...
19fe528076
| Author | SHA1 | Date | |
|---|---|---|---|
| 19fe528076 | |||
| 604b9b37c1 |
-141
@@ -1,141 +0,0 @@
|
||||
# Day_2 · Django 视角下的 Python 基础
|
||||
|
||||
> 今天还不用学 Django,也不需要会面向对象。
|
||||
> 下面的题目只用到基础语法(字典、列表、字符串、函数、异常处理),
|
||||
> 但每一题都是在模拟后端开发中**真正会遇到**的场景。
|
||||
> 做完你会发现:Django 里那些看似神秘的东西,底层其实就是这些基础语法。
|
||||
|
||||
## 今日知识点 → 以后在 Django / Celery 里对应什么
|
||||
|
||||
| 今天练的 | 以后在 Django/Celery 里会看到 |
|
||||
| --- | --- |
|
||||
| 字典、列表的增删改查 | ORM 查询结果、`request.GET`/`request.POST`、JSON 响应 |
|
||||
| 字符串分割与格式化 | URL 路由解析、日志输出、模板渲染 |
|
||||
| 函数、`*args`/`**kwargs` | 视图函数 `def index(request, **kwargs)`、中间件、信号 |
|
||||
| 异常处理(自定义异常 + try/except) | `Http404`、`ValidationError`、Celery 任务失败与重试 |
|
||||
|
||||
---
|
||||
|
||||
## 题目一(必做):解析 URL 查询参数
|
||||
|
||||
**背景**:浏览器访问 `https://example.com/register?username=zhangsan&age=20&city=qingdao`,
|
||||
`?` 后面的 `username=zhangsan&age=20&city=qingdao` 叫做**查询参数(query string)**。
|
||||
Django 里 `request.GET` 就是帮你解析好的字典。今天我们手写一个解析器,看清它背后发生了什么。
|
||||
|
||||
**要求**:写函数 `parse_query_string(query)`,输入一行查询参数(不含 `?`),返回一个字典。
|
||||
|
||||
必须处理的边界情况:
|
||||
|
||||
1. 空输入 → 返回空字典 `{}`
|
||||
2. 重复键,如 `name=a&name=b` → 后者覆盖前者:`{'name': 'b'}`
|
||||
3. 参数没有 `=`,如 `flag` → 值为空字符串:`{'flag': ''}`
|
||||
4. 值为空,如 `name=` → `{'name': ''}`
|
||||
|
||||
**输入输出示例**
|
||||
|
||||
```
|
||||
输入: username=zhangsan&age=20&city=qingdao
|
||||
输出: {'username': 'zhangsan', 'age': '20', 'city': 'qingdao'}
|
||||
|
||||
输入: name=a&name=b&flag
|
||||
输出: {'name': 'b', 'flag': ''}
|
||||
|
||||
输入: (空字符串)
|
||||
输出: {}
|
||||
```
|
||||
|
||||
**进阶(加分)**:URL 编码还原——把值中的 `+` 还原为空格,`%20` 也还原为空格。
|
||||
如 `name=Zhang+San` → `{'name': 'Zhang San'}`。
|
||||
|
||||
> 提示:
|
||||
> - 先 `split('&')` 把整体切成若干段,再对每一段 `split('=', 1)`。
|
||||
> 想想为什么 `maxsplit` 要写成 1?(如果值里本身就含 `=` 会怎样?)
|
||||
> - 字典赋值 `d[key] = value` 天然处理了"覆盖"和"空值"两种情况。
|
||||
|
||||
**出题意图**:Django 的 `QueryDict`(`request.GET`)底层干的就是这件事。
|
||||
把它吃透,以后看到 `request.GET.get('page')` 就不再是黑魔法,而是老朋友。
|
||||
|
||||
---
|
||||
|
||||
## 题目二(必做):万能日志记录器(函数 + *args/**kwargs)
|
||||
|
||||
**背景**:后端程序要记录"哪个函数被调用了、用了多久",这叫**日志(logging)**。
|
||||
Django 中间件、Celery 任务、接口调用统计里,到处都是这种"给函数包一层"的操作。
|
||||
|
||||
**要求**:写函数 `log_call(func)`,它接收一个**函数**作为参数,返回一个**新函数**。
|
||||
新函数在被调用时:
|
||||
|
||||
1. 打印 `[LOG] 调用函数 <函数名>,参数为 args=(...), kwargs={...}`
|
||||
2. 调用原函数,拿到结果
|
||||
3. 打印 `[LOG] <函数名> 执行完成,耗时 x.xxxx 秒`(用 `time.time()` 算耗时)
|
||||
4. 返回原函数的结果
|
||||
|
||||
参数要用 `*args` 和 `**kwargs` 打包——因为你不知道原函数会接收什么参数。
|
||||
|
||||
**示例**
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
logged_add = log_call(add)
|
||||
print(logged_add(1, 2))
|
||||
# [LOG] 调用函数 add,参数为 args=(1, 2), kwargs={}
|
||||
# [LOG] add 执行完成,耗时 0.0001 秒
|
||||
# 3
|
||||
```
|
||||
|
||||
**进阶(加分)**:给 `log_call` 加第二个参数 `label`(默认空字符串),
|
||||
打印日志时带上标签,如 `log_call(add, label="核心计算")` → `[核心计算][LOG] ...`。
|
||||
|
||||
> 提示:如果你已经看过装饰器语法,可以用 `@log_call` 直接写在函数头上;
|
||||
> 没学过就写成 `log_call(add)(1, 2)` 这种普通调用,两种都行。
|
||||
|
||||
**出题意图**:Django 视图函数接收 `request`、URL 参数通过 `**kwargs` 传进来;
|
||||
Celery 的任务、日志中间件全部建立在"函数作为参数 + `*args/**kwargs`"之上。
|
||||
这是后端开发里出现频率最高的 Python 技巧,没有之一。
|
||||
|
||||
---
|
||||
|
||||
## 题目三(进阶):模拟注册接口的参数校验
|
||||
|
||||
**背景**:后端接口收到请求后的第一件事永远是**校验参数**。
|
||||
字段缺失、格式不对,都要返回明确的错误信息,绝不能带着脏数据往下走。
|
||||
Django 里抛 `ValidationError`、Celery 里任务失败后重试——这一切的起点都是 try/except。
|
||||
|
||||
**要求**:
|
||||
|
||||
1. 定义一个字典模拟前端传来的注册数据:
|
||||
`data = {"username": "zhangsan", "email": "zs@163.com", "age": "20"}`(注意 age 是字符串)
|
||||
2. 自定义一个异常类 `ValidationError(Exception)`。
|
||||
3. 写函数 `validate_registration(data)`,按以下规则校验,**任何一个不满足就抛 `ValidationError`**:
|
||||
- `username` 必须存在且长度 ≥ 3
|
||||
- `email` 必须存在且同时包含 `@` 和 `.`
|
||||
- `age` 必须存在、能转成 int、且 `1 <= age <= 120`
|
||||
4. 在调用方用 try/except 捕获:失败打印 `校验失败: <异常信息>`,成功打印 `注册成功,欢迎 <username>!`
|
||||
|
||||
**进阶(加分)**:把规则本身做成数据——写一个通用函数
|
||||
`validate(data, rules)`,其中 `rules` 是一个字典:
|
||||
`{字段名: (校验函数, 错误信息)}`,返回所有不满足的字段的错误列表。
|
||||
(提示:校验函数就是你平时写的普通函数或 lambda。)
|
||||
这是后端参数校验框架(比如 DRF 的 Serializer)的雏形,能想出来说明你已经开始有"抽象"的意识了。
|
||||
|
||||
**出题意图**:Django forms / serializers 的校验、Celery 任务的失败处理,
|
||||
核心模式都是"**校验 → 抛异常 → 上层捕获处理**"。
|
||||
今天的题目就是把这个模式焊死在你的肌肉记忆里。
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 题目一 4 个边界情况全部通过(含进阶则 +1)
|
||||
- [ ] 题目二能打印出完整的调用日志(含进阶则 +1)
|
||||
- [ ] 题目三能对"缺字段 / 格式错 / 年龄越界"三种情况分别报错(含进阶则 +1)
|
||||
|
||||
完成后打开 `参考解答.py` 对照。建议看完后关掉文件,自己再默写一遍。
|
||||
|
||||
> 写给还没开始的你:这三天是递进关系——
|
||||
> Day_2 用基础语法模拟后端场景,Day_3 用面向对象复刻 ORM 的雏形,Day_4 真正上手 Django。
|
||||
> 每天只做必做题,也足够支撑你开学后跟上节奏。
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Day_2 参考解答
|
||||
=============
|
||||
今天的题目只用基础语法,但每个都是后端开发的真实场景。
|
||||
建议:先自己写,写不出来再看;看完后把代码关掉,自己再默写一遍。
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
# ============================================================
|
||||
# 题目一:解析 URL 查询参数
|
||||
# ============================================================
|
||||
|
||||
def parse_query_string(query):
|
||||
"""把 'username=zhangsan&age=20' 解析成 {'username': 'zhangsan', 'age': '20'}"""
|
||||
result = {}
|
||||
if not query: # 边界 1:空输入
|
||||
return result
|
||||
for item in query.split('&'): # 先按 & 切成若干段
|
||||
if '=' in item:
|
||||
key, value = item.split('=', 1) # maxsplit=1:值里可能也含有 '=',只切第一个
|
||||
else:
|
||||
key, value = item, '' # 边界 3:没有 = 的参数,值记为空字符串
|
||||
result[key] = value # 字典赋值天然处理"重复键覆盖"和"值为空"
|
||||
return result
|
||||
|
||||
|
||||
# 进阶:URL 编码还原(把 + 和 %20 还原成空格)
|
||||
def url_decode(value):
|
||||
return value.replace('+', ' ').replace('%20', ' ')
|
||||
|
||||
|
||||
def parse_query_string_advanced(query):
|
||||
result = {}
|
||||
if not query:
|
||||
return result
|
||||
for item in query.split('&'):
|
||||
if '=' in item:
|
||||
key, value = item.split('=', 1)
|
||||
else:
|
||||
key, value = item, ''
|
||||
result[key] = url_decode(value)
|
||||
return result
|
||||
|
||||
|
||||
# 一个小测试
|
||||
if __name__ == '__main__':
|
||||
print(parse_query_string('username=zhangsan&age=20&city=qingdao'))
|
||||
# {'username': 'zhangsan', 'age': '20', 'city': 'qingdao'}
|
||||
print(parse_query_string('name=a&name=b&flag'))
|
||||
# {'name': 'b', 'flag': ''}
|
||||
print(parse_query_string(''))
|
||||
# {}
|
||||
print(parse_query_string_advanced('name=Zhang+San&city=A%20B'))
|
||||
# {'name': 'Zhang San', 'city': 'A B'}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 题目二:万能日志记录器
|
||||
# ============================================================
|
||||
|
||||
def log_call(func, label=""):
|
||||
"""给函数包一层日志。func 被调用时打印参数和耗时。"""
|
||||
def wrapper(*args, **kwargs):
|
||||
tag = f"[{label}] " if label else ""
|
||||
print(f"{tag}[LOG] 调用函数 {func.__name__},参数为 args={args}, kwargs={kwargs}")
|
||||
start = time.time()
|
||||
result = func(*args, **kwargs) # 原样透传参数
|
||||
elapsed = time.time() - start
|
||||
print(f"{tag}[LOG] {func.__name__} 执行完成,耗时 {elapsed:.4f} 秒")
|
||||
return result # 原样返回结果
|
||||
return wrapper
|
||||
|
||||
|
||||
@log_call # 等价于 add = log_call(add)
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
|
||||
def multiply(a, b):
|
||||
return a * b
|
||||
|
||||
multiply = log_call(multiply, label="核心计算") # 进阶:带标签
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(add(1, 2))
|
||||
print(multiply(3, 4))
|
||||
# 输出示例:
|
||||
# [LOG] 调用函数 add,参数为 args=(1, 2), kwargs={}
|
||||
# [LOG] add 执行完成,耗时 0.0000 秒
|
||||
# 3
|
||||
# [核心计算][LOG] 调用函数 multiply,参数为 args=(3, 4), kwargs={}
|
||||
# [核心计算][LOG] multiply 执行完成,耗时 0.0000 秒
|
||||
# 12
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 题目三:模拟注册接口的参数校验
|
||||
# ============================================================
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""自定义校验异常:参数不合法时抛出"""
|
||||
pass
|
||||
|
||||
|
||||
def validate_registration(data):
|
||||
username = data.get("username")
|
||||
email = data.get("email")
|
||||
age = data.get("age")
|
||||
|
||||
if not username or len(username) < 3:
|
||||
raise ValidationError("用户名必须存在且长度不少于 3")
|
||||
if not email or "@" not in email or "." not in email:
|
||||
raise ValidationError("邮箱必须包含 @ 和 .")
|
||||
if age is None:
|
||||
raise ValidationError("年龄必须存在")
|
||||
try:
|
||||
age_int = int(age) # 字符串 "20" 也能转
|
||||
except (TypeError, ValueError):
|
||||
raise ValidationError("年龄必须是整数")
|
||||
if not (1 <= age_int <= 120):
|
||||
raise ValidationError("年龄必须在 1~120 之间")
|
||||
|
||||
|
||||
def register():
|
||||
data = {"username": "zhangsan", "email": "zs@163.com", "age": "20"}
|
||||
try:
|
||||
validate_registration(data)
|
||||
print(f"注册成功,欢迎 {data['username']}!")
|
||||
except ValidationError as e:
|
||||
print(f"校验失败: {e}")
|
||||
|
||||
|
||||
# ---- 进阶:把规则做成数据 ----
|
||||
def is_valid_email(email):
|
||||
return isinstance(email, str) and "@" in email and "." in email
|
||||
|
||||
|
||||
def validate(data, rules):
|
||||
"""rules: {字段名: (校验函数, 错误信息)},返回所有错误列表"""
|
||||
errors = []
|
||||
for field, (check, message) in rules.items():
|
||||
if not check(data.get(field)):
|
||||
errors.append(f"{field}: {message}")
|
||||
return errors
|
||||
|
||||
|
||||
RULES = {
|
||||
"username": (lambda v: isinstance(v, str) and len(v) >= 3, "用户名长度不足 3"),
|
||||
"email": (is_valid_email, "邮箱格式不正确"),
|
||||
"age": (lambda v: isinstance(v, int) and 1 <= v <= 120, "年龄非法"),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
register() # 注册成功,欢迎 zhangsan!
|
||||
print(validate({"username": "zs", "email": "bad", "age": 200}, RULES))
|
||||
# ['username: 用户名长度不足 3', 'email: 邮箱格式不正确', 'age: 年龄非法']
|
||||
-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
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
# Day_4 · 设计一个真正的 Django 小应用
|
||||
|
||||
> 前提:你应该已经跟着教程跑通了 Django 的安装,执行过
|
||||
> `django-admin startproject mysite` 和 `python manage.py startapp blog`,
|
||||
> 知道 `settings.py` / `urls.py` / `views.py` / `models.py` 大概长什么样。
|
||||
> 今天不是"照着教程敲一遍",而是"**先想清楚,再动手设计**"。
|
||||
> 题目里每一步都问了为什么——能回答上,才算真的懂了。
|
||||
|
||||
## 今日目标
|
||||
|
||||
做一个**社团招新管理系统**:成员(Member)发布文章(Article),成员可以评论(Comment)。
|
||||
覆盖:模型设计、ORM 查询、视图、URL 路由、模板、以及"从请求到响应"的完整链路。
|
||||
|
||||
## 环境准备
|
||||
|
||||
```bash
|
||||
pip install django
|
||||
django-admin startproject mysite
|
||||
cd mysite
|
||||
python manage.py startapp blog
|
||||
# 记得把 'blog' 加进 settings.py 的 INSTALLED_APPS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 题目一(必做):设计数据模型 models.py
|
||||
|
||||
**背景**:一个博客/招新系统,核心数据是"文章",文章有作者,作者是社团成员,文章下面有评论。
|
||||
|
||||
**要求**:在 `blog/models.py` 里写出完整代码,包含三个模型:
|
||||
|
||||
1. **Member**(成员):昵称 `nickname`、邮箱 `email`(要**唯一**)、
|
||||
加入时间 `joined_at`(自动记录创建时间)、简介 `bio`(允许为空)
|
||||
2. **Article**(文章):标题 `title`、正文 `content`、作者 `author`
|
||||
(外键 → Member,作者被删时文章级联删除)、是否发布 `is_published`(默认 False)、
|
||||
发布时间 `created_at`(自动记录)、浏览量 `views`(默认 0)
|
||||
3. **Comment**(评论):正文 `content`、所属文章 `article`
|
||||
(外键,`related_name="comments"`)、评论者 `commenter`(外键 → Member)
|
||||
|
||||
四个要求:
|
||||
|
||||
- 三个模型都要有合理的 `__str__`(Article 返回标题,Comment 返回"评论者 + 内容前 20 字",Member 返回昵称)
|
||||
- 用 `Meta` 设置排序:Article 按 `created_at` 倒序,Comment 按 `id` 正序
|
||||
- 字段参数用对:`max_length`、`default`、`null=True`/`blank=True` 该加的地方要加
|
||||
- 思考题写进注释里(各一两句即可)
|
||||
|
||||
**思考题(写进注释)**:
|
||||
|
||||
1. 为什么外键要用 `on_delete=models.CASCADE`?如果改成 `SET_NULL` 会发生什么、需要什么前提?
|
||||
2. `related_name="comments"` 是干什么的?(提示:`article.comments.all()` 是什么?)
|
||||
|
||||
---
|
||||
|
||||
## 题目二(必做):写视图 + ORM 查询 views.py
|
||||
|
||||
**要求**:在 `blog/views.py` 里写四个视图函数:
|
||||
|
||||
1. **`index(request)`**:查出所有**已发布**的文章(`is_published=True`),按时间倒序,
|
||||
渲染 `index.html`,把文章列表传进模板
|
||||
2. **`article_detail(request, pk)`**:用 `get()` 取单篇文章,**不存在时抛 `Http404`**;
|
||||
浏览量 +1 并保存;渲染 `detail.html`
|
||||
3. **`add_comment(request, pk)`**:POST 请求时读取 `request.POST` 里的 `content`,
|
||||
创建一条评论(评论者先用 `Member.objects.first()` 随便取一个,我们还没做登录),
|
||||
完成后**重定向**回文章详情页;GET 请求也直接重定向回详情页
|
||||
4. **(进阶)`author_articles(request, nickname)`**:按作者昵称查该作者的全部文章,
|
||||
并且**用 ORM 统计每篇文章的评论数**(提示:`annotate` + `Count`,配合 `related_name`),
|
||||
渲染 `author.html`。想不出 `annotate` 就先用 Python 算,但要写一句注释说明两种做法的区别
|
||||
|
||||
**思考题(写进注释)**:
|
||||
|
||||
- 浏览量 +1 有两种写法:
|
||||
a) `article.views += 1; article.save()`
|
||||
b) `Article.objects.filter(pk=pk).update(views=F("views") + 1)`
|
||||
两个请求同时点击文章时,a 会出什么问题?b 为什么没问题?(提示:并发 / 原子性)
|
||||
|
||||
---
|
||||
|
||||
## 题目三(必做):URL 路由 + 完整链路说明
|
||||
|
||||
**要求**:
|
||||
|
||||
1. 写出项目级 `urls.py`(含 `include`)和应用级 `blog/urls.py`,路由表覆盖:
|
||||
- 列表页 `""` → `index`
|
||||
- 文章详情 `/article/<int:pk>/` → `article_detail`
|
||||
- 提交评论 `/article/<int:pk>/comment/` → `add_comment`
|
||||
- 作者文章页 `/author/<str:nickname>/` → `author_articles`
|
||||
每个路由都要起 `name`。
|
||||
2. **写一段话(150 字内)**:用户在浏览器输入 `/article/3/` 回车后,
|
||||
从"请求到响应"经历了哪些环节?必须提到:URL 匹配、视图函数、
|
||||
ORM 查询、`render`、模板、`HttpResponse` 这几个词。
|
||||
3. **(进阶)** 给出 `index.html` 的模板片段:
|
||||
- 用 `{% for %}` 循环展示文章标题和"查看详情"链接
|
||||
- 用模板过滤器把 `created_at` 格式化成 `Y-m-d H:i`
|
||||
- 空列表时显示"暂无文章"
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 题目一:三个模型写全,思考题有自己的答案(不必和参考一致,能自圆其说就行)
|
||||
- [ ] 题目二:四个视图能跑通,`python manage.py runserver` 后列表页、详情页、评论、作者页都能访问
|
||||
- [ ] 题目三:路由表写全 + 链路说明写出来(进阶则 +1)
|
||||
|
||||
**验收动作(强烈建议)**:跑起来 `runserver`,在浏览器里真的走一遍
|
||||
"打开列表页 → 点进文章 → 提交评论 → 回到详情页看到评论"。
|
||||
能跑通,说明 Day_4 真正过关。
|
||||
|
||||
完成后打开 `参考解答.md` 对照。
|
||||
|
||||
> 进度提示:Day_4 做完,你就完成了"基础语法 → 面向对象 → 框架实战"的完整闭环。
|
||||
> 下一步可以自己加功能:分页、搜索、`@login_required`、用 Celery 给文章作者发通知……
|
||||
> 每加一个功能,都是一次新的练习。
|
||||
-203
@@ -1,203 +0,0 @@
|
||||
# Day_4 参考解答
|
||||
|
||||
> 说明:这里是完整参考实现,按文件拆成四块。
|
||||
> 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 views.py。
|
||||
|
||||
---
|
||||
|
||||
## 1. blog/models.py
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
"""社团成员"""
|
||||
nickname = models.CharField(max_length=50)
|
||||
email = models.EmailField(unique=True) # 邮箱唯一
|
||||
joined_at = models.DateTimeField(auto_now_add=True) # 创建时自动记录
|
||||
bio = models.TextField(blank=True, null=True) # 允许为空
|
||||
|
||||
def __str__(self):
|
||||
return self.nickname
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
"""文章"""
|
||||
title = models.CharField(max_length=100)
|
||||
content = models.TextField()
|
||||
author = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.CASCADE, # 作者被删 => 文章一起删
|
||||
related_name="articles", # member.articles.all() 能取到该作者全部文章
|
||||
)
|
||||
is_published = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
views = models.PositiveIntegerField(default=0) # 浏览量不会为负,用 PositiveIntegerField
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"] # 新文章在前
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
"""评论"""
|
||||
content = models.TextField()
|
||||
article = models.ForeignKey(
|
||||
Article,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comments", # article.comments.all() 能取到该文章全部评论
|
||||
)
|
||||
commenter = models.ForeignKey(Member, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"] # 按评论先后排序
|
||||
|
||||
def __str__(self):
|
||||
return f"评论({self.commenter}):{self.content[:20]}"
|
||||
```
|
||||
|
||||
**思考题答案(供对照)**:
|
||||
|
||||
1. `on_delete=models.CASCADE`:作者被删除时,他写的文章(以及文章下的评论)**级联删除**,
|
||||
不会留下指向不存在作者的孤儿数据。若改成 `SET_NULL`,删作者时文章的 `author`
|
||||
会被置为 `NULL`——但前提是外键字段允许为空,即
|
||||
`author = models.ForeignKey(Member, null=True, blank=True, on_delete=models.SET_NULL)`。
|
||||
2. `related_name="comments"`:定义**反向查询**的名字。有了它,从文章查评论就是
|
||||
`article.comments.all()`(不需要自己再写 `Comment.objects.filter(article=...)`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. blog/views.py
|
||||
|
||||
```python
|
||||
from django.db.models import Count, F
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.http import Http404
|
||||
|
||||
from .models import Article, Comment, Member
|
||||
|
||||
|
||||
def index(request):
|
||||
# 只要已发布文章,按时间倒序(Meta.ordering 已生效,order_by 可省略,写上更明确)
|
||||
articles = Article.objects.filter(is_published=True).order_by("-created_at")
|
||||
return render(request, "blog/index.html", {"articles": articles})
|
||||
|
||||
|
||||
def article_detail(request, pk):
|
||||
# get_object_or_404 = get() + 捕获 DoesNotExist 后抛 Http404
|
||||
article = get_object_or_404(Article, pk=pk)
|
||||
|
||||
# 浏览量 +1。为什么不用 article.views += 1; article.save()?
|
||||
# 两个用户同时点进来:两人都读到 views=10,各自 +1 后都写回 11,
|
||||
# 丢了 1 次访问——这就是"并发丢失更新"。
|
||||
# 用 F("views") + 1 是在数据库层完成"读旧值+1再写回",
|
||||
# 原子操作,不会丢更新。
|
||||
Article.objects.filter(pk=pk).update(views=F("views") + 1)
|
||||
|
||||
return render(request, "blog/detail.html", {"article": article})
|
||||
|
||||
|
||||
def add_comment(request, pk):
|
||||
article = get_object_or_404(Article, pk=pk)
|
||||
if request.method == "POST":
|
||||
content = request.POST.get("content", "").strip()
|
||||
if content:
|
||||
member = Member.objects.first() # 简化:还没做登录,先随便取一个成员
|
||||
Comment.objects.create(
|
||||
content=content,
|
||||
article=article,
|
||||
commenter=member,
|
||||
)
|
||||
return redirect("article_detail", pk=pk) # POST 后重定向,避免刷新重复提交
|
||||
|
||||
|
||||
def author_articles(request, nickname):
|
||||
author = get_object_or_404(Member, nickname=nickname)
|
||||
|
||||
# 进阶:annotate 在数据库层完成"按文章分组数评论数",
|
||||
# 返回的每篇文章都带一个动态属性 comment_count。
|
||||
articles = (
|
||||
Article.objects.filter(author=author)
|
||||
.annotate(comment_count=Count("comments")) # 反向查询名直接拿来用
|
||||
.order_by("-created_at")
|
||||
)
|
||||
return render(request, "blog/author.html", {"author": author, "articles": articles})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 路由
|
||||
|
||||
**项目级 mysite/urls.py**:
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("blog.urls")), # 把 / 开头的请求交给 blog 应用处理
|
||||
]
|
||||
```
|
||||
|
||||
**应用级 blog/urls.py**:
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "blog" # 配合 name 使用:模板里 {% url 'blog:article_detail' a.pk %}
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("article/<int:pk>/", views.article_detail, name="article_detail"),
|
||||
path("article/<int:pk>/comment/", views.add_comment, name="add_comment"),
|
||||
path("author/<str:nickname>/", views.author_articles, name="author_articles"),
|
||||
]
|
||||
```
|
||||
|
||||
**路由命名与反向解析**:`name` 给路由起了名字,模板和视图里用
|
||||
`{% url 'blog:article_detail' a.pk %}` / `redirect("article_detail", pk=pk)` 引用,
|
||||
而不是手写死 URL 字符串——这样以后改路径,代码不用跟着改。
|
||||
|
||||
---
|
||||
|
||||
## 4. 完整链路说明(题目三第 2 问示例答案)
|
||||
|
||||
> 用户在浏览器输入 `/article/3/` 回车后:Django 首先在 **URL 路由表**中逐条匹配,
|
||||
> 命中 `path("article/<int:pk>/")`,把 `pk=3` 提取出来并调用对应的**视图函数**
|
||||
> `article_detail(request, pk=3)`。视图内执行 **ORM 查询**(`get_object_or_404` 对应
|
||||
> 一条 SELECT),拿到文章对象后调用 **`render`** 把数据填入模板、生成 HTML,
|
||||
> 最终打包成 **`HttpResponse`** 返回给浏览器渲染。
|
||||
|
||||
---
|
||||
|
||||
## 5. 进阶:index.html 模板片段
|
||||
|
||||
```html
|
||||
{% if articles %}
|
||||
<ul>
|
||||
{% for a in articles %}
|
||||
<li>
|
||||
<a href="{% url 'blog:article_detail' a.pk %}">{{ a.title }}</a>
|
||||
<span>{{ a.created_at|date:"Y-m-d H:i" }}</span>
|
||||
· 作者:{{ a.author.nickname }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>暂无文章,快来发布第一篇吧!</p>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] `python manage.py makemigrations blog` 与 `migrate` 无报错
|
||||
- [ ] `runserver` 后,列表页 / 详情页 / 评论提交 / 作者页全部可访问
|
||||
- [ ] 删掉一个 Member 后,他的文章和评论也一起消失(验证 CASCADE)
|
||||
- [ ] 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"
|
||||
@@ -8,15 +8,24 @@
|
||||
|
||||
请在该工单下方"撰写"处提交你的文件然后点击"评论",如果显示"登录 并参与到对话中"就请点击"登录"并注册你的账户
|
||||
|
||||
## 目录结构
|
||||
## 题目安排
|
||||
|
||||
| 目录 | 内容 | 前置要求 |
|
||||
| --- | --- | --- |
|
||||
| Day_2 | Django 视角下的 Python 基础(字典 / 函数 / `*args`/`**kwargs` / 异常) | 掌握 Python 基础语法 |
|
||||
| Day_3 | 面向对象 × 手写一个小 ORM(类 / 继承 / descriptor) | 掌握基础语法即可,无需学过 Django |
|
||||
| Day_4 | 设计一个真正的 Django 小应用(models / views / urls / 模板) | 已跟着教程跑通 Django 环境 |
|
||||
| Day_2 | 字典与字符串:模拟查询结果、URL 参数解析 | 掌握 Python 基础语法 |
|
||||
| Day_3 | 函数与异常:日志记录器、接口参数校验 | 掌握 Python 基础语法 |
|
||||
| Day_4 | 面向对象(一):User 类 | 无需学过 Django |
|
||||
| Day_5 | 面向对象(二):继承,复刻 models.Model | 承接 Day_4 |
|
||||
| Day_6 | 面向对象(三):descriptor,复刻 models.CharField | 进阶,量力而行 |
|
||||
| 进阶_Django | 设计一个真正的 Django 小应用 | **可选**,不做要求 |
|
||||
|
||||
每个目录包含当日题目(`README.md`)与参考解答(`参考解答.py` 或 `参考解答.md`)。
|
||||
|
||||
## 进阶任务说明(进阶_Django)
|
||||
|
||||
1. **前端**(模板 HTML / CSS / JS)**允许使用 AI 辅助**
|
||||
2. **后端**(models / views / urls / ORM 查询 / 业务逻辑)**必须自己手搓**,不允许 AI 代写
|
||||
3. 完成后**找学长验收**:验收时会围绕你的代码提问,答得上来才算通过
|
||||
|
||||
> 定位目标:为用 Django、Celery 等框架编写后端程序打下基础
|
||||
> 难度递进:基础语法 → 面向对象 → 框架实战
|
||||
> 难度递进:基础语法 → 函数与异常 → 面向对象 → 框架实战
|
||||
|
||||
Reference in New Issue
Block a user