跳转至

第 2 周(9/14 一 – 9/20 日):工程化——像一个包那样组织代码

本周目标:src 布局 + pyproject.toml + uv 打包与入口点;Git 工作流并推送到 cnb.cool;typing 进阶与类型检查器;ruff/docstring 规范;Pydantic(两天);logging 与配置/密钥;正则;文件与序列化进阶;SQLite。周日晚交付小项目 知识库 CLI kb

时长:周一–周五 4h × 5 + 周六 10h + 周日 9/20 调休上课 4h = 34h。

本周练习exercises/week2/w2_0N_*.py + test_w2_0N_*.py;小项目是仓库里的一个真正的包 projects/kb/(uv workspace 成员),uv syncuv 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 元数据、dependenciesrequires-python)、[project.scripts](入口点 → uv run kb)、[dependency-groups](PEP 735,uv add --dev)、[build-system](uv_build/hatchling) | A | | uv init --packageuv adduv syncuv runuv build(产出 wheel/sdist)、uvx(临时运行工具)、uv.lock 的意义 | A | | uv workspace:本仓库根 pyproject.toml 声明 [tool.uv.workspace] members = ["projects/*"],所有小项目共享一个 .venv 与锁文件 | B | | __init__.py/__all__/__main__.pypython -m kb)、绝对导入优先、相对导入只在包内 | A | | PEP 723 单文件脚本内联依赖:# /// script + uv run script.py 自动装依赖 | B | | importlib.metadata.version("kb")__version__、SemVer、CHANGELOG.md | B | | 不再使用:setup.py、手工维护 requirements.txtpip 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.tomlsrc/kb/__init__.py__main__.pycli.py 只有 hello 命令、tests/test_cli.py);读懂每个文件为什么存在。 2. 仓库根 uv syncuv 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 --graphswitch -cmergerestore/restore --stagedstash.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 周已用) | | TypedDictNotRequiredReadOnly) | 给"形状固定的字典"(JSON 对象)加类型 | | Literal["a", "b"] | 只允许这几个值;比字符串常量安全 | | FinalSelfAnnotated[T, ...] | 常量、返回自身类型、给类型附加元数据(Pydantic 用它放约束) | | collections.abcIterable/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]) -> Tclass 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 | | overloadTypeGuard/TypeIs(收窄类型的函数)、NevercastTYPE_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 简化、RUFANN(注解)——本仓库开 E F W I UP Bkb 项目再开 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]]@timedParamSpec 保留签名(测试 reveal_type 注释说明);User(TypedDict)NotRequired["email"] + def make_user(**kwargs: Unpack[User])Mode = Literal["r", "w"] + open_mode(m: Mode)@overloadparse(x: str) -> int / parse(x: bytes) -> strRepository[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"→3strict=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]ValidationErrorerrors() 结构(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.pyNote(title 1–100 字、body、tags ≤5 个且自动小写去重、created_at 默认 now、priority Literal)Address 嵌套进 ProfileProfile.age 0–150;@model_validator 保证 start < endMoney(amount: Decimal, currency 3 位大写) + 严格模式;parse_notes(raw: list[dict]) -> tuple[list[Note], list[str]] 收集每条错误的 loc/msgConfigDict(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_aliaspopulate_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-settingsBaseSettings + SettingsConfigDict(env_file=".env", env_prefix="KB_"),嵌套与类型转换;.env.gitignore,仓库放 .env.example | A | | 与 dataclass/TypedDict/msgspec 的边界:内部领域模型用 dataclass,外部输入/输出边界用 Pydantic | A |

练习 w2_05_pydantic_schema_settings.pyEvent 联合类型(Click/KeyPress 判别);dump_for_api(model) -> dictmode="json" 处理 datetime/Decimalschema_summary(Model) -> dictmodel_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):何时值得引入——多服务、需要机器解析日志时;warningslogging.captureWarnings(True) | C |

练习 w2_06_logging_config.pysetup_logging(level, log_file: Path \| None, json: bool) 返回根 logger,幂等(重复调用不重复加 handler);JsonFormatter;用 caplog 测试模块 logger 输出;load_config(path: Path) -> AppConfigtomllib + 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/DOTALLMatch.group/groupdict/spansub 传函数、前后查找 (?=...)(?<=...)(B)、中文 [\u4e00-\u9fff]、灾难性回溯与超时意识、何时不用正则(简单场景用 str 方法、结构化数据用解析器)。 练习 w2_07_regex.pyparse_log_line 命名分组解析 2026-09-05 12:00:01 [ERROR] module: messageextract_emails/extract_cn_phones/extract_dates(ISO)normalize_whitespacestrip_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/moveshutil.rmtree/copytreetempfile | A | | 编码:utf-8 vs utf-8-sig(Excel CSV 的 BOM);二进制 rb/wbhashlib.sha256 文件哈希;大文件分块 | A | | 原子写:同目录临时文件 + os.replace(第 1 周已写,今天用在 JSON 导出) | A | | json.dump(default=) 处理 datetime/Decimal/setjson.loads(object_hook=)tomllib(只读);csv.DictWriter(newline="")pickle 只用于自己写的可信数据 | A / B | | datetime/timedelta/zoneinfo/datetime.UTC(3.11+)/ISO 8601 isoformat/fromisoformat;存储一律 UTC | A | | sqlite3connectwith conn: 事务、? 参数化(防注入)、row_factory = sqlite3.Row、建表/索引、executemanyPRAGMA journal_mode=WALpython -m sqlite3 app.db 交互 shell(3.12+) | A | | SQLAlchemy 2 / SQLModel / aiosqlite:知道存在,需要复杂查询或多表关系时再学 | C |

练习 w2_08_files_sqlite.pyiter_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)/deletesearchLIKE 或在 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(CliRunnertmp_pathmonkeypatch)、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 ./researchuv 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/08091213

模块 主读 补充
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 pathlibsqlite3 文档;《流畅的 Python》第 2–3 章 Real Python sqlite3 教程

本周陷阱清单(周测必考)

在 src 布局里从仓库根直接 python src/kb/cli.py 导入失败(要用 uv run kbpython -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() 无时区存库