第 2 周(9/14 一 – 9/20 日):工程化——像一个包那样组织代码¶
本周目标:src 布局 +
pyproject.toml+ uv 打包与入口点;Git 工作流并推送到 cnb.cool;typing 进阶与类型检查器;ruff/docstring 规范;Pydantic(两天);logging 与配置/密钥;正则;文件与序列化进阶;SQLite。周日晚交付小项目 知识库 CLIkb。时长:周一–周五 4h × 5 + 周六 10h + 周日 9/20 调休上课 4h = 34h。
本周练习:
exercises/week2/w2_0N_*.py+test_w2_0N_*.py;小项目是仓库里的一个真正的包projects/kb/(uv workspace 成员),uv sync后uv run kb --help即可用。对应
learn-agent:M7+M8 → Day 2(类型注解、Pydantic、JSON Schema);M9 → 可观测性与密钥;M11 → Week 2 的 SQLite 记忆;M6 → 每天git commit。
周一 9/14(4h)|M6a 项目结构、pyproject.toml、uv 打包¶
块 1(复习):第 1 周周测错题重做 15 分钟;文本管线遗留收尾。
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| src 布局(src/kb/__init__.py)为什么是 PyPA 现行建议:避免"在仓库根目录能 import、装好后却不能"的假阳性 | A |
| pyproject.toml 三段:[project](PEP 621 元数据、dependencies、requires-python)、[project.scripts](入口点 → uv run kb)、[dependency-groups](PEP 735,uv add --dev)、[build-system](uv_build/hatchling) | A |
| uv init --package、uv add、uv sync、uv run、uv build(产出 wheel/sdist)、uvx(临时运行工具)、uv.lock 的意义 | A |
| uv workspace:本仓库根 pyproject.toml 声明 [tool.uv.workspace] members = ["projects/*"],所有小项目共享一个 .venv 与锁文件 | B |
| __init__.py/__all__/__main__.py(python -m kb)、绝对导入优先、相对导入只在包内 | A |
| PEP 723 单文件脚本内联依赖:# /// script + uv run script.py 自动装依赖 | B |
| importlib.metadata.version("kb")、__version__、SemVer、CHANGELOG.md | B |
| 不再使用:setup.py、手工维护 requirements.txt、pip install 到全局 | A(识别) |
块 3(练习):exercises/week2/w2_01_packaging.md(操作清单,完成后打勾)+ exercises/week2/w2_01_inline_script.py(PEP 723:一个用 rich 打印表格的单文件脚本,uv run 直接跑)。操作清单:
1. 在 projects/kb/ 里核对骨架(已给:pyproject.toml、src/kb/__init__.py、__main__.py、cli.py 只有 hello 命令、tests/test_cli.py);读懂每个文件为什么存在。
2. 仓库根 uv sync;uv run kb hello 小明 输出问候;uv run python -m kb hello 小明 同样可用;解释两者路径差别。
3. uv build --package kb,看 dist/ 里 wheel 的内容(ouch list 或解压);然后删掉 dist/。
4. uv run python -c "import kb, importlib.metadata as m; print(kb.__file__, m.version('kb'))"。
5. 给 kb 加一个 version 子命令,打印版本号;写一条测试。
周二 9/15(4h)|M6b Git 工作流与 cnb.cool + M7a typing 进阶(一)¶
块 2(Git,A 档):init/status/add/diff/diff --staged/commit/log --oneline --graph、switch -c、merge、restore/restore --staged、stash、.gitignore(Python 模板:__pycache__/ .venv/ .pytest_cache/ .ruff_cache/ .coverage htmlcov/ *.db .env site/)、简版 Conventional Commits(feat/fix/docs/refactor/test/chore)。cnb.cool(腾讯云原生构建,国内直连):新建仓库 → 添加远程 git remote add origin <cnb 地址> → SSH key 或令牌 → git push -u origin main;可选再加 GitHub 作第二远程(git remote add github ...)。今天把 learn-python 整个仓库推上去,确认 .venv/、site/、.env 没被提交。
练习 exercises/week2/w2_02_git_drills.md:8 个情景题(改错文件怎么撤销、提交错了怎么改最后一条信息、想试一个想法怎么开分支、分支合并冲突怎么解、把误提交的 .env 从历史里删掉该搜什么),每题写出命令并实际操作一遍。
块 3(typing 进阶一,A 档):
| 知识点 | 一句话 |
| --- | --- |
| Protocol | 结构化子类型:有这些方法就算实现,用来定义接口(第 1 周已用) |
| TypedDict(NotRequired、ReadOnly) | 给"形状固定的字典"(JSON 对象)加类型 |
| Literal["a", "b"] | 只允许这几个值;比字符串常量安全 |
| Final、Self、Annotated[T, ...] | 常量、返回自身类型、给类型附加元数据(Pydantic 用它放约束) |
| collections.abc 的 Iterable/Iterator/Sequence/Mapping/Callable | 参数用宽泛类型(Iterable),返回用具体类型(list) |
| 3.14 延迟注解:不再需要 from __future__ import annotations;前向引用直接写 | 知道 |
周三 9/16(4h)|M7b typing 进阶(二)、类型检查器、ruff 规范¶
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| PEP 695 泛型:def first[T](xs: Sequence[T]) -> T、class Box[T]、type Json = dict[str, "Json"] \| list["Json"] \| str \| int \| float \| bool \| None | A |
| ParamSpec(给装饰器写正确类型:def deco[**P, R](fn: Callable[P, R]) -> Callable[P, R]) | B |
| overload、TypeGuard/TypeIs(收窄类型的函数)、Never、cast、TYPE_CHECKING | B |
| 类型检查器 2026:VS Code 用 Pylance(pyright 内核),python.analysis.typeCheckingMode = "standard";Astral ty 仍是 0.0.x(本周核实 0.0.78,2026-09-02),用 uvx ty check src 体验但不作为门禁;mypy 知道存在 | A(Pylance)/ B(ty) |
| ruff 规则家族:E/W 风格、F 逻辑、I 导入、UP 现代写法、B 常见 bug、N 命名、D docstring、SIM 简化、RUF、ANN(注解)——本仓库开 E F W I UP B,kb 项目再开 N D SIM | A |
| docstring:Google 风格(Args:/Returns:/Raises:),公开函数必写一句话 + 示例;__all__ | A |
| prek/pre-commit:提交前自动跑 ruff(知道存在,kb 里可选启用) | C |
练习 w2_03_typing.py(pytest 测运行时行为 + 必须 Pylance 零报错、uvx ty check exercises/week2/w2_03_typing.py 零错误):Json 递归别名 + is_json(value) -> TypeIs[Json];class Stack[T] + def pairwise[T](xs) -> list[tuple[T, T]];@timed 用 ParamSpec 保留签名(测试 reveal_type 注释说明);User(TypedDict) 含 NotRequired["email"] + def make_user(**kwargs: Unpack[User]);Mode = Literal["r", "w"] + open_mode(m: Mode);@overload 的 parse(x: str) -> int / parse(x: bytes) -> str;Repository[T](Protocol) 泛型协议。
周四 9/17(4h)|M8a Pydantic(一):模型、字段、校验¶
版本基准:Pydantic 2.13.x(2.12 起完整支持 Python 3.14 的延迟注解),
pydantic-settings同期版本,详见research/09。
块 2(新知识,全部 A 档):BaseModel 与类型驱动的解析(lax 模式 "3"→3,strict=True 关闭);默认值与 Optional——int | None = None 才是"可省略";Field(gt=0, min_length=1, max_length=50, pattern=r"...", description=..., alias=..., default_factory=list);Annotated[int, Field(gt=0)] 写法;嵌套模型与 list[Item];ValidationError 的 errors() 结构(loc/msg/type)与友好打印;@field_validator("x", mode="after"/"before")、@model_validator(mode="after") 跨字段校验;ConfigDict(extra="forbid", str_strip_whitespace=True, frozen=True);model_copy(update=)。v1 老写法识别:@validator、.dict()、.parse_obj()、class Config。
预测:
from pydantic import BaseModel, Field, ValidationError
class Item(BaseModel):
name: str = Field(min_length=1)
qty: int = Field(gt=0)
tags: list[str] = []
print(Item(name="a", qty="3")) # qty 是 int 还是 str?
try: Item(name="", qty=0)
except ValidationError as e: print(len(e.errors()), [x["loc"] for x in e.errors()])
a = Item(name="a", qty=1); b = Item(name="b", qty=1)
a.tags.append("x"); print(b.tags) # 共享了吗?(Pydantic 会深拷贝默认值)
练习 w2_04_pydantic_models.py:Note(title 1–100 字、body、tags ≤5 个且自动小写去重、created_at 默认 now、priority Literal);Address 嵌套进 Profile,Profile.age 0–150;@model_validator 保证 start < end;Money(amount: Decimal, currency 3 位大写) + 严格模式;parse_notes(raw: list[dict]) -> tuple[list[Note], list[str]] 收集每条错误的 loc/msg;ConfigDict(extra="forbid") 拒绝未知字段。
周五 9/18(4h)|M8b Pydantic(二):序列化、JSON Schema、Settings、TypeAdapter¶
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| model_dump(mode="json", exclude_none=True, by_alias=True) / model_dump_json(indent=2) / model_validate_json | A |
| 别名:alias(输入)与 serialization_alias、populate_by_name | B |
| @computed_field、@field_serializer | B |
| TypeAdapter(list[Note]).validate_python(...):不建模型也能校验任意类型;复用它提高性能 | A |
| 判别联合:Annotated[Cat \| Dog, Field(discriminator="kind")] | B |
| model_json_schema():看 properties/required/description/default 如何来自类型与 Field——这是所有"函数 → 结构化参数说明"的机制 | A |
| pydantic-settings:BaseSettings + SettingsConfigDict(env_file=".env", env_prefix="KB_"),嵌套与类型转换;.env 进 .gitignore,仓库放 .env.example | A |
| 与 dataclass/TypedDict/msgspec 的边界:内部领域模型用 dataclass,外部输入/输出边界用 Pydantic | A |
练习 w2_05_pydantic_schema_settings.py:Event 联合类型(Click/KeyPress 判别);dump_for_api(model) -> dict 用 mode="json" 处理 datetime/Decimal;schema_summary(Model) -> dict 从 model_json_schema() 提取 {"required": [...], "fields": {name: {"type", "description", "default"}}};Settings(BaseSettings) 读 KB_DB_PATH/KB_LOG_LEVEL(测试用 monkeypatch.setenv);TypeAdapter 校验 list[Note] 并计时对比逐条建模型。
周六 9/19(10h)|M9 logging 与配置 + M10 正则 + M11 文件与 SQLite + 项目¶
上午前 2 块 — M9 logging 与配置:
| 知识点 | 档 |
| --- | --- |
| 每个模块顶部 logger = logging.getLogger(__name__);库代码不配置 handler,只有入口(CLI main)配置根 logger | A |
| 级别语义:DEBUG 排查 / INFO 里程碑 / WARNING 可恢复 / ERROR 失败 / logger.exception 带堆栈 | A |
| dictConfig:console handler(rich.logging.RichHandler)+ RotatingFileHandler(encoding="utf-8") + JSON Formatter(手写或 python-json-logger);3.12+ QueueHandler 非阻塞写日志(B) | A |
| 为什么不用 print:无级别、无时间、无模块、无法关闭 | A |
| 配置来源优先级:命令行参数 > 环境变量 > .env > 默认值;tomllib 读 TOML 配置;密钥永不进代码与 Git | A |
| structlog(v26):何时值得引入——多服务、需要机器解析日志时;warnings → logging.captureWarnings(True) | C |
练习 w2_06_logging_config.py:setup_logging(level, log_file: Path \| None, json: bool) 返回根 logger,幂等(重复调用不重复加 handler);JsonFormatter;用 caplog 测试模块 logger 输出;load_config(path: Path) -> AppConfig(tomllib + Pydantic);resolve_setting(cli_value, env_name, default) 实现优先级。
上午第 3 块 + 下午第 1 块 — M10 正则:
re.search/match/fullmatch/findall/finditer/sub/split/compile、原始字符串、\d\w\s.^$[]、量词与贪婪/懒惰 .*?、分组/命名分组 (?P<name>...)/非捕获 (?:...)、re.VERBOSE/IGNORECASE/MULTILINE/DOTALL、Match.group/groupdict/span、sub 传函数、前后查找 (?=...)(?<=...)(B)、中文 [\u4e00-\u9fff]、灾难性回溯与超时意识、何时不用正则(简单场景用 str 方法、结构化数据用解析器)。
练习 w2_07_regex.py:parse_log_line 命名分组解析 2026-09-05 12:00:01 [ERROR] module: message;extract_emails/extract_cn_phones/extract_dates(ISO);normalize_whitespace;strip_markdown(text)(去 #、**、链接保留文字、代码块整体删除);tokenize_zh_en(text)(中文按字、英文按词);mask_secrets(text)(sk-...、手机号中间 4 位);VERBOSE 写一个可读的 URL 正则并提取 scheme/host/path。
下午第 2、3 块 — M11 文件与序列化进阶 + SQLite:
| 知识点 | 档 |
| --- | --- |
| pathlib 全家:rglob/iterdir/stat/rename/unlink/with_suffix/relative_to/mkdir(parents, exist_ok);3.14 copy/move;shutil.rmtree/copytree;tempfile | A |
| 编码:utf-8 vs utf-8-sig(Excel CSV 的 BOM);二进制 rb/wb;hashlib.sha256 文件哈希;大文件分块 | A |
| 原子写:同目录临时文件 + os.replace(第 1 周已写,今天用在 JSON 导出) | A |
| json.dump(default=) 处理 datetime/Decimal/set;json.loads(object_hook=);tomllib(只读);csv.DictWriter(newline="");pickle 只用于自己写的可信数据 | A / B |
| datetime/timedelta/zoneinfo/datetime.UTC(3.11+)/ISO 8601 isoformat/fromisoformat;存储一律 UTC | A |
| sqlite3:connect、with conn: 事务、? 参数化(防注入)、row_factory = sqlite3.Row、建表/索引、executemany、PRAGMA journal_mode=WAL、python -m sqlite3 app.db 交互 shell(3.12+) | A |
| SQLAlchemy 2 / SQLModel / aiosqlite:知道存在,需要复杂查询或多表关系时再学 | C |
练习 w2_08_files_sqlite.py:iter_markdown_files(root) -> Iterator[Path];file_sha256(path);export_json(path, notes)(原子写 + default=);read_csv_with_bom(path);utc_now_iso();class SqliteNoteRepository(实现第 1 周的 Repository 协议:add/get/list/search(pattern)/delete,search 用 LIKE 或在 Python 侧用正则);migrate(conn) 建表建索引幂等;用 tmp_path 下的 db 文件测试。
晚上 2 块 — 小项目 kb 主体(见下节)。
周日 9/20(调休上课,4h)|完成 kb、推送、周测¶
块 1:kb 收尾(测试、覆盖率、ruff、Pylance);块 2:README + .env.example + Conventional Commits 整理提交历史 + git push;块 3:周测(exercises/week2/quiz_week2.md,40 分钟)+ AI 评审(模板 4,只改前 3 条);收尾:周日志、预览下周(读 research/10 核心结论)。
小项目:知识库 CLI projects/kb/(≈8h,周六晚 + 周日)¶
用途:命令行管理学习笔记:添加、列出、按关键词/正则搜索、导出 JSON、从 Markdown 目录批量导入。全部知识点:src 布局 + 入口点、Pydantic 模型与 Settings、SQLite 仓储、Protocol、logging、正则、pathlib、typer/rich、pytest(CliRunner、tmp_path、monkeypatch)、Git。
结构(骨架已给,TODO 处由你实现):
projects/kb/
├── pyproject.toml # name = "kb"; [project.scripts] kb = "kb.cli:app"; 依赖 typer rich pydantic pydantic-settings
├── README.md
├── .env.example # KB_DB_PATH=./kb.db KB_LOG_LEVEL=INFO
├── src/kb/
│ ├── __init__.py # __version__
│ ├── __main__.py # from kb.cli import app; app()
│ ├── settings.py # Settings(BaseSettings):db_path, log_level, log_file
│ ├── logging_config.py # setup_logging(settings)
│ ├── models.py # Note(BaseModel):id|None, title, body, tags, created_at
│ ├── repository.py # NoteRepository(Protocol) + SqliteNoteRepository + InMemoryNoteRepository
│ ├── service.py # NoteService:add/list/search/export_json/import_markdown(纯逻辑,不 print)
│ ├── markdown_import.py # 用正则从 .md 提取标题与标签(`#tag`),复用第 1 周管线思路
│ └── cli.py # typer:add/list/search/export/import/version;只在这里 print(rich Table)
└── tests/
├── conftest.py # fixture:临时 db 的 repository、service、CliRunner
├── test_models.py test_repository.py test_service.py test_markdown_import.py test_cli.py
验收:uv run kb add "标题" --tag python --body "..." / kb list --tag python / kb search "正则.*表达式" --regex / kb export out.json / kb import ./research;uv run pytest projects/kb --cov=kb --cov-report=term-missing 覆盖率 ≥80%;uv run ruff check projects/kb 零警告(含 N D SIM);Pylance/uvx ty check projects/kb/src 零错误;日志同时进控制台(rich)与 kb.log(JSON、UTF-8);.env 未入库、.env.example 已入库;≥6 次 Conventional Commits 并推送 cnb.cool。
本周阅读(详见 research/08、09、12、13)¶
| 模块 | 主读 | 补充 |
|---|---|---|
| M6 | Python Packaging User Guide(src 布局、pyproject);uv 文档 Projects/Workspaces | 《Python 工匠》工程与规范章 |
| Git | Pro Git 中文版第 2–3 章;Learn Git Branching 交互站 | cnb.cool 帮助文档 |
| M7 | typing.python.org 文档站;Real Python type checking 系列 |
《流畅的 Python》第 8、15 章 |
| M8 | Pydantic 官方 Concepts:Models → Fields → Validators → Serialization → JSON Schema → Settings | research/09 的 v1→v2 对照表 |
| M9 | 官方 Logging HOWTO + Cookbook | Rich RichHandler 文档 |
| M10 | 官方 Regex HOWTO;re 文档 |
regex101.com(在线调试,选 Python 方言) |
| M11 | pathlib、sqlite3 文档;《流畅的 Python》第 2–3 章 |
Real Python sqlite3 教程 |
本周陷阱清单(周测必考)¶
在 src 布局里从仓库根直接 python src/kb/cli.py 导入失败(要用 uv run kb 或 python -m kb)|.env 提交进 Git|Optional[int] 以为可省略|Pydantic 默认值写成可变对象却以为会共享(其实会拷贝——和 dataclass 相反)|model_dump() 不带 mode="json" 导出 datetime 失败|库模块里 basicConfig 抢配置|logger.error(e) 丢堆栈(应 logger.exception)|重复 setup_logging 日志打两遍|正则忘 r""|re.match 只匹配开头|.* 贪婪吃掉整行|SQL 字符串拼接|忘 with conn: 事务不提交|json.dump 中文变 \uXXXX|CSV 不写 newline=""|Windows 上 datetime.now() 无时区存库