tenacity 默认 retry_if_exception_type 不含指数退避,需显式配置 wait_exponential;其 multiplier、min、max 控制退避节奏,单位为秒且须为浮点数。

tenacity 默认的 retry_if_exception_type 不会自动指数退避
很多人以为只要用了 tenacity 就默认带退避逻辑,其实不是。它的基础重试(比如 retry_if_exception_type)是立即重试,没有间隔。必须显式配置退避策略,否则可能触发接口限流或雪崩。
指数退避需要组合使用 wait_exponential,它接受 multiplier(基数)、min 和 max 三个关键参数:
-
multiplier=1表示第一次等待 1 秒,第二次 2 秒,第三次 4 秒……即min * 2^n,但实际是multiplier * 2^n -
min是最小等待时间(单位:秒),防止首次等待过短;max是上限,避免退避时间无限增长 - 注意:
wait_exponential的min/max单位是秒,且只接受浮点数,传整数如min=1会被当作 1.0 秒
示例:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import requests <p>@retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type(requests.exceptions.RequestException) ) def fetch_data(url): return requests.get(url, timeout=5).json() </p>
如何让重试只针对特定 HTTP 状态码(比如 429/503)
tenacity 没有内置的 “按 status_code 重试” 判断器,得自己写 retry_if_result 或 retry_if_exception。常见错误是直接在装饰器里判断响应对象——但失败时根本没返回值,只能靠异常或捕获 Response 对象本身。
推荐做法:把请求逻辑包进函数并显式抛出异常,再用 retry_if_exception 匹配:
- 不要依赖返回值做状态码判断(因为请求失败时可能连
Response都没有) - 统一用
raise抛出自定义异常(如HTTPRetryableError),比匹配response.status_code更可靠 - 如果坚持用
retry_if_result,必须确保函数总是返回Response实例(包括失败情况),不推荐
示例(安全写法):
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
<p>class HTTPRetryableError(Exception):
pass</p><p>def _safe_request(url):
resp = requests.get(url, timeout=5)
if resp.status_code in (429, 503, 504):
raise HTTPRetryableError(f"Retryable status: {resp.status_code}")
resp.raise_for_status() # 其他 4xx/5xx 视为不可重试
return resp.json()</p><p>@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=0.5, min=0.5, max=3),
retry=retry_if_exception(lambda e: isinstance(e, HTTPRetryableError))
)
def fetch_with_status_backoff(url):
return _safe_request(url)
</p>
before_sleep 回调里怎么拿到当前重试次数和下次等待时间
调试退避行为时,光看日志不够直观。before_sleep 是唯一能实时访问重试上下文的地方,但它接收的是 tenacity.RetryCallState 对象,不是原始参数。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
关键字段有:
-
retry_state.attempt_number:当前是第几次尝试(从 1 开始计数) -
retry_state.next_action.sleep:下一次将等待的秒数(float),这个值由wait策略动态计算得出 -
retry_state.outcome.exception():上一次失败的异常(可用于分类日志)
示例(打印真实退避节奏):
import logging
logging.basicConfig(level=logging.INFO)
<p>def log_retry(retry_state):
sleep = retry_state.next_action.sleep
attempt = retry_state.attempt_number
exc = retry_state.outcome.exception()
logging.info(f"Attempt {attempt} failed: {type(exc).<strong>name</strong>}. Retrying in {sleep:.2f}s")</p><p>@retry(
stop=stop_after_attempt(4),
wait=wait_exponential(multiplier=1, min=0.1, max=2),
before_sleep=log_retry
)
def flaky_function():
raise ConnectionError("Simulated failure")
</p>
并发场景下共享退避状态容易踩的坑
多个线程/协程共用同一个重试函数(比如类方法或模块级函数)时,tenacity 的退避逻辑是 per-call 的,不会跨调用共享状态——这点很安全。但容易忽略的是:全局配置(如 wait_exponential 实例)被复用时,其内部状态(如随机种子)可能被干扰。
更隐蔽的问题来自 before_sleep 回调里的共享资源(比如写同一个日志文件、更新同一个计数器),或者你在回调里手动修改了重试策略参数(比如动态改 max)——这些都不是线程安全的。
- 避免在
before_sleep中做非幂等操作(如发通知、写 DB) - 不要复用同一个
wait_exponential(...)实例给多个装饰器(应每次新建) - 异步函数(
async def)必须用tenacity.AsyncRetrying,不能直接套同步装饰器,否则事件循环会卡住
简单起见,优先用函数装饰器写法,而不是手动创建 Retrying 实例 —— 前者自动隔离上下文,后者需自行管理生命周期。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










