Files
zhuangang/plan/week06-python-basics.md
T

14 KiB
Raw Blame History

转岗计划 · 第 6 周学习计划(Python 速成:给 JS 开发者)

所属阶段:第二段“Python 与 AI 应用”的第一周。Python 是求职的主语言,后面的 RAG 项目(第 78 周)和多 Agent 项目(第 1114 周)都用它。总纲见同目录 README.md,下周计划见 rag-project.md。

目标:一周内把写 AI 应用服务需要的 Python 补齐:基础语法、类型标注、asyncio、pydantic、httpx、pytest、FastAPI 和 SSE。不追求全面,只学项目里马上会用到的。

学法:你有 7 年 JS 和一个月 Go 的底子,每个知识点都先想“JS / Go 里对应的是什么”,再看 Python 的写法。每天的练习文件第一段写一张小对照表。

本周只学 Python,不开 RAG 和 Agent 项目(戒律:同一周不同时开新框架和新功能)。招聘里 FastAPI 和 Flask 都常见,本计划用 FastAPI;Flask 概念更少,会了 FastAPI 再看 Flask 半天就能上手。

参考资料:


最终项目结构

py-learning/
├── pyproject.toml          # uv 管理依赖
├── .python-version
├── day1_basics.py
├── day2_typing.py
├── day3_errors_files.py
├── day4_async.py
├── day5_models.py
├── day6_http.py
├── fastapi_demo/
│   ├── __init__.py
│   ├── main.py             # FastAPI 应用
│   └── static/
│       └── sse.html        # 用 EventSource 订阅 SSE 的测试页
└── tests/
    ├── test_day2.py
    ├── test_day5.py
    ├── test_day6.py
    └── test_fastapi_demo.py

第 1 天:环境和基础语法

学习内容

  • 用 uv 管理 Python 版本和依赖(相当于 nvm + npm 合在一起):curl -LsSf https://astral.sh/uv/install.sh | sh。
  • 缩进代替大括号;None / True / False;list、dict、tuple、set。
  • f-string(相当于模板字符串)、切片 a[1:3]、解包 a, *rest = items。
  • 列表推导式和字典推导式(相当于 map + filter)。
  • 函数:默认参数、关键字参数、*args、**kwargs;默认参数不要用可变对象(def f(x=[]) 是经典坑)。
  • if __name__ == "__main__": 的作用。

要建的文件

  • py-learning/:运行 uv init py-learning && cd py-learning && uv python pin 3.12,生成 pyproject.toml 和 .python-version。
  • day1_basics.py:
    • 文件开头用注释写一张 JS ↔ Python 对照表(至少 10 行:const/let、===、null/undefined、arr.map、arr.filter、对象展开、解构、模板字符串、for...of、箭头函数)。
    • def word_count(text: str) -> dict[str, int]:和第 1 周 Go 的 WordCount 做同一件事,对比代码长短。
    • def unique(items: list[str]) -> list[str]:去重并保持原顺序。
    • 演示可变默认参数的坑和正确写法(def f(x=None))。

运行方式:uv run python day1_basics.py

完成标准:能说清楚 Python 的 list 和 tuple 区别,以及为什么默认参数不能用 []。


第 2 天:类型标注、dataclass 和 Protocol

学习内容

  • 类型标注只是提示,运行时不检查(和 TS 一样),要靠检查工具:uv add --dev pyright ruff pytest。
  • list[str]、dict[str, int]、X | None、Literal["a", "b"]、TypedDict。
  • @dataclass:相当于带构造函数的结构体。
  • Protocol:Python 的“隐式接口”,和 Go 的 interface 思路一样,Agent 项目里 LLM 客户端和存储层都会用它。
  • 类、self、@property、@classmethod。

要建的文件

  • day2_typing.py:
    • TaskStatus = Literal["todo", "doing", "done"]
    • @dataclass class Task:id: int、title: str、status: TaskStatus = "todo"、tags: list[str] = field(default_factory=list)。
    • class TaskStore(Protocol):add(self, task: Task) -> None、list(self) -> list[Task]。
    • class MemoryTaskStore:不继承 TaskStore,但实现同样的方法(体会“隐式实现”)。
    • def count_by_status(tasks: list[Task]) -> dict[TaskStatus, int]
  • tests/test_day2.py:测试 count_by_status;用 pytest.mark.parametrize 写表格驱动测试(对应 Go 的表格驱动测试)。

运行方式:uv run pytest -v、uv run pyright day2_typing.py、uv run ruff check .

完成标准:故意把 status 写成 "deleted",pyright 能报错;能说清楚 Protocol 和 Go 接口的相同点。


第 3 天:异常、上下文管理器和文件

学习内容

  • try / except / else / finally,raise ... from err(相当于 Go 的 %w 包装错误)。
  • 自定义异常层级:一个基类,下面分具体错误。
  • with 语句(上下文管理器):自动关闭文件、连接,相当于 Go 的 defer。
  • pathlib.Path、json.loads / json.dumps(注意 ensure_ascii=False 才能正常输出中文)。
  • logging 基本用法。

要建的文件

  • day3_errors_files.py:
    • class AppError(Exception),带 code: str 属性;子类 NotFoundError、ValidationError。
    • def load_tasks(path: Path) -> list[dict]:文件不存在返回空列表;JSON 格式错误时 raise ValidationError(...) from err。
    • def save_tasks(path: Path, tasks: list[dict]) -> None:先写临时文件再 replace,避免写到一半崩溃导致文件损坏(Agent 项目里“写状态要原子”就是这个道理)。
    • 用 contextlib.contextmanager 写一个 timer(name),在 with timer("load"): 结束时打印耗时。

运行方式:uv run python day3_errors_files.py

完成标准:能说清楚 raise X from err 保留了什么信息,以及“先写临时文件再改名”为什么更安全。


第 4 天:asyncio(本周最重要的一天)

学习内容

  • async def / await 和 JS 很像,但要有事件循环:入口用 asyncio.run(main())。
  • 和 JS 的区别:调用 async 函数只得到协程对象,不会自动开始执行,要 await 或 asyncio.create_task。
  • asyncio.gather(相当于 Promise.all)、asyncio.wait_for / asyncio.timeout(超时)、asyncio.Queue(相当于 Go 的 channel)、asyncio.Event。
  • 取消:task.cancel() 会在任务里抛出 CancelledError,要在 finally 里清理,不要吞掉这个异常。
  • 异步生成器 async def ... yield,以及 async for:SSE 推送就是靠它。
  • 不能在 async 函数里调用会阻塞的同步代码(比如 time.sleep、同步的数据库驱动),否则整个服务卡住;实在要用就 await asyncio.to_thread(...)。

要建的文件

  • day4_async.py:
    • async def fake_llm(name: str, delay: float) -> str:await asyncio.sleep(delay) 后返回结果,模拟一次模型调用。
    • async def run_parallel():用 gather 同时调 3 个 fake_llm,打印总耗时,和顺序调用对比。
    • async def run_with_timeout():给一个 3 秒的调用设 1 秒超时,捕获 TimeoutError 并打印。
    • async def producer_consumer():一个生产者往 Queue 放 5 个任务,两个消费者处理,全部处理完后退出(queue.join())。
    • async def ticker(n: int):异步生成器,每 0.5 秒 yield 一个数字;用 async for 消费,消费到第 3 个时取消,观察 finally 是否执行。
    • 文件开头写对照表:Promise.all ↔ gather、setTimeout ↔ asyncio.sleep、Go 的 go f() ↔ create_task、Go 的 channel ↔ Queue、Go 的 context 取消 ↔ task.cancel()。

运行方式:uv run python day4_async.py

完成标准:能说清楚“调用 async 函数但没 await”会发生什么;能解释为什么在 async 服务里用同步阻塞调用会拖垮所有请求。


第 5 天:pydantic 数据模型

学习内容

  • pydantic 是“带运行时校验的类型”,作用类似前端的 zod:外部进来的数据(请求体、模型输出的 JSON)都要先过一遍校验。
  • BaseModel、Field(min_length=..., max_length=...)、默认值、model_validate、model_validate_json、model_dump、model_dump_json。
  • Literal 字段和“按某个字段区分类型”的联合类型(discriminated union)。
  • ValidationError 里能拿到哪个字段、为什么错。
  • pydantic-settings:从环境变量和 .env 读配置。

要建的文件

  • uv add pydantic pydantic-settings
  • day5_models.py:
    • class Decision(BaseModel):action: Literal["dispatch", "finish", "ask_human"]、target: str | None = None、instruction: str = ""、reason: str。(这就是第 11~12 周多 Agent 项目里协调者输出的雏形)
    • def parse_decision(raw: str) -> Decision:模型常常会在 JSON 外面包一层 ```json 代码块或多说几句话,先提取出第一个 {...} 再 model_validate_json;失败时抛出自定义的 BadModelOutput,错误信息里带上原始文本的前 200 个字。
    • class Settings(BaseSettings):llm_base_url: str、llm_api_key: str = ""、llm_model: str、llm_timeout_s: float = 30,从 .env 读取。
  • tests/test_day5.py:参数化测试 parse_decision:正常 JSON、带代码块的 JSON、前后有废话的 JSON、action 拼错、缺少 reason、完全不是 JSON。

运行方式:uv run pytest tests/test_day5.py -v

完成标准:6 种输入都按预期通过或报错;能说清楚“为什么模型输出必须当成不可信的外部输入来校验”。


第 6 天:httpx 异步请求和测试

学习内容

  • httpx.AsyncClient:设置 base_url、timeout、请求头;用 async with 管理生命周期,复用连接。
  • 状态码和异常:raise_for_status()、httpx.TimeoutException、httpx.ConnectError。
  • 测试时不真的发请求:httpx.MockTransport 可以按请求返回任意响应,用来模拟超时、429、500。
  • pytest 跑异步测试:uv add --dev pytest-asyncio,在 pyproject.toml 里设置 asyncio_mode = "auto"。

要建的文件

  • uv add httpx
  • day6_http.py:
    • class HttpError(Exception),带 code 和 retryable: bool。
    • async def get_json(client: httpx.AsyncClient, url: str, retries: int = 2) -> dict:超时和 5xx、429 时等待后重试(等待时间逐次翻倍),401 / 400 不重试,最后抛出带错误码的 HttpError。
    • main() 里请求一个公开接口(例如 https://httpbin.org/json)打印结果。
  • tests/test_day6.py,用 MockTransport 测试:
    • 200 直接返回。
    • 前两次 503、第三次 200,最终成功,并断言一共请求了 3 次。
    • 401 不重试,直接抛 code == "AUTH"。
    • 一直超时,最终抛 code == "TIMEOUT"。

运行方式:uv run python day6_http.py、uv run pytest tests/test_day6.py -v

完成标准:四个测试都通过;能说清楚哪些错误值得重试、哪些不值得。


第 7 天:FastAPI 基础和 SSE

学习内容

  • FastAPI 路由、路径参数、查询参数,请求体直接用 pydantic 模型(对照第 2 周的 Gin)。
  • 依赖注入 Depends。
  • lifespan:服务启动和关闭时执行的代码(第 13 周用它在启动时恢复未完成的任务)。
  • 后台任务:用 asyncio.create_task 启动,要把任务对象保存起来,否则可能被垃圾回收。
  • SSE 格式:id: 1\nevent: progress\ndata: {...}\n\n;用 StreamingResponse(生成器, media_type="text/event-stream") 返回;浏览器用 EventSource 订阅,断线会自动重连并带上 Last-Event-ID 请求头。
  • 检测客户端断开:await request.is_disconnected()。

要建的文件

  • uv add fastapi uvicorn
  • fastapi_demo/main.py:
    • GET /health。
    • POST /jobs:请求体 {"n": 10},用 create_task 启动一个后台任务,每秒把进度追加到内存里的 jobs[job_id]["events"] 列表,返回 {"job_id": ...}。
    • GET /jobs/{job_id}:返回当前状态和已产生的事件数。
    • GET /jobs/{job_id}/events:SSE。先补发 Last-Event-ID 之后的事件,再等待新事件,任务完成后发 event: done 并结束;每 15 秒没有新事件就发一行 : ping 心跳。
    • 关键实验:浏览器关掉后,后台任务要继续跑完(因为任务和 SSE 连接是分开的)。
  • fastapi_demo/static/sse.html:一个按钮启动任务,用 EventSource 显示进度。
  • tests/test_fastapi_demo.py:用 fastapi.testclient.TestClient 测 /health 和创建任务。

运行方式:uv run uvicorn fastapi_demo.main:app --reload,浏览器打开测试页;或者 curl -N localhost:8000/jobs/<id>/events。

完成标准

  • 启动任务后关掉页面,再打开、重新订阅,能看到任务已经继续往前跑,而且能补齐中间漏掉的事件。
  • 能说清楚“编排为什么不能只活在浏览器里”——这就是重点 C,也是第 13 周要做的核心功能的缩小版。
  • uv run ruff check . && uv run pytest 通过,提交并推送到 zhuangang 仓库(路径 py-learning/)。
  • 在总纲 README.md 的进度记录里勾掉第 6 周。

本周自查清单

  • 会用 uv 建项目、加依赖、跑脚本
  • 熟悉列表推导式、解包、f-string,知道可变默认参数的坑
  • 会写类型标注、dataclass、Protocol,会用 pyright 检查
  • 会用自定义异常和 raise ... from
  • 理解 asyncio:协程、create_task、gather、超时、取消、Queue、异步生成器
  • 会用 pydantic 校验外部数据(包括模型输出)
  • 会用 httpx 发异步请求、按错误类型决定是否重试,会用 MockTransport 测试
  • 会写 FastAPI 接口和 SSE,理解后台任务和 SSE 连接是两回事

下周预告:第 7~8 周做 RAG 知识库问答项目(FastAPI + Chroma),见 rag-project.md。本周写的 httpx 重试、pydantic 解析、SSE 都会直接用上。