函数、模块与依赖注入
本节目标
学完本节,你应能用小函数分离解析、业务计算与副作用,把代码组织成可导入模块,并通过显式参数注入时钟、网络或存储依赖,而不是在核心逻辑中隐藏全局调用。
函数是最小契约
一个工程函数至少应让读者看出:
- 输入是什么,是否已校验;
- 返回什么;
- 会抛哪些业务相关异常;
- 是否读文件、联网、写状态或读取环境变量;
- 重复调用是否会产生额外副作用。
def unfinished_ids(tasks: list[Task]) -> list[str]:
return [task.task_id for task in tasks if task.status != "done"]这是纯函数:结果只由输入决定,没有隐藏 I/O。纯函数不是道德要求,而是更易测试、缓存、并发与复用。
“函数式核心,命令式外壳”
可以把程序分成:
命令式外壳:读文件 / 调 API / 读时钟 / 写日志
↓ 转成已校验对象
函数式核心:计算、决策、排序、汇总
↓ 返回明确结果或错误
命令式外壳:写文件 / 调工具 / 输出不要让一个函数同时读取文件、解析 JSON、调用模型、修改数据库并打印结果。失败时难以定位,测试也必须搭建所有依赖。
位置参数、关键字参数与默认值
def search(
query: str,
*,
limit: int = 5,
include_archived: bool = False,
) -> list[Result]:
...* 让后续参数只能按名称传递:search("RAG", limit=10) 比 search("RAG", 10, False) 更清楚。默认值应是不可变值;不要把可变列表或字典作为默认参数。
模块与入口
模块是可导入的 .py 文件。可测试模块不应在导入时执行真实工作:
def main() -> int:
# 解析 CLI,调用应用函数,返回退出码
return 0
if __name__ == "__main__":
raise SystemExit(main())测试可以导入函数而不触发命令行、网络或文件写入。导入时读取密钥、创建客户端或启动后台任务,会让测试和工具发现过程变得不可预测。
显式依赖注入
**依赖注入(dependency injection)**只是把外部能力作为参数提供,而不是在函数内部硬编码创建。最小形式可以是可调用对象:
from collections.abc import Callable
def with_retry(
operation: Callable[[], str],
*,
attempts: int,
sleep: Callable[[float], None],
) -> str:
...生产代码注入真实 time.sleep 与 API 调用;测试注入会记录次数的假函数和不等待的 sleep。这比修改全局对象或在测试中真的联网更稳定。
依赖较复杂时,可用 Protocol 描述需要的方法:
from typing import Protocol
class NoteStore(Protocol):
def search(self, query: str, limit: int) -> list[str]: ...
def answer(store: NoteStore, query: str) -> list[str]:
return store.search(query, limit=5)调用方只依赖最小能力,不必知道具体数据库或 SDK 类。
错误返回还是异常
- 对“没有匹配结果”等正常分支,返回空集合或明确结果类型;
- 对输入不满足契约、依赖不可用等不能按正常路径继续的情况,抛出具体异常;
- 不要同时返回
None、打印错误并抛异常;调用方会不知道哪个才是接口。
公共函数应记录异常语义,但不要把供应商原始异常、密钥或完整请求直接暴露给模型或用户。
模块边界建议
小项目可以从以下分工开始:
app/
├── models.py # 已校验对象与结果类型
├── parsing.py # 外部数据 → 内部对象
├── service.py # 纯业务用例
├── adapters.py # 文件/API/数据库边界
└── cli.py # 参数、输出和退出码
tests/不是每个项目都需要这些文件。拆分依据是变化原因与副作用边界,不是追求目录数量。
常见错误
- 在模块顶层读取环境变量并创建真实客户端;
- 为“可测试”引入庞大抽象层,却没有第二种实现或明确边界;
- 捕获所有异常后返回空列表,让依赖故障伪装成“无结果”;
- 函数通过全局变量共享可变状态;
main()不返回退出码,CLI 失败仍显示成功。
练习
把“读取任务 JSON → 校验 → 统计 → 写报告”拆成至少四个函数。标出纯函数、I/O 函数、异常边界与重复执行的副作用。再把“当前时间”作为参数或可调用依赖注入,证明测试不依赖真实时钟。
自测
- 纯函数为什么更易测试?
- 导入模块时为什么不应执行真实工作?
- 依赖注入一定需要框架或容器吗?
Protocol应描述整个供应商 SDK 还是最小能力?- 空结果与依赖失败为什么不能都返回
[]?
相关概念与下一步
- 工具契约与权限进入 Tool Calling;这里仅学习 Python 接口可测试性。
- 下一节 文件、JSON 与输入校验 实现不可信数据到内部对象的边界。
参考资料
获取日期:2026-07-14。