Python 官方教程综合练习手册:Oh My Pi Terminal Edition
这版按“先教概念 → 再看函数怎么用 → 再拆代码逻辑 → 最后做练习”的顺序写。目标读者是:会一点 C,知道变量、函数、循环这些词,但 Python 还不系统。
0. 使用方式
不要把这份文档当成“看完就会”的文章。它是练习手册。每个练习都按同一个学习节奏来:
1. 先理解这个语法/函数解决什么问题
2. 看最小例子,确认会运行
3. 拆代码逻辑,知道每一行为什么存在
4. 再写练习,不要跳过验收
5. 把错误记录下来,下一周会用到
0.1 官方教程映射
| 官方教程 | 本手册用途 |
|---|---|
| 2. Using the Python Interpreter | 解释器、脚本、命令行运行 |
| 3. An Informal Introduction to Python | 数字、文本、列表、第一段程序 |
| 4. More Control Flow Tools | if、for、range、match、函数、参数、编码风格 |
| 5. Data Structures | list、tuple、set、dict、comprehension、循环技巧 |
| 6. Modules | 模块、包、__name__ == "__main__"、导入规范 |
| 7. Input and Output | f-string、文件读写、JSON |
| 8. Errors and Exceptions | 语法错误、异常、try、raise、清理动作 |
| 9. Classes | 对象、作用域、类、实例、迭代器、生成器 |
| 10. Brief Tour of the Standard Library | pathlib、csv、json、argparse、statistics、logging |
| 12. Virtual Environments and Packages | 虚拟环境、包管理、依赖复现 |
| 15. Floating-Point Arithmetic | 浮点误差、math.isclose、金融/统计小数风险 |
0.2 最终项目结构
最终你会得到一个小项目:
python-official-practice/
├── README.md
├── pyproject.toml
├── data/
│ ├── scores.csv
│ └── prices.csv
├── src/
│ ├── basics.py
│ ├── stats_utils.py
│ ├── data_io.py
│ ├── metrics.py
│ ├── cli.py
│ ├── toy_backtest.py
│ └── experiment.py
├── reports/
│ ├── python_basics_report.md
│ ├── data_summary.md
│ ├── backtest_report.md
│ └── floating_point_notes.md
└── notes/
└── official_tutorial_mapping.md
它不是大项目,但它覆盖进入 AI / Quant 之前真正要会的东西:
变量 → 列表 → 字符串 → 条件 → 循环 → 函数 → 数据结构
→ 模块 → 文件 → JSON → 异常 → CLI → 类 → 迭代器 → 浮点数
0.3 四周路线
| 周 | 主题 | 最终交付物 |
|---|---|---|
| Week A | 环境、基本类型、控制流 | src/basics.py、reports/python_basics_report.md |
| Week B | 函数、参数、数据结构、风格 | src/stats_utils.py、src/metrics.py |
| Week C | 模块、文件、JSON、异常、CLI | src/data_io.py、src/cli.py、reports/data_summary.md |
| Week D | 类、迭代器、标准库、浮点数、综合项目 | src/toy_backtest.py、src/experiment.py、reports/backtest_report.md |
1. Week A:解释器、虚拟环境、基本类型、控制流
Week A 的目标不是“背 Python 语法”,而是能把一段数据放进变量,写几行逻辑处理它,然后在命令行跑起来。
1.1 先理解:解释器、脚本、虚拟环境
Python 有两种常见运行方式。
第一种是交互式解释器:
python
进去以后输入:
1 + 1
它会立刻返回结果。这个方式适合试语法。
第二种是脚本文件:
python src/basics.py
这个方式适合正式练习。你后面写的 AI / Quant 项目基本都应该放在 .py 文件里,而不是只写在 notebook 里。
虚拟环境的作用是隔离依赖。你这个项目需要的包,不应该污染整个系统 Python。
函数和命令怎么用
这里先不用自己写函数,只学会几个命令:
| 命令 | 作用 |
|---|---|
uv init | 初始化 Python 项目 |
uv venv | 创建虚拟环境 |
source .venv/bin/activate.fish | 在 fish shell 里启用虚拟环境 |
python --version | 查看当前 Python 版本 |
python -c "..." | 直接执行一小段 Python 代码 |
代码逻辑
mkdir 创建目录
cd 进入目录
uv init 创建 pyproject.toml
uv venv 创建 .venv
source 激活 .venv
python --version 验证当前 Python 可用
Exercise A-00:创建练习项目
mkdir -p ~/Code/python-official-practice
cd ~/Code/python-official-practice
uv init
uv venv
source .venv/bin/activate.fish
python --version
python -c "print('hello python')"
mkdir -p src data reports notes
验收:
- 当前目录有
pyproject.toml。 command -v python指向.venv/bin/python。python -c "print('hello python')"能输出hello python。
1.2 数字:先把统计量算出来
AI / Quant 会大量处理数字。先从最小统计任务开始:一组分数,求数量、均值、最小值、最大值。
你要学的函数
| 函数 | 怎么用 | 含义 |
|---|---|---|
len(xs) | len([1, 2, 3]) | 返回元素个数 |
sum(xs) | sum([1, 2, 3]) | 返回总和 |
min(xs) | min([1, 2, 3]) | 返回最小值 |
max(xs) | max([1, 2, 3]) | 返回最大值 |
print(x) | print("mean", 2.0) | 把结果显示到终端 |
最小例子:
scores = [80, 92, 75, 88, 95]
mean_score = sum(scores) / len(scores)
print(mean_score)
代码逻辑
scores = [...] 把多个分数放进一个 list
sum(scores) 求总分
len(scores) 求人数
sum(scores) / len(scores) 总分除以人数,得到平均分
print(...) 把结果显示出来
这和 C 的数组很像,但 Python 的 list 更灵活:它知道自己的长度,所以可以直接 len(scores)。
Exercise A-01:均值、最小值、最大值
文件:src/basics.py
写入:
scores = [80, 92, 75, 88, 95]
print("n =", len(scores))
print("mean =", sum(scores) / len(scores))
print("min =", min(scores))
print("max =", max(scores))
运行:
python src/basics.py
验收:你能解释这四个问题:
scores是什么类型?len(scores)返回什么?- 为什么均值要除以
len(scores)? print("mean =", value)为什么会显示两段内容?
1.3 字符串:把脏列名清洗成规范列名
真实数据里,列名经常不干净。例如:
" Final Score "
"Student ID"
"Group Name"
后续 pandas / SQL / sklearn 里,更推荐列名像这样:
final_score
student_id
group_name
你要学的方法
字符串后面可以接方法调用:
| 方法 | 怎么用 | 含义 |
|---|---|---|
strip() | " A ".strip() | 去掉两边空白 |
lower() | "ABC".lower() | 转小写 |
replace(a, b) | "A B".replace(" ", "_") | 把 a 替换成 b |
最小例子:
raw_name = " Final Score "
clean_name = raw_name.strip().lower().replace(" ", "_")
print(clean_name)
代码逻辑
raw_name.strip() " Final Score " → "Final Score"
.lower() "Final Score" → "final score"
.replace(" ", "_") "final score" → "final_score"
这叫链式调用。左边一步的结果,会传给右边下一步。
Exercise A-02:清洗字段名
输入:
columns = [" Student ID ", "Final Score", "Group Name"]
目标输出:
["student_id", "final_score", "group_name"]
推荐先用普通循环,不要一开始就写列表推导式:
columns = [" Student ID ", "Final Score", "Group Name"]
clean_columns = []
for name in columns:
clean_name = name.strip().lower().replace(" ", "_")
clean_columns.append(clean_name)
print(clean_columns)
验收:你能解释:
clean_columns = []为什么要先创建空列表?for name in columns每次拿到的是什么?append是把元素加到哪里?
1.4 列表过滤:去掉缺失值和异常值
真实数据经常有缺失值。Python 里常用 None 表示“没有值”。有些旧数据会用 -999 这种特殊数字表示异常占位。
你要学的语法
| 语法 | 含义 |
|---|---|
x is not None | 判断 x 不是缺失值 |
x != -999 | 判断 x 不是异常占位值 |
and | 两个条件都要满足 |
append | 把合格数据放进新列表 |
最小例子:
values = [1.2, None, 3.4, -999, 2.8]
clean = []
for x in values:
if x is not None and x != -999:
clean.append(x)
print(clean)
代码逻辑
values 保存原始数据
clean 保存清洗后的数据
for x in values 逐个检查
if 条件成立,说明 x 是有效值
clean.append(x) 保存有效值
这里不要急着用“高级写法”。先把循环和条件看懂。
Exercise A-03:过滤缺失值
输入:
values = [1.2, None, 3.4, -999, 2.8, None, 5.0]
要求输出:
- 有效数据列表。
- 有效样本数量。
- 有效数据均值。
提示:均值仍然用 sum(clean) / len(clean)。
验收:如果 clean 为空,你能说明为什么不能直接除以 len(clean)。
1.5 条件函数:把分数变成等级
现在开始写第一个函数。函数的作用是:给一个输入,返回一个结果。
函数怎么用
你希望这样使用它:
grade = score_to_grade(95)
print(grade)
也就是说,score_to_grade 接收一个分数,返回一个等级。
函数定义长什么样
def score_to_grade(score: float) -> str:
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
return "D"
代码逻辑
def 定义函数
score_to_grade 函数名
score: float 参数叫 score,期望是浮点数
-> str 返回值期望是字符串
if score >= 90 如果分数至少 90
return "A" 立刻返回 A,函数结束
return "D" 前面条件都不满足,返回 D
注意:return 后函数就结束了。因此这里不一定需要 elif。
Exercise A-04:分数等级
文件:src/basics.py
实现:
def score_to_grade(score: float) -> str:
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
return "D"
print(score_to_grade(95))
print(score_to_grade(85))
print(score_to_grade(75))
print(score_to_grade(60))
然后加上自检:
assert score_to_grade(95) == "A"
assert score_to_grade(85) == "B"
assert score_to_grade(75) == "C"
assert score_to_grade(60) == "D"
验收:你能解释为什么 85 不会返回 A。
1.6 match:根据命令选择行为
match 类似其他语言里的 switch,适合处理“命令分发”。
函数怎么用
你希望这样调用:
message = handle_command("summary")
print(message)
最小例子
def handle_command(command: str) -> str:
match command:
case "summary":
return "run data summary"
case "metrics":
return "run metrics"
case "backtest":
return "run backtest"
case _:
return "unknown command"
代码逻辑
match command 检查 command 的值
case "summary" 如果值等于 summary
return ... 返回对应说明
case _ 兜底分支,匹配所有未知输入
case _ 很重要。没有它,用户输错命令时,你的程序可能没有清楚反馈。
Exercise A-05:实现命令分发
文件:src/basics.py
要求支持:
summary
metrics
backtest
验收:
assert handle_command("summary") == "run data summary"
assert handle_command("metrics") == "run metrics"
assert handle_command("backtest") == "run backtest"
assert handle_command("abc") == "unknown command"
2. Week B:函数、参数、数据结构、编码风格
Week B 的目标是把零散代码变成可复用函数。以后你写 pandas、sklearn、PyTorch,都离不开函数。
2.1 均值函数:从公式变成可复用函数
函数怎么用
你希望以后这样写:
from stats_utils import mean
score_mean = mean([80, 92, 75])
print(score_mean)
这样比每次写 sum(xs) / len(xs) 更清楚。
先写最小版本
def mean(xs: list[float]) -> float:
return sum(xs) / len(xs)
这个版本有问题:如果 xs 是空列表,会发生除以 0。
加上错误处理
def mean(xs: list[float]) -> float:
"""Return arithmetic mean of a non-empty list."""
if not xs:
raise ValueError("xs must not be empty")
return sum(xs) / len(xs)
代码逻辑
if not xs 如果列表为空
raise ValueError(...) 主动报错,告诉调用者输入不合法
return sum(xs) / len(xs) 正常情况下返回均值
raise 不是程序崩溃的坏习惯。相反,它是在错误输入出现时给出明确解释。
Exercise B-01:统计函数库
文件:src/stats_utils.py
实现:
def mean(xs: list[float]) -> float:
"""Return arithmetic mean of a non-empty list."""
if not xs:
raise ValueError("xs must not be empty")
return sum(xs) / len(xs)
def variance(xs: list[float]) -> float:
"""Return population variance of a non-empty list."""
m = mean(xs)
return mean([(x - m) ** 2 for x in xs])
然后自己补:
def std(xs: list[float]) -> float:
...
def covariance(xs: list[float], ys: list[float]) -> float:
...
def zscore(xs: list[float]) -> list[float]:
...
验收:
assert mean([1, 2, 3]) == 2
assert variance([1, 2, 3]) == 2 / 3
思考:variance 里为什么可以调用自己写的 mean?
2.2 默认参数:不要让多个调用共享同一个列表
Python 官方教程特别提醒:默认参数只在函数定义时创建一次。
错误函数怎么用会出问题
def add_tag_bad(tag: str, tags: list[str] = []) -> list[str]:
tags.append(tag)
return tags
print(add_tag_bad("ai"))
print(add_tag_bad("quant"))
你可能以为第二次输出只有 quant,但实际会把第一次的 ai 也留下。
正确函数怎么用
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
代码逻辑
tags 默认是 None 不共享可变列表
if tags is None 如果调用者没传列表
tags = [] 创建一个新的空列表
tags.append(tag) 加入标签
return tags 返回结果
Exercise B-02:安全默认参数
文件:src/stats_utils.py
实现:
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
验收:
assert add_tag("ai") == ["ai"]
assert add_tag("quant") == ["quant"]
assert add_tag("python", ["study"]) == ["study", "python"]
2.3 数据结构:选择合适的容器
Python 的数据结构不是装饰品。选错结构,代码会绕。
| 结构 | 适合保存 | 示例 |
|---|---|---|
list | 有顺序、可重复的数据 | 收益率序列 |
tuple | 固定组合结果 | (train, valid) |
set | 去重、集合差异 | 用户 ID 集合 |
dict | 名字到值的映射 | 指标字典 |
函数怎么用
你希望指标函数返回清楚的字典:
counts = confusion_counts([1, 0, 1], [1, 0, 0])
print(counts["tp"])
代码逻辑
二分类里:
y_true = 1, y_pred = 1 → tp
y_true = 0, y_pred = 0 → tn
y_true = 0, y_pred = 1 → fp
y_true = 1, y_pred = 0 → fn
Exercise B-03:指标字典
文件:src/metrics.py
实现:
def accuracy(y_true: list[int], y_pred: list[int]) -> float:
if len(y_true) != len(y_pred):
raise ValueError("length mismatch")
if not y_true:
raise ValueError("empty labels")
correct = 0
for a, b in zip(y_true, y_pred):
if a == b:
correct += 1
return correct / len(y_true)
def confusion_counts(y_true: list[int], y_pred: list[int]) -> dict[str, int]:
if len(y_true) != len(y_pred):
raise ValueError("length mismatch")
counts = {"tp": 0, "tn": 0, "fp": 0, "fn": 0}
for a, b in zip(y_true, y_pred):
if a == 1 and b == 1:
counts["tp"] += 1
elif a == 0 and b == 0:
counts["tn"] += 1
elif a == 0 and b == 1:
counts["fp"] += 1
elif a == 1 and b == 0:
counts["fn"] += 1
else:
raise ValueError("labels must be 0 or 1")
return counts
验收:
y_true = [1, 0, 1, 1]
y_pred = [1, 0, 0, 1]
assert accuracy(y_true, y_pred) == 0.75
assert confusion_counts(y_true, y_pred) == {"tp": 2, "tn": 1, "fp": 0, "fn": 1}
2.4 列表推导式:把循环写短,但不要写晦涩
列表推导式是 Python 常用写法。它适合“从旧列表生成新列表”。
普通循环写法
names = [" Alice ", "", "BOB", " Carol"]
clean = []
for name in names:
name = name.strip()
if name:
clean.append(name.lower())
函数怎么用
clean = [name.strip().lower() for name in names if name.strip()]
代码逻辑
name.strip().lower() 新列表里的元素长什么样
for name in names 从 names 里逐个拿元素
if name.strip() 只保留非空字符串
Exercise B-04:批量清洗字符串
输入:
names = [" Alice ", "", "BOB", " Carol"]
输出:
["alice", "bob", "carol"]
验收:你能把列表推导式改回普通 for 循环。
3. Week C:模块、文件、JSON、异常、CLI
Week C 的目标是把“能跑的脚本”变成“能复用的小工具”。这是从学习代码走向项目代码的关键一步。
3.1 模块:把函数放进文件
当代码变长,不应该所有东西都写在一个文件里。
src/
├── stats_utils.py 统计函数
├── metrics.py 模型指标函数
├── data_io.py 文件读写函数
└── cli.py 命令行入口
模块怎么用
如果 src/stats_utils.py 里有:
def mean(xs: list[float]) -> float:
if not xs:
raise ValueError("xs must not be empty")
return sum(xs) / len(xs)
那么其他文件可以导入:
from stats_utils import mean
print(mean([1, 2, 3]))
代码逻辑
from stats_utils import mean 从 stats_utils.py 里拿 mean 函数
mean([1, 2, 3]) 调用这个函数
Exercise C-01:模块拆分
要求:
mean、variance放到src/stats_utils.py。accuracy、confusion_counts放到src/metrics.py。src/cli.py负责调用它们。
验收:
python src/cli.py
能看到一个最小输出,例如:
mean = 2.0
accuracy = 0.75
3.2 文件读取:从磁盘拿数据
真实项目不会永远把数据写死在代码里。你要学会从 CSV 文件读数据。
函数怎么用
你希望这样使用:
from pathlib import Path
from data_io import read_scores_csv
rows = read_scores_csv(Path("data/scores.csv"))
print(rows)
你要学的标准库
| 模块/对象 | 作用 |
|---|---|
pathlib.Path | 表示文件路径 |
csv.DictReader | 把 CSV 每一行读成字典 |
with ... as f | 打开文件,并确保最后关闭 |
最小数据
文件:data/scores.csv
student_id,group,score
s1,A,80
s2,A,92
s3,B,75
s4,B,88
代码逻辑
import csv
from pathlib import Path
def read_scores_csv(path: Path) -> list[dict[str, str]]:
with path.open("r", encoding="utf-8", newline="") as f:
return list(csv.DictReader(f))
逐行解释:
import csv 使用 Python 标准库 csv
from pathlib import Path 使用 Path 表示路径
path.open(...) 打开文件
encoding="utf-8" 按 UTF-8 读取文本
newline="" 让 csv 模块正确处理换行
csv.DictReader(f) 第一行作为列名,每行变成 dict
list(...) 把可迭代结果转成列表
Exercise C-02:CSV 读写
文件:src/data_io.py
实现 read_scores_csv,然后写一个函数把 score 转成数字:
def parse_scores(rows: list[dict[str, str]]) -> list[float]:
scores = []
for row in rows:
scores.append(float(row["score"]))
return scores
验收:
rows = read_scores_csv(Path("data/scores.csv"))
scores = parse_scores(rows)
assert scores == [80.0, 92.0, 75.0, 88.0]
3.3 JSON:保存配置
CSV 适合表格数据。JSON 适合配置。
函数怎么用
你希望这样读取配置:
config = read_json(Path("config.json"))
print(config["input"])
最小配置
文件:config.json
{
"input": "data/scores.csv",
"output": "reports/data_summary.md",
"pass_score": 60
}
代码逻辑
import json
from pathlib import Path
def read_json(path: Path) -> dict:
text = path.read_text(encoding="utf-8")
return json.loads(text)
逐行解释:
path.read_text(...) 把整个文件读成字符串
json.loads(text) 把 JSON 字符串转成 Python dict
return 把 dict 返回给调用者
Exercise C-03:实验配置
文件:src/data_io.py
实现:
def read_json(path: Path) -> dict:
text = path.read_text(encoding="utf-8")
return json.loads(text)
验收:
config = read_json(Path("config.json"))
assert config["input"] == "data/scores.csv"
assert config["pass_score"] == 60
3.4 异常:让错误变得可理解
初学者常见错误是:程序错了,但不知道错在哪里。异常处理不是为了隐藏错误,而是为了把错误解释清楚。
函数怎么用
你希望用户给错路径时看到:
input file not found: data/missing.csv
而不是一大段看不懂的 traceback。
错误写法
try:
rows = read_scores_csv(path)
except Exception:
pass
这会把错误吞掉。不要这样写。
正确写法
try:
rows = read_scores_csv(path)
except FileNotFoundError as exc:
raise SystemExit(f"input file not found: {path}") from exc
代码逻辑
try 尝试运行可能失败的代码
except FileNotFoundError 只捕获文件不存在这一类错误
as exc 保存原始错误
raise SystemExit(...) 给命令行用户一个清楚提示
from exc 保留原始错误链,方便调试
Exercise C-04:异常条件
在 src/data_io.py 或 src/cli.py 中处理:
- 输入文件不存在。
- CSV 缺少
score列。 score不能转为数字。- 输出目录不存在时自动创建。
验收:故意把输入路径写错,程序必须输出清楚错误,而不是静默结束。
3.5 CLI:从命令行传参数
CLI 是 command line interface。你可以不用改代码,只通过命令传不同输入输出。
命令怎么用
目标命令:
python src/cli.py summary --input data/scores.csv --output reports/data_summary.md
含义:
python src/cli.py 运行 cli.py
summary 子任务:做数据摘要
--input ... 输入文件
--output ... 输出报告
代码逻辑
from argparse import ArgumentParser
def parse_args():
parser = ArgumentParser(description="Summarize score CSV")
parser.add_argument("command", choices=["summary"])
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
return parser.parse_args()
逐行解释:
ArgumentParser(...) 创建参数解析器
add_argument("command") 读取位置参数 summary
choices=["summary"] 限制只能输入 summary
add_argument("--input") 读取输入路径
required=True 必须提供这个参数
parse_args() 从命令行解析参数
Exercise C-05:生成 Markdown 摘要报告
文件:src/cli.py
目标:读 data/scores.csv,输出 reports/data_summary.md。
报告至少包含:
# Data Summary
- rows: 4
- mean score: 83.75
- min score: 75.0
- max score: 92.0
验收命令:
python src/cli.py summary --input data/scores.csv --output reports/data_summary.md
cat reports/data_summary.md
4. Week D:类、迭代器、标准库、浮点数、综合项目
Week D 的目标是把前面的函数组织成更像真实项目的结构,并理解两个 AI / Quant 特别容易踩坑的东西:迭代器和浮点数。
4.1 dataclass:保存配置对象
类不是必须一开始学,但你需要读懂这种写法,因为后续 PyTorch、sklearn、Quant 回测都会有配置对象。
类怎么用
你希望这样创建配置:
config = ExperimentConfig(input_path="data/scores.csv", output_dir="reports")
print(config.input_path)
代码逻辑
from dataclasses import dataclass
@dataclass(frozen=True)
class ExperimentConfig:
input_path: str
output_dir: str
seed: int = 42
逐行解释:
@dataclass(frozen=True) 自动生成初始化方法,并禁止修改字段
class ExperimentConfig 定义一个配置类型
input_path: str 输入路径字段
output_dir: str 输出目录字段
seed: int = 42 随机种子,默认是 42
Exercise D-01:实验配置类
文件:src/experiment.py
实现 ExperimentConfig,然后运行:
config = ExperimentConfig(input_path="data/scores.csv", output_dir="reports")
assert config.seed == 42
验收:你能解释 config.input_path 和字典 config["input_path"] 的区别。
4.2 迭代器和生成器:逐行处理数据
如果文件很大,不应该一次把所有内容读进内存。生成器可以一行一行地产生数据。
函数怎么用
for line in iter_non_empty_lines(Path("notes/raw.txt")):
print(line)
代码逻辑
from pathlib import Path
def iter_non_empty_lines(path: Path):
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
yield line
逐行解释:
for line in f 文件对象可以逐行迭代
line.strip() 去掉换行和两边空格
if line 空字符串会被当成 False
yield line 产出一行,但函数不一次性结束
return 是一次性返回。yield 是“这次先给你一个值,下次循环再继续”。
Exercise D-02:逐行读取
文件:src/data_io.py
实现 iter_non_empty_lines。
验收:输入文件中有空行时,输出里不能包含空字符串。
4.3 浮点数:不要用 == 比较小数
Python 官方教程第 15 章非常重要。它解释了为什么很多小数不能被二进制浮点数精确表示。
函数怎么用
错误比较:
0.1 + 0.1 + 0.1 == 0.3
推荐比较:
import math
math.isclose(0.1 + 0.1 + 0.1, 0.3)
代码逻辑
0.1 在二进制里不能精确表示
三个近似值相加,结果仍然是近似值
== 要求完全相等,所以可能失败
math.isclose 允许非常小的误差
这对 Quant 特别重要。收益率、波动率、Sharpe、loss 都是浮点数,不要随便用 == 判断。
Exercise D-03:浮点复盘报告
文件:reports/floating_point_notes.md
写清楚:
- 为什么
0.1 + 0.2 != 0.3。 round是显示/近似,不是改变真实存储。- 为什么金融收益率和模型 loss 不能用
==比较小数。 - 什么时候用
math.isclose。
验收:报告里必须有一段你自己的解释,不要只复制代码。
4.4 收益率函数:从价格变成收益率
Quant 的最小输入通常是价格序列。最小计算是收益率。
函数怎么用
returns = pct_change([100, 101, 99])
print(returns)
期望:
[None, 0.010000000000000009, -0.01980198019801982]
第一天没有前一天价格,所以收益率是 None。
代码逻辑
def pct_change(values: list[float]) -> list[float | None]:
returns: list[float | None] = [None]
for prev, curr in zip(values, values[1:]):
if prev == 0:
returns.append(None)
else:
returns.append(curr / prev - 1)
return returns
逐行解释:
returns = [None] 第一项没有前值
values[1:] 从第二个价格开始
zip(values, values[1:]) 组成相邻价格对
prev == 0 防止除以 0
curr / prev - 1 收益率公式
returns.append(...) 保存每一天收益率
Exercise D-04:收益率计算
文件:src/toy_backtest.py
实现 pct_change,验收:
import math
returns = pct_change([100, 101, 99])
assert returns[0] is None
assert math.isclose(returns[1], 0.01)
assert math.isclose(returns[2], 99 / 101 - 1)
4.5 shift:避免未来函数
回测里最危险的错误之一是未来函数:用今天收盘后的信息,在今天就交易。
如果你根据今天收益率生成信号,那么真正能执行的仓位应该从下一天开始。
函数怎么用
signals = [1, 0, 1]
positions = shift(signals, fill=0)
print(positions)
输出:
[0, 1, 0]
含义:
第 1 天没有前一天信号,所以仓位 0
第 2 天使用第 1 天信号
第 3 天使用第 2 天信号
代码逻辑
def shift(values: list[int], fill: int = 0) -> list[int]:
return [fill] + values[:-1]
逐行解释:
values[:-1] 去掉最后一个元素
[fill] + ... 在最前面补一个默认值
Exercise D-05:仓位滞后
文件:src/toy_backtest.py
实现 shift,验收:
assert shift([1, 0, 1], fill=0) == [0, 1, 0]
assert shift([1], fill=0) == [0]
你必须能解释:为什么不能直接 position = signal。
4.6 Toy Quant Backtest:综合练习
现在把前面学到的东西串起来。
读 CSV
↓
取 close 列
↓
计算收益率
↓
根据收益率生成 signal
↓
shift(signal) 得到 position
↓
计算策略收益
↓
输出 Markdown 报告
输入数据
文件:data/prices.csv
date,close
2024-01-01,100
2024-01-02,101
2024-01-03,99
2024-01-04,102
关键函数怎么用
rows = read_prices(Path("data/prices.csv"))
closes = [float(row["close"]) for row in rows]
returns = pct_change(closes)
signals = [1 if r is not None and r > 0 else 0 for r in returns]
positions = shift(signals, fill=0)
代码逻辑
read_prices 从 CSV 读取字典列表
float(row["close"]) 把文本价格转成数字
pct_change 价格 → 收益率
signal 收益率为正则看多,否则空仓
shift 信号滞后一日,避免未来函数
position * return 仓位乘收益率,得到策略收益
Exercise D-06:完整 toy backtest
文件:src/toy_backtest.py
要求:
- 用
csv读取数据。 - 用
pct_change计算收益率。 - 用
signal = 1 if return > 0 else 0生成简单信号。 - 用
position = shift(signal)避免未来函数。 - 用
math.isclose做浮点验收。 - 输出
reports/backtest_report.md。
报告至少包含:
# Toy Backtest Report
- rows: 4
- total return: ...
- max daily return: ...
- min daily return: ...
- warning: signal is shifted by one day to avoid look-ahead bias
验收命令:
python src/toy_backtest.py --input data/prices.csv --output reports/backtest_report.md
cat reports/backtest_report.md
4.7 doctest:把示例变成自检
小函数可以直接在 docstring 里写示例。doctest 会执行这些示例。
函数怎么用
python src/stats_utils.py
如果 doctest 通过,通常没有输出。如果失败,会显示哪里不一致。
代码逻辑
def mean(xs: list[float]) -> float:
"""
Return arithmetic mean.
>>> mean([1, 2, 3])
2.0
"""
if not xs:
raise ValueError("xs must not be empty")
return sum(xs) / len(xs)
if __name__ == "__main__":
import doctest
doctest.testmod()
逐行解释:
>>> mean([1, 2, 3]) docstring 里的交互式示例
2.0 期望输出
doctest.testmod() 扫描当前模块里的 docstring 示例并执行
Exercise D-07:给统计函数加 doctest
文件:src/stats_utils.py
至少给 mean 和 variance 加 doctest。
验收:
python src/stats_utils.py
没有失败信息。
5. 最终检查清单
完成后,你应该能运行:
python src/basics.py
python src/cli.py summary --input data/scores.csv --output reports/data_summary.md
python src/toy_backtest.py --input data/prices.csv --output reports/backtest_report.md
python src/stats_utils.py
最终交付:
src/basics.pysrc/stats_utils.pysrc/metrics.pysrc/data_io.pysrc/cli.pysrc/toy_backtest.pysrc/experiment.pyreports/data_summary.mdreports/backtest_report.mdreports/floating_point_notes.mdnotes/official_tutorial_mapping.md
6. 评分标准
| 项目 | 分值 | 合格标准 |
|---|---|---|
| 环境可复现 | 15 | README 写清环境和命令,.venv 不入 Git |
| 基础语法 | 15 | 能写数字、字符串、列表、控制流练习 |
| 函数质量 | 20 | 有类型提示、docstring、错误处理、assert/doctest |
| 数据结构 | 10 | list/dict/set/tuple 使用合适 |
| 模块化 | 15 | src/ 拆分清楚,导入关系明确 |
| 文件/CLI | 15 | 能读 CSV/JSON,能输出报告,CLI 可运行 |
| 综合项目 | 10 | toy backtest 或数据摘要项目可复现 |
低于 80 分:不要进入 pandas/sklearn。先补 Python。
7. 每日复盘模板
# Day X Python Official Practice
## 今天读的官方章节
## 今天学会的函数或语法
## 今天写的代码
## 每一行代码的逻辑解释
## 今天遇到的错误
## 我现在还解释不清的地方
## 明天继续
最低日任务:
读官方文档 20 分钟
跟着手册敲代码 40 分钟
运行和修错 20 分钟
写复盘 10 分钟
8. 什么时候可以进入 pandas / sklearn / PyTorch
不要用“看过 Python 教程”当作标准。用下面这些能力判断:
能解释 list / dict / set / tuple 的区别
能写带参数和返回值的函数
能处理空列表、路径不存在、类型转换失败
能把函数拆进不同模块
能从命令行传 input/output
能读 CSV 和 JSON
能输出 Markdown 报告
能解释为什么回测要 shift signal
能解释为什么浮点数不能直接 == 比较
这些都能做到,再进入 pandas / sklearn / PyTorch。否则后面遇到的错误看起来像库的问题,其实是 Python 基础问题。
喜欢的话,留下你的评论吧~