Skip to content

操作指南:lifespan 与 app.state

使用 FastAPI 的 lifespan 上下文管理器和 app.state 管理资源的模式。


1. 基本 lifespan 模式

在应用启动时初始化、在关闭时清理的资源(数据库连接、缓存、外部客户端),使用 lifespan 管理。

python
from contextlib import asynccontextmanager
from typing import AsyncIterator
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    # 启动:初始化资源
    app.state.db = await create_db_connection()
    yield
    # 关闭:清理资源
    await app.state.db.close()

app = FastAPI(lifespan=lifespan)

2. 对 app.state 进行类型安全访问

app.state 是一个 starlette.datastructures.State 对象,可以动态添加任意属性。由于没有类型注解,访问时类型检查不适用。

推荐:定义带类型的访问器函数

python
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession

def get_db(request: Request) -> AsyncSession:
    db: AsyncSession = request.app.state.db  # type: ignore[attr-defined]  # reason: always set by lifespan
    return db

或定义带类型的包装器:

python
from dataclasses import dataclass

@dataclass
class AppState:
    db: AsyncSession
    cache: RedisClient

def get_app_state(request: Request) -> AppState:
    return AppState(
        db=request.app.state.db,
        cache=request.app.state.cache,
    )

3. TestClient 与 lifespan

如果以普通方式使用 TestClient(不使用上下文管理器),lifespan 不会运行。

python
# ❌ lifespan 不会运行
client = TestClient(app)
r = client.get("/")  # AttributeError,因为 app.state.db 未设置

# ✅ 使用 with 块运行 lifespan
with TestClient(app) as client:
    r = client.get("/")  # lifespan 正常启动和关闭

注意,如果另一个测试已经运行了 with TestClient(app)app.state 可能持续存在,看起来没有 with 也能工作。这是依赖测试顺序的 bug,请始终使用 with 块。

pytest fixture 模式

python
from collections.abc import Generator

import pytest
from fastapi.testclient import TestClient

@pytest.fixture
def client() -> Generator[TestClient, None, None]:
    with TestClient(app) as c:
        yield c

返回类型应为 Generator[TestClient, None, None](单独的 TestClient 会导致类型错误)。


4. 通过 app.state 管理 TtlCache

nene2.cache.TtlCache 存储在 app.state 上,避免测试中的全局变量。

python
from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse

from nene2.cache import TtlCache

@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    app.state.cache = TtlCache[dict[str, object]](ttl_seconds=60.0)
    yield
    # TtlCache 无需清理(仅内存)

app = FastAPI(lifespan=lifespan)


def get_cache(request: Request) -> TtlCache[dict[str, object]]:
    cache: TtlCache[dict[str, object]] = request.app.state.cache  # type: ignore[attr-defined]  # reason: always set by lifespan
    return cache


@app.get("/items/{item_id}")
def get_item(
    item_id: int,
    cache: TtlCache[dict[str, object]] = Depends(get_cache),
) -> JSONResponse:
    key = f"item:{item_id}"
    if (cached := cache.get(key)) is not None:
        return JSONResponse({"source": "cache", **cached})
    result = {"item_id": item_id, "name": f"Item {item_id}"}
    cache.set(key, result)
    return JSONResponse({"source": "fresh", **result})

全局变量 vs. app.state

方式优点缺点
全局变量简单测试间共享状态
app.state每个测试可通过独立 TestClient 重置需要 type: ignore

5. app.state 上设置的值在 lifespan 之外消失

lifespan 内设置 app.state 的值。旧的 startup 事件 API 在 FastAPI 0.93+ 中已废弃。

python
# ❌ 旧 API(已废弃)
@app.on_event("startup")
async def startup() -> None:
    app.state.db = await create_db_connection()

# ✅ 使用 lifespan
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    app.state.db = await create_db_connection()
    yield
    await app.state.db.close()

根据 MIT 许可证发布。