async def 不能直接套同步装饰器,因为同步装饰器返回的函数默认是同步的,无法透传协程特性,导致未 await 警告或 TypeError;必须用 async def 返回新协程,并用 functools.wraps 保留元信息。

asyncio 里用装饰器写重试,为什么 async def 不能直接套同步装饰器
因为同步装饰器返回的函数默认是同步的,就算你装饰的是 async def,调用时也会报 RuntimeWarning: coroutine 'xxx' was never awaited,或者直接抛 TypeError: object XXX can't be used in 'await' expression。本质是装饰器没把原协程的异步特性透传下去。
实操建议:
- 装饰器内部必须返回一个新的
async def函数,不能用def - 用
functools.wraps(func)保留原函数的__name__、__doc__等元信息,否则调试时会找不到真实函数名 - 别在装饰器里直接
await func()—— 要等被装饰函数真正被调用时才 await,否则装饰阶段就执行了
重试逻辑里 asyncio.sleep() 怎么用才不阻塞整个事件循环
asyncio.sleep() 本身是协程,必须 await,但它不会阻塞事件循环,只是让当前任务暂停,控制权交还给 event loop 去跑别的任务。常见错误是忘了 await,写成 asyncio.sleep(1)(没 await),结果 sleep 不生效,重试立刻连发。
实操建议:
- 重试间隔必须用
await asyncio.sleep(delay),不是time.sleep()(那是同步阻塞) - 延迟时间建议从短到长,比如指数退避:
delay = min(60, base_delay * (2 ** attempt)) - 如果重试前要清缓存或重置连接,确保这些操作也是 async 的;混用同步 I/O(如普通
requests.get)会导致事件循环卡住
捕获异常时,哪些错误该重试、哪些该立刻抛出
不是所有异常都适合重试。比如 ValueError 或 TypeError 是代码逻辑错,重试一百次也没用;而 ConnectionError、TimeoutError、ClientResponseError(aiohttp)这类网络抖动导致的才值得重试。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
实操建议:
- 用
except (aiohttp.ClientError, asyncio.TimeoutError) as e:这类明确的异常类型,避免裸except: - 对 HTTP 场景,注意区分状态码:400/401/403/404 属于客户端错误,一般不重试;5xx 才考虑重试
- 可以加个白名单参数:
@retry(exceptions=(asyncio.TimeoutError, aiohttp.ServerDisconnectedError))
装饰器参数怎么设计才能兼顾灵活性和可读性
硬编码重试次数和延迟很不实用。比如测试环境想快速失败(retry=1),生产环境要稳一点(retry=3, delay=1)。但参数太多又容易用错。
实操建议:
- 必选参数只留
retries(最大尝试次数),其余全设默认值:delay=1、backoff=2、exceptions=(Exception,) - 把
exceptions设为 tuple 类型,方便传多个异常:exceptions=(aiohttp.ClientError, asyncio.TimeoutError) - 避免用
**kwargs隐藏关键行为;像是否记录日志、是否 jitter(随机偏移延迟)这种,显式作为布尔参数暴露出来
重试机制最麻烦的从来不是写几行 await asyncio.sleep(),而是判断“这次到底该不该 retry”——网络超时和 JSON 解析失败看起来都是异常,但语义天差地别。漏判或误判一次,可能让下游服务被刷爆,也可能让业务请求永远卡死。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










