Python 官方教程综合练习手册:Oh My Pi Terminal Edition

公開日: 2026-07-27 12:30 7695文字 39 min read

この投稿は「日本語」では表示できません。元の投稿を表示しています。
按 Python 官方教程重组的 4 周项目化练习手册:用 Oh My Pi 终端美学讲清解释器、函数、数据结构、模块、文件、CLI、类、生成器、浮点数和 toy quant backtest。
oh-my-pi python manual · terminal edition
oh-my-pi@python:~/official-tutorial$ cat beginner-practice-manual.md
Python Official Tutorial × AI / Quant Practice

把官方教程变成能敲、能跑、能解释的 4 周终端练习。

每个练习都先讲函数怎么用,再拆代码逻辑,最后给验收问题。目标不是背语法,而是能进入 pandas、sklearn、PyTorch 和回测项目前不再被 Python 基础卡住。

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 Toolsifforrangematch、函数、参数、编码风格
5. Data Structureslist、tuple、set、dict、comprehension、循环技巧
6. Modules模块、包、__name__ == "__main__"、导入规范
7. Input and Outputf-string、文件读写、JSON
8. Errors and Exceptions语法错误、异常、tryraise、清理动作
9. Classes对象、作用域、类、实例、迭代器、生成器
10. Brief Tour of the Standard Librarypathlibcsvjsonargparsestatisticslogging
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.pyreports/python_basics_report.md
Week B函数、参数、数据结构、风格src/stats_utils.pysrc/metrics.py
Week C模块、文件、JSON、异常、CLIsrc/data_io.pysrc/cli.pyreports/data_summary.md
Week D类、迭代器、标准库、浮点数、综合项目src/toy_backtest.pysrc/experiment.pyreports/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:模块拆分

要求:

  • meanvariance 放到 src/stats_utils.py
  • accuracyconfusion_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.pysrc/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

至少给 meanvariance 加 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.py
  • src/stats_utils.py
  • src/metrics.py
  • src/data_io.py
  • src/cli.py
  • src/toy_backtest.py
  • src/experiment.py
  • reports/data_summary.md
  • reports/backtest_report.md
  • reports/floating_point_notes.md
  • notes/official_tutorial_mapping.md

6. 评分标准

项目分值合格标准
环境可复现15README 写清环境和命令,.venv 不入 Git
基础语法15能写数字、字符串、列表、控制流练习
函数质量20有类型提示、docstring、错误处理、assert/doctest
数据结构10list/dict/set/tuple 使用合适
模块化15src/ 拆分清楚,导入关系明确
文件/CLI15能读 CSV/JSON,能输出报告,CLI 可运行
综合项目10toy 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 基础问题。

気に入ったならばコメントを残してくださいね~

© 2026 江无没有月 @miku
Powered by theme astro-koharu · Inspired by Shoka