装饰器函数本身不必是协程,但其返回的包装器必须为async def才能统一处理同步和异步函数;需用inspect.iscoroutinefunction判断后分别调用或await,否则会引发运行时错误。

async def 装饰器函数本身必须是协程吗?
不是。装饰器函数(即你写的 @my_decorator 外层函数)本身不需要是 async def,但它的返回值——也就是实际替换原函数的那个“包装器”——必须能正确处理同步和异步两种被装饰函数。混淆这点会导致 RuntimeWarning: coroutine 'xxx' was never awaited 或直接抛出 TypeError: object X can't be used in 'await' expression。
- 如果只装饰普通函数,包装器用
def即可; - 如果要同时支持
async def和def,包装器必须是协程(async def),并在内部判断被装饰对象是否为协程对象(用inspect.iscoroutinefunction); - 别试图在同步包装器里
await一个协程——它根本不会被调度执行。
如何让一个装饰器同时兼容 sync 和 async 函数?
核心是运行时检查:拿到被装饰函数后,先用 inspect.iscoroutinefunction 判断类型,再决定同步调用还是 await。不能硬编码 await,否则同步函数会崩。
import inspect
import functools
def log_execution(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
if inspect.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
注意:functools.wraps 必须套在 async def wrapper 上,否则 func.__name__ 等元信息丢失;且 wrapper 必须声明为 async,否则无法 await 协程——哪怕你其实不 await 它,类型系统也要求上下文支持协程语义。
为什么用 asyncio.create_task 或 asyncio.to_thread 会改变行为?
这不是装饰器本身的逻辑问题,而是你主动引入并发语义。比如在装饰器里对同步函数用 asyncio.to_thread 包装,就把它变成了异步执行;对异步函数用 create_task 会使其脱离当前 await 链,变成“火种式”并发。
立即学习“Python免费学习笔记(深入)”;
-
to_thread适合 CPU 不敏感但阻塞的同步操作(如time.sleep、文件读写); -
create_task适合想“发出去就不管”的异步任务,但要注意错误不会冒泡到调用方; - 多数场景下,装饰器只做拦截和增强,不改执行模型——强行并发反而破坏调用者对顺序和异常的预期。
装饰器返回值类型不一致怎么办?
Python 类型提示没法自动推导 async def 和 def 混合后的返回类型,mypy 默认报错 Returning Any from function declared to return "X"。解决办法是用 Union[Awaitable[T], T],但更务实的做法是:明确文档说明该装饰器返回值与原函数一致,并在类型注解中用 Any 或忽略(尤其内部工具函数)。
真正容易翻车的是测试:你得分别写 test_sync_func 和 test_async_func,且后者必须用 async def test_xxx + await 调用,漏掉 await 就只返回协程对象,断言永远通过不了。
最常被忽略的一点:装饰器里的日志、计时、上下文管理等副作用代码,如果用了异步 I/O(比如写入网络日志),就必须确保整个链路都是异步的——否则你会在同步函数里卡住事件循环,或在异步函数里触发 RuntimeError: no running event loop。


















