异步装饰器必须确保返回协程对象,否则await报错;需用inspect.iscoroutinefunction判断原函数类型并分流处理,配合functools.wraps保留签名,避免task泄漏。

async装饰器必须返回协程对象,否则await会报错
直接在普通装饰器里套用 async def 并不等于“支持异步”——关键看被装饰函数调用后返回的是不是协程对象。如果装饰器内部没做适配,await decorated_func() 会抛 TypeError: object XXX can't be used in 'await' expression。
常见错误是只给装饰器本身加 async,却忘了让它的返回值也变成协程:
- ❌ 错误:装饰器函数用
async def wrapper(...),但最后return func(...)(func 是同步函数)→ 返回普通值 - ✅ 正确:若原函数是
async def,wrapper 内必须return await func(...);若要兼容同步/异步函数,得先判断类型再分流处理 - 性能影响:每次调用都需用
inspect.iscoroutinefunction()判断,有轻微开销;高频场景建议提前缓存判断结果
用functools.wraps保留原函数签名,否则type hints和IDE提示全丢
异步装饰器如果不处理元信息,help()、类型检查工具(如 mypy)、甚至 VS Code 的参数提示都会显示 wrapper 的签名,而不是原始函数的。这不是报错问题,但会让协作和调试变困难。
实操上必须两步走:
- 用
@functools.wraps(func)包裹 wrapper 函数 - wrapper 本身也要声明为
async def wrapper(*args, **kwargs),且返回值与原函数一致(协程 or 普通值) - 注意:如果原函数带类型注解(比如
def f() -> str:),@wraps能透传,但装饰器中间层若修改了返回类型(比如加了日志返回None),就得手动补typing.overload或用typing.cast
同步函数被async装饰器包裹后,不能直接await
这是最常踩的坑:以为“加了 async 装饰器 = 自动变协程”,结果运行时卡死或报错。本质是 Python 不允许 await 普通函数返回值。
正确做法是让装饰器具备双模式识别能力:
- 检测
func是否为协程函数:inspect.iscoroutinefunction(func) - 是 → 在 wrapper 中
return await func(*args, **kwargs) - 否 → 在 wrapper 中
return func(*args, **kwargs)(保持同步调用) - 别偷懒写成
return await asyncio.to_thread(func, ...)——这会把同步函数强行扔进线程池,有额外调度成本,且掩盖了设计意图
装饰器里用asyncio.create_task容易引发未等待的task泄漏
有些场景需要在装饰器中“顺手”启动一个后台任务(比如打日志、上报指标),这时如果直接 asyncio.create_task(...) 但不 await、也不加到某个生命周期管理里,task 可能被 GC 掉或静默失败,尤其在短生命周期的 HTTP 请求中。
稳妥做法只有两种:
- 明确需要等待 → 用
await asyncio.create_task(...) - 明确不等待 → 把 task 存到全局 weakset 或 request context 中,在合适时机(如响应结束)统一
await或取消 - 绝对不要裸写
asyncio.create_task(...)后就不管——Python 3.11+ 会警告RuntimeWarning: coroutine ... was never awaited,但低版本可能直接丢弃
复杂点在于:装饰器本身无上下文感知能力,所以“合适时机”往往得靠外层框架配合(比如 FastAPI 的 BackgroundTasks),纯装饰器很难闭环。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











