很多人学 Python 时都有一种错觉:变量、循环、类、async 都看过,FastAPI 教程也跟着敲过,可一到真实需求——“接收订单、校验参数、计算金额、保存数据、返回接口”——代码立刻散成一地。问题通常不是知识点没学,而是知识点之间没有形成业务链路。语法像砖,函数像工位,数据结构像货架,对象像订单本体,模块像车间,异常像消防通道,异步像调度系统,装饰器像安检门,FastAPI 才是对外营业的窗口。
我是老李。为了系统的复习python3,用一个可运行的“订单 API”贯穿全篇。先把九块知识放进一张文字思维导图,再逐章把每一块的概念、类比、最佳实践、权威资料、代码和易错点讲透。
九大知识点思维导图
- 语言地基
- 01 Python 基础环境与语法:让数据有名字、有类型、有边界
- 02 流程控制与函数:让规则可选择、可重复、可组合
- 工程骨架
- 05 模块、包与文件:拆开职责,接住持久化输入输出
- 06 异常处理与调试:把故障变成可识别、可定位的信息
- 服务出口
- 08 高级特性:用类型、装饰器和上下文统一横切能力
- 09 FastAPI 实战:把九层能力组装成可测试的 HTTP 服务
接下来不要把它当九门互不相干的内容。我们只做一件事:让一张订单从原始字典出发,经过校验、计算、存储、并发查询,最后变成一个行为明确的 API。
01、Python 基础环境与语法:先把地基浇平
Python 基础不只是“会写 print”。真正进入工程后,基础包含四件事:选定解释器版本、隔离依赖、理解对象与变量、用类型提示表达意图。可以把解释器理解成厨房,虚拟环境是每家店独立的调料柜;所有项目共用一个柜子,今天升级的盐,很可能让昨天的菜变味。
当前示例要求 Python 3.11 以上,并把项目元数据写进 pyproject.toml。业务里金额不用 float,而用 Decimal:二进制浮点适合大量科学计算,却不适合要求十进制精确的货币账本。
from decimal import Decimal
商品 = "机械键盘"
数量: int = 2
单价 = Decimal("399.00")
金额 = (单价 * 数量).quantize(Decimal("0.01"))
assert 金额 == Decimal("798.00")
最佳实践:版本约束写进项目配置;依赖放进隔离环境;金额、时间、标识符选择与业务含义一致的类型;变量名表达“是什么”,不要只写 x1、tmp2。Python 官方教程的解释器与基础语法是这一层最可靠的起点,项目打包则遵循 Python Packaging User Guide。
易错点与思考:类型提示不会自动拦截错误输入,它更像建筑图纸,不是门禁。真正的运行时校验要在边界发生,这也是第九章使用 Pydantic 的原因。
02、流程控制与函数:把规则做成流水线
if、for 和函数分别解决“走哪条路”“重复处理什么”“这段能力叫什么”。类比订单分拣中心:条件是岔道开关,循环是传送带,函数是可复用工位。把所有逻辑塞进一大段脚本,就像工人追着包裹满仓库跑,任何规则变化都会牵一发动全身。
下面的函数只负责筛掉数量非法的订单,再把有效字典交给模型校验。输入和输出都写清楚,调用方不需要知道内部的循环细节。
def 筛选有效订单(
原始订单: list[dict[str, object]],
) -> list[OrderCreate]:
return [
OrderCreate.model_validate(订单)
for 订单 in 原始订单
if isinstance(订单.get("quantity"), int)
and 订单["quantity"] > 0
]
最佳实践:函数保持单一职责;优先提前返回,减少多层嵌套;默认参数不要使用可变对象;公开函数补充类型和简短文档。需要传很多离散参数时,先判断它们是否应该组成一个模型。Python 官方的流程控制详细说明了函数参数、match 和循环控制。
易错点与思考:列表推导式不是越短越好。当过滤、转换、记录日志和异常恢复挤在同一行时,应还原成普通循环或拆成多个函数。代码的第一读者永远是下一位维护者。
03、数据结构:给每类数据安排正确货架
列表、元组、集合、字典都能“装数据”,但它们承诺的行为不同:列表保序且可变,元组适合固定组合,集合强调去重与成员判断,字典通过键快速定位值。它们像仓库里的传送带、封箱、门禁名单和编号货架,选错容器,后面只能用额外代码补救。
订单仓库最自然的结构是 dict[UUID, OrderRead],因为核心动作是“按订单号查询”;如果改用列表,每次查询都要线性扫描。
class InMemoryOrderRepository:
def __init__(self) -> None:
self._orders: dict[UUID, OrderRead] = {}
async def save(self, order: OrderRead) -> None:
self._orders[order.id] = order
async def get(self, order_id: UUID) -> OrderRead | None:
return self._orders.get(order_id)
最佳实践:先从访问模式倒推容器,而不是凭熟悉度选择;需要稳定顺序用列表,需要唯一性用集合,需要键值索引用字典;跨边界传输时再转换成 JSON 友好结构。官方数据结构教程还解释了栈、队列、推导式和遍历技巧。
易错点与思考:dict.get() 返回 None,但“没有这个键”和“键对应的值就是 None”可能是两种业务状态。容器的默认行为必须与领域语义对齐,必要时显式抛出业务异常。
04、面向对象:让数据和规则重新站在一起
面向对象的价值不是把所有函数塞进 class,而是找到应共同变化的状态与行为。订单服务像餐厅领班:它不亲自管理仓库货架,也不负责 HTTP 接待,只协调“计算金额、创建订单、保存订单”这组业务规则。
class OrderService:
def __init__(self, repository: InMemoryOrderRepository) -> None:
self._repository = repository
async def create(self, command: OrderCreate) -> OrderRead:
amount = (
command.unit_price * command.quantity
).quantize(Decimal("0.01"))
order = OrderRead(
id=uuid4(),
item=command.item,
quantity=command.quantity,
unit_price=command.unit_price,
amount=amount,
status=OrderStatus.CREATED,
)
await self._repository.save(order)
return order
最佳实践:优先组合而不是深层继承;构造函数接收依赖,不在方法内部偷偷创建;领域对象避免暴露任意修改入口;类名表示职责,而不是笼统地叫 Utils、Manager。Python 官方类教程是语法依据,但工程设计还要不断追问:哪些变化应该被封装在一起?
易错点与思考:不要为了“面向对象”制造只有一个静态方法的空壳类。没有状态、也不需要多态的能力,普通函数往往更清楚。对象是边界工具,不是代码的宗教。
05、模块、包与文件:把单间作坊改造成车间
当代码超过一个文件,模块边界就开始决定维护成本。模块像车间,包像厂区目录,__init__.py 是厂区入口,导入路径则是物流路线。本例把数据模型放进 models.py,把仓储和业务规则放进 service.py,把 HTTP 入口放进 main.py;每个文件只回答一类问题。
文件读写也属于边界操作。与其手写字符串路径,不如用 pathlib.Path;文本必须明确编码;JSON 需要处理 Python 对象到传输格式的转换。
from pathlib import Path
import json
文件 = Path("orders.json")
文件.write_text(
json.dumps(订单列表, ensure_ascii=False),
encoding="utf-8",
)
最佳实践:使用绝对导入或清晰的包内相对导入;入口文件负责组装,不承载领域规则;文件操作使用上下文管理器或 Path 高层方法;项目配置逐步迁移到 pyproject.toml。可对照官方的模块教程、输入输出教程和 PyPA 的pyproject.toml 现代化指南。
下面不是示意结果,而是本次文章配套程序的真实运行输出:同一条链路依次经过语法、函数、容器、对象、文件、异常、异步、装饰器和 API。
同一套代码还执行了三项接口契约测试:创建与查询成功、非法数量被边界校验拒绝、不存在的订单被稳定翻译为 404。截图来自本次实际执行的 pytest 输出。
易错点与思考:循环导入通常不是“导入语句写错”,而是职责互相纠缠。若 models.py 要导入 main.py,同时 main.py 又导入模型,就应重新划分依赖方向,而不是继续调整导入顺序。
06、异常处理与调试:让故障会说人话
异常不是程序失败的同义词,而是从底层向上层传递“无法按约定完成”的结构化信号。它像高速公路的分流系统:正常车辆走主路,可恢复故障进入处理匝道,资源清理无论成功失败都必须经过出口。
订单不存在时,仓储层返回空值,服务层把它提升为 OrderNotFound,接口层再翻译成 HTTP 404。这样领域代码不认识 HTTP,接口代码也不用猜“空值到底是什么意思”。
class OrderNotFound(Exception):
def __init__(self, order_id: UUID) -> None:
self.order_id = order_id
super().__init__(f"订单不存在:{order_id}")
async def get(self, order_id: UUID) -> OrderRead:
order = await self._repository.get(order_id)
if order is None:
raise OrderNotFound(order_id)
return order
最佳实践:捕获最具体的异常;只在能补充上下文、恢复或翻译语义的层处理;保留异常链;日志记录订单号等定位字段,但不泄露密钥和隐私;清理动作放进上下文管理器或 finally。官方错误与异常教程给出了 try/except/else/finally 的准确语义。
易错点与思考:except Exception: pass 相当于把火警铃拔掉。调试也不能只盯最后一行报错,要同时保存复现输入、完整堆栈和关键状态,再从“异常在哪里被创建”逆向追根因。
07、并发与异步:别在等待时让整条线停工
asyncio 适合大量 I/O 等待:数据库、HTTP、消息队列在等待响应时,事件循环可以推进别的任务。它像一个调度员管理多条装卸线;异步不是凭空增加工人,而是减少工人在闸门前干等。CPU 密集计算则应考虑进程池、原生扩展或独立计算服务。
Python 3.11 引入的 TaskGroup 提供结构化并发:任务在一个明确作用域内创建,并在离开作用域前统一等待;其中一个失败时,兄弟任务不会悄悄漂在后台。
async def get_many(
self, order_ids: list[UUID]
) -> list[OrderRead]:
async with asyncio.TaskGroup() as task_group:
tasks = [
task_group.create_task(self.get(order_id))
for order_id in order_ids
]
return [task.result() for task in tasks]
最佳实践:只在异步函数中调用非阻塞库;为外部请求设置超时;限制并发量;正确处理取消;让任务有清晰的创建者和收口点。Python 官方的asyncio 任务文档与 FastAPI 的异步说明共同解释了语言层和框架层的边界。
易错点与思考:把普通阻塞文件、网络或 CPU 计算直接放进 async def,会堵住事件循环。判断标准不是函数前面有没有 async,而是等待期间能否把控制权交还调度器。
08、高级特性:用少量机制统一横切能力
装饰器、泛型类型、描述符和上下文管理器常被叫作“高级语法”,但它们真正的用途是减少重复协议。装饰器像服务入口的统一安检门:每个业务函数不必重复写“开始日志、执行、结束日志”,但安检门必须保留原函数身份和签名。
参数 = ParamSpec("参数")
返回值 = TypeVar("返回值")
def 记录调用(
函数: Callable[参数, Awaitable[返回值]],
) -> Callable[参数, Awaitable[返回值]]:
@wraps(函数)
async def 包装器(
*args: 参数.args, **kwargs: 参数.kwargs
) -> 返回值:
日志.info("开始调用 %s", 函数.__name__)
结果 = await 函数(*args, **kwargs)
日志.info("完成调用 %s", 函数.__name__)
return 结果
return 包装器
ParamSpec 保留参数形状,TypeVar 保留返回类型,functools.wraps 保留名称和元数据。这比一个到处返回 Any 的装饰器更适合静态检查,也不会让 FastAPI 在检查签名时失去信息。进一步理解属性、方法绑定和框架模型行为,可阅读官方描述符指南。
最佳实践:高级特性只抽取稳定、重复的协议;装饰器保持透明;上下文管理器用于成对的获取与释放;元编程必须给团队带来高于理解成本的收益。
易错点与思考:炫技式抽象会把三行重复代码变成三天排查成本。判断一个高级特性是否值得:删掉它之后,业务语义会更散,还是反而更清楚?
09、FastAPI 实战:把九层能力交付成服务
FastAPI 是出口,不是全部业务。请求先由 Pydantic 模型校验,再通过依赖注入拿到 OrderService,服务调用仓储,结果由响应模型序列化;领域异常在最外层统一翻译。这个方向保持单向,测试才能替换依赖,规则也不会绑死在 Web 框架里。
订单服务 = Annotated[
OrderService,
Depends(get_order_service),
]
@app.post(
"/orders",
response_model=OrderRead,
status_code=status.HTTP_201_CREATED,
)
async def create_order(
command: OrderCreate,
service: 订单服务,
) -> OrderRead:
return await service.create(command)
输入模型用 Field 声明数量与价格边界;输出模型单独定义,避免把内部字段意外暴露。应用依赖在 lifespan 中组装,业务异常经 exception_handler 返回稳定 404。对应的权威资料是 FastAPI 的入门教程、请求体、依赖注入、异常处理以及 Pydantic 的模型文档。
测试不只验证“能返回 200”,还覆盖三类契约:合法订单创建并查询得到 201/200,非法数量由边界校验返回 422,不存在订单由领域异常映射为 404。
def test_reject_invalid_quantity() -> None:
with TestClient(app) as client:
response = client.post(
"/orders",
json={
"item": "机械键盘",
"quantity": 0,
"unit_price": "399.00",
},
)
assert response.status_code == 422
最佳实践:路由只做协议转换与调用编排;请求模型和响应模型分开;依赖显式注入;为成功、校验失败和业务失败都写测试;测试使用官方推荐的 TestClient,详见 FastAPI 的测试教程。生产环境还应把内存仓库替换为数据库仓库,并补上迁移、鉴权、可观测性、限流与部署策略。
易错点与思考:如果路由函数里同时出现 SQL、价格计算、重试、日志格式和 HTTP 响应拼装,FastAPI 再快也救不了可维护性。框架负责接待,业务服务负责做决定,仓储负责保存事实。
回看这条路径,九块知识并不是从“简单”机械排到“困难”:语法给数据命名,函数组织规则,容器安排数据,对象封装职责,模块建立边界,异常表达失败,异步管理等待,高级特性统一协议,FastAPI 最后把它们交付出去。真正的进阶,不是记住更多关键字,而是知道每个知识点应该站在业务链路的哪一层。