为什么选 Canary¶
Canary 只做一件事:把与应用同寿命的对象连接起来,并运行它们的启动与关闭。本页说明它适合 什么、不适合什么,以及与其他 Python 库的比较。
下面对比表中的每一行都是实测结果,而非凭印象:
benchmarks/comparison.py
用每个库搭建同一张小图并打印结果。可以自己运行:
测量于 2026-09-23,Python 3.13;各库版本固定在脚本头部。
适合的场景¶
- 异步服务、守护进程与后台任务,其中长寿命对象持有资源——连接池、客户端、后台任务—— 需要按依赖顺序启动与关闭。
- 启动与关闭是对象自身的一部分。 单元自带
@start/@stop钩子;能并发的部分并发启动, 启动失败时回收已获取的资源,stop()之后可以再次启动。增加阶段只需一行:Phase("migrate", after=init)。 - 用带类型的属性代替查找。 对类型检查器来说
self.db就是Database,不需要调用容器, 也不需要重复写注解。
不适合的场景¶
- 请求级对象。 Canary 在一张图里每个类型只有一个实例,没有请求作用域。每个请求的值交给
Web 框架——FastAPI 的
Depends与 Canary 配合良好,见下文——或使用带作用域的库,例如 dishka。 - 不能感知任何框架的领域类。 单元要继承
Canary。dishka、dependency-injector 与 injector 则通过普通类的__init__注入。 - 同一类型的多个实例,或构造参数。 单元一律无参构造、每个类型一个;所需的值在
@init或@start中从依赖读取。 - 同步程序。
init()、start()与stop()是协程。钩子可以是同步的,但总要有人运行事件 循环。 - 成熟度。 Canary 还年轻,只有一位维护者。其他库的社区更大,也有现成的 Web 框架集成。
对比¶
场景:Config,两个互不依赖、各需 0.1 秒获取的资源 Database 与 Cache,以及同时需要二者的
Service。
| Canary 1.1.0 | dishka 1.10.1 | dependency-injector 4.49.1 | injector 0.24.0 | FastAPI Depends 0.141.1 |
|
|---|---|---|---|---|---|
| 装配方式 | 类上的 dep() 属性 |
Provider 类 |
由 provider 组成的容器 | module 与 binder | handler 签名中的 Depends(...) |
| 你的类 | 继承 Canary |
普通类 | 普通类 | 普通类,__init__ 上加 @inject |
普通函数 |
mypy --strict 推断的类型 |
svc.db 为 Database |
get(Service) 为 Service |
service() 为 Service¹ |
get(Service) 为 Service |
取自参数注解 |
| 获取与释放 | @start / @stop 钩子 |
异步生成器工厂 | Resource provider |
— | 生成器依赖 |
| 两个互不依赖的资源 | 0.10 秒,并发 | 0.20 秒,串行 | 0.10 秒,并发 | — | 0.20 秒,每个请求 |
| 有依赖的资源逆序释放 | 是 | 是 | 是 | — | 是,每个请求 |
| 启动中途失败 | 异常到达调用方之前已释放 | close() 时释放 |
shutdown_resources() 时释放 |
— | 每个请求 |
| 关闭后再次启动 | 是 | 是 | 是 | — | — |
| 测试中替换依赖 | scope_of(root).provide(...) |
后注册的 Provider |
provider.override(...) |
binder.bind(...) |
app.dependency_overrides |
| 请求级或上下文作用域 | 无 | Scope.REQUEST |
ContextLocalSingleton |
threadlocal |
默认按请求 |
| 安装的第三方包 | 0 | 0 | 0 | 0 | 9 |
¹ 依赖链中有异步资源时,service() 在运行时返回一个可等待对象,而静态类型仍是 Service;
并且在 --strict 下,await container.init_resources() 会被报错,因为它的类型是
Awaitable[None] | None。
这张表说明了什么、没说明什么:
- 耗时来自休眠 0.1 秒的资源。它只说明互不依赖的资源是否并发获取,不代表各库本身的快慢。
- 启动中途失败。 所有带生命周期的库最终都会释放已获取的资源,区别在于由谁触发:Canary
在
start()内部回滚,只看到异常的调用方无需再清理;dishka 与 dependency-injector 由调用方 调用close()/shutdown_resources()释放,通常写在finally里。 - injector 没有生命周期 API,生命周期相关的行不适用。
- FastAPI
Depends是请求级的:每个请求都会获取并释放两个资源。与应用同寿命的对象放在 应用的lifespan中,需要自己编写。 - 零依赖并非 Canary 独有。 dishka、dependency-injector 与 injector 同样不安装任何其他包。
Canary 与 FastAPI 配合¶
二者分工不同,可以直接配合:Canary 管理与应用同寿命的对象,Depends 把每个请求的值交给
handler。
service = LibraryService()
@asynccontextmanager
async def lifespan(_app: FastAPI):
async with service: # 整张图启动,关闭时回收
yield
app = FastAPI(lifespan=lifespan)
def books() -> BookRepository:
return service.books # 与应用同寿命的单元
@app.get("/books/{book_id}")
async def get_book(book_id: int, repo: Annotated[BookRepository, Depends(books)]):
...
完整版本(含测试)见 Canary-Framework-Example
的 library 项目,它正是以这种方式构建的 FastAPI 服务。