单例与依赖作用域
单例模式的本意是“全局唯一实例”。Python 里模块就是单例,但单例的真正考点在作用域:实例活多久、谁共享。FastAPI 的依赖系统把这个问题具体化了。
模块单例
# config.py 模块级对象
settings = load_settings()import config 多次执行,模块只加载一次(sys.modules 缓存),config.settings 全局唯一。这就是 Python 的单例:不需要类,不需要 getInstance。
传统 Java 式单例类(__new__ 控制实例化)在 Python 里是反模式:全局状态难测试、并发初始化有竞态、违背显式优于隐式。
lru_cache:进程级单例
需要“惰性初始化 + 全局复用”时,@lru_cache 是惯用方案:
from functools import lru_cache
@lru_cache
def get_db_pool():
return create_pool() # 首次调用创建, 之后复用- 缓存的是返回值,天然单例
- 无参数或参数可哈希时才有效;需要按参数区分实例(如多租户)时传参数即可
- 测试隔离:
get_db_pool.cache_clear()重置
FastAPI 依赖的作用域
FastAPI 的 Depends 默认每次请求重新执行依赖函数(请求级,同一请求内多次引用复用一次),这常被误解:
依赖默认请求级, lru_cache 提升为进程级
三种作用域的写法:
| 作用域 | 写法 | 生命周期 |
|---|---|---|
| 请求级(默认) | 普通依赖函数 | 每次请求新建 |
| 应用级(单例) | 依赖函数加 @lru_cache |
进程内唯一 |
| 请求级带清理 | yield 依赖 |
请求结束执行清理(关会话) |
@lru_cache
def get_settings(): # 应用级: 全局读一次配置
return Settings()
def get_db(): # 请求级: 每次请求一个会话
db = SessionLocal()
try:
yield db
finally:
db.close()与 Spring 的对比
| 维度 | FastAPI | Spring |
|---|---|---|
| 单例 | lru_cache / 模块对象 | 默认 singleton |
| 请求级 | 依赖默认行为 | request 作用域 |
| 会话级 | 手动实现(request 上下文) | session 作用域 |
| 作用域声明 | 无专门语法,靠缓存装饰器 | @Scope 注解 |
面试结论:FastAPI 没有内置作用域语法,靠 Python 特性拼装;Spring 有完整作用域体系。这是框架设计取向差异。
面试追问
- Python 的单例怎么写? 模块级对象或 lru_cache。类加 new 是 Java 思维,不惯用
- FastAPI 依赖每次请求都执行吗? 默认是。要复用实例就 lru_cache 包依赖函数
- yield 依赖是什么? 请求结束执行 finally 清理资源(数据库会话、连接)。FastAPI 的资源管理惯用法
- 单例有什么问题? 全局状态难测试、并发初始化竞态。需要时可注入替换,别到处直接引用
- 怎么在测试里重置单例? lru_cache 用 cache_clear(),模块对象用 monkeypatch 替换