Python使用Tenacity一行代碼實現(xiàn)自動重試詳解
在與AI大模型API服務(wù)交互時,我們總會面對一個無法回避的現(xiàn)實:網(wǎng)絡(luò)并不總是可靠。代理可能中斷,API會限制請求頻率,連接可能超時,甚至網(wǎng)絡(luò)會短暫中斷。幸運的是,這些問題通常是暫時的。如果第一次請求失敗,稍等片刻再試一次,往往就能成功。
這種“再試一次”的策略,就是重試。它不是什么高深的技術(shù),卻是構(gòu)建可靠、健壯應(yīng)用程序的關(guān)鍵一環(huán)。
一切始于一個簡單的 API 調(diào)用
讓我們從一個真實場景開始:調(diào)用 AI 模型的 API 來完成字幕翻譯。一段基礎(chǔ)的代碼可能長這樣:
# 一個基本的 API 調(diào)用函數(shù)
from openai import OpenAI, APIConnectionError
def translate_text(text: str) -> str:
message = [
{'role': 'system', 'content': '您是一名頂級的字幕翻譯引擎。'},
{'role': 'user', 'content': f'<INPUT>{text}</INPUT>'},
]
model = OpenAI(api_key="YOUR_API_KEY", base_url="...")
try:
response = model.chat.completions.create(model="gpt-4o", messages=message)
if response.choices:
return response.choices[0].message.content.strip()
raise RuntimeError("API未返回有效結(jié)果")
except APIConnectionError as e:
print(f"網(wǎng)絡(luò)連接失敗: {e}。需要重試...")
raise # 程序在這里崩潰
except Exception as e:
print(f"發(fā)生其他錯誤: {e}")
raise
這段代碼能工作,但它很“脆弱”。一旦遇到網(wǎng)絡(luò)問題,它只會打印一條消息然后崩潰。我們當(dāng)然可以手動寫一個 for 循環(huán)和 time.sleep 來實現(xiàn)重試:
# 手動實現(xiàn)重試
for attempt in range(3):
try:
# ... API 調(diào)用邏輯 ...
return response.choices[0].message.content.strip()
except APIConnectionError as e:
print(f"第 {attempt + 1} 次嘗試失敗: {e}")
if attempt == 2: # 檢查是否是最后一次嘗試
raise
# ... 對其他異常也要重復(fù)寫相似的邏輯 ...
這種方式很快就會讓代碼變得復(fù)雜和混亂。重試邏輯和業(yè)務(wù)邏輯混雜在一起,而且如果我們需要在多個地方重試,就不得不編寫大量重復(fù)、易錯的代碼。
這時,tenacity 庫就派上用場了。
Tenacity 入門:一行代碼實現(xiàn)優(yōu)雅重試
tenacity 是一個專為 Python 設(shè)計的通用重試庫。它的核心理念就是用簡單、清晰的方式,為任何可能失敗的操作添加重試能力。
安裝:pip install tenacity
我們可以用 @retry 裝飾器輕松改造上面的函數(shù):
from tenacity import retry
@retry
def translate_text(text: str) -> str:
# ... 內(nèi)部邏輯和之前完全一樣,無需任何改動 ...
僅僅加了一行 @retry,這個函數(shù)就煥然一新?,F(xiàn)在,如果 translate_text 函數(shù)內(nèi)部拋出任何異常,tenacity 都會自動捕獲它,并立即重新調(diào)用該函數(shù)。它會一直重試,永不停止,直到函數(shù)成功返回一個值。
精細(xì)控制:讓重試按我們的意愿行事
“永遠重試”通常不是我們想要的。我們需要設(shè)定一些邊界。tenacity 提供了豐富的參數(shù)來實現(xiàn)精細(xì)的控制。
1. 設(shè)置停止條件 (stop)
我們不希望無限次地重試。最常見的需求是“最多嘗試 N 次”,這可以通過 stop_after_attempt 實現(xiàn)。
from tenacity import retry, stop_after_attempt
# 總共嘗試 3 次
@retry(stop=stop_after_attempt(3))
def translate_text(text: str) -> str:
# ...
注意:一個重要的認(rèn)知細(xì)節(jié) stop_after_attempt(N) 指的是總共的嘗試次數(shù),而不是“重試次數(shù)”。
stop_after_attempt(1)意味著:執(zhí)行 1 次,如果失敗,立即停止。它根本不會重試。stop_after_attempt(3)意味著:總共執(zhí)行 3 次,即首次嘗試 + 2 次重試。
記住這個簡單的規(guī)則:如果你希望在失敗后能額外重試 Y 次,那么你應(yīng)該設(shè)置 stop_after_attempt(Y + 1)。
我們也可以按時間來限制,比如 stop_after_delay(10) 表示“10秒后停止”。更棒的是,你可以用 | (或) 操作符將它們組合起來,哪個條件先滿足就停止。
from tenacity import stop_after_delay
# 總次數(shù)達到 5 次或總耗時超過 30 秒,就停止
@retry(stop=(stop_after_attempt(5) | stop_after_delay(30)))
def translate_text(text: str) -> str:
# ...
2. 設(shè)置等待策略 (wait)
連續(xù)不斷地快速重試可能會壓垮服務(wù)器或達到頻率限制。在兩次重試之間加入等待是明智之舉。最簡單的是固定等待,使用 wait_fixed:
from tenacity import retry, wait_fixed
# 每次重試前都等待 2 秒
@retry(wait=wait_fixed(2))
def translate_text(text: str) -> str:
# ...
在與網(wǎng)絡(luò)服務(wù)交互時,更推薦指數(shù)退避 (wait_exponential)。它會隨著重試次數(shù)的增加,逐漸拉長等待時間(比如 2s, 4s, 8s...),能有效避免在服務(wù)高峰期造成“重試風(fēng)暴”。
from tenacity import wait_exponential
# 首次重試等 2^1=2s, 之后等 4s, 8s... 最多等到 10s
@retry(wait=wait_exponential(multiplier=1, min=2, max=10))
def translate_text(text: str) -> str:
# ...
3. 決定何時重試 (retry)
默認(rèn)情況下,tenacity 會在遇到任何異常時都進行重試。但這并不總是對的。
比如,APIConnectionError (網(wǎng)絡(luò)問題) 或 RateLimitError (請求太頻繁) 是典型的可恢復(fù)錯誤,重試很有可能會成功。但 AuthenticationError (密鑰錯誤) 或 PermissionDeniedError (無權(quán)限) 則是致命錯誤,重試多少次都注定失敗。
我們可以通過 retry_if_not_exception_type 來告訴 tenacity 遇到某些致命錯誤時不要重試。
注意:一個常見的語法陷阱 當(dāng)指定多個異常類型時,你可能會直覺地寫成 AuthenticationError | PermissionDeniedError。
# 錯誤的方式!這無法按預(yù)期工作 @retry(retry=retry_if_not_exception_type(AuthenticationError | PermissionDeniedError))
在現(xiàn)代 Python 中,A | B 創(chuàng)建的是一個 UnionType 對象,而 tenacity 的這個函數(shù)期望接收一個包含異常類型的元組 (tuple)。
正確的寫法是:
from openai import AuthenticationError, PermissionDeniedError # 正確的方式!使用元組 @retry(retry=retry_if_not_exception_type((AuthenticationError, PermissionDeniedError)))
這個小小的括號,至關(guān)重要。
當(dāng)重試最終失敗時
如果 tenacity 在用盡所有嘗試后依然失敗,它會怎么做?默認(rèn)情況下,它會拋出一個 RetryError,其中包含了最后一次失敗時的原始異常。
但有時我們不希望程序崩潰,而是想執(zhí)行一些自定義的收尾工作,比如記錄一條詳細(xì)的錯誤日志,并返回一個友好的錯誤提示。這就是 retry_error_callback 的用武之地。
from tenacity import RetryCallState
def my_error_callback(retry_state: RetryCallState):
# retry_state 對象包含了這次重試的所有信息
print(f"所有 {retry_state.attempt_number} 次嘗試均失敗!")
return "默認(rèn)的翻譯結(jié)果或錯誤提示"
@retry(stop=stop_after_attempt(3), retry_error_callback=my_error_callback)
def translate_text(text: str) -> str:
# ...
現(xiàn)在,如果函數(shù)連續(xù)失敗 3 次,它不會拋出異常,而是會返回 my_error_callback 函數(shù)的返回值。
注意:回調(diào)函數(shù)里的一個微妙陷阱 在回調(diào)函數(shù)中,我們?nèi)绾伟踩孬@取最后一次的異常信息?
def return_last_value(retry_state: RetryCallState):
# 危險!這會重新拋出異常!
return "失?。? + retry_state.outcome.result()
retry_state.outcome 代表了最后一次嘗試的結(jié)果。如果那次嘗試是失敗的,調(diào)用 .result() 方法會重新拋出那個異常,導(dǎo)致我們的回調(diào)函數(shù)自身崩潰。
正確的做法是使用 .exception() 方法,它會安全地返回異常對象,而不會拋出它:
def return_last_value(retry_state: RetryCallState):
# 安全!這只會返回異常對象
last_exception = retry_state.outcome.exception()
return f"經(jīng)過 {retry_state.attempt_number} 次嘗試后失敗。最后一次錯誤是: {last_exception}"
當(dāng) Tenacity 遇到面向?qū)ο?/h2>
隨著代碼庫的增長,我們通常會把邏輯封裝在類里。這時,我們會遇到兩個更深層次的問題:作用域和繼承。
1. 回調(diào)函數(shù)如何訪問 self
假設(shè)我們的回調(diào)函數(shù)需要訪問類的實例變量(比如 self.name)。我們可能會很自然地這樣寫:
class TTS:
def __init__(self, name):
self.name = name
def _my_callback(self, retry_state):
print(f"實例 {self.name} 的任務(wù)失敗了。")
# ...
# 這會失?。ameError: name 'self' is not defined
@retry(retry_error_callback=self._my_callback)
def run(self):
# ...
這會立即報錯,因為 @retry 裝飾器是在定義類的時候執(zhí)行的,那時還沒有任何類的實例,自然也就沒有 self。
最優(yōu)雅的解決方案是**“內(nèi)部函數(shù)閉包”**模式。我們將裝飾器應(yīng)用在一個定義于實例方法內(nèi)部的函數(shù)上:
class TTS:
def __init__(self, name):
self.name = name
def run(self):
# 在這里,self 是可用的!
@retry(
# 因為裝飾器在 run 方法內(nèi)部,它可以“捕獲”到 self
retry_error_callback=self._my_callback
)
def _execute_task():
# 這里是真正需要重試的邏輯
print(f"正在為 {self.name} 執(zhí)行任務(wù)...")
raise ValueError("任務(wù)失敗")
# 調(diào)用被裝飾的內(nèi)部函數(shù)
return _execute_task()
def _my_callback(self, retry_state: RetryCallState):
# ...
這是一種非常強大且 Pythonic 的模式,完美解決了作用域問題。
2. 如何在父類中定義重試策略,讓所有子類繼承
這是我們討論的最后一個,也是最體現(xiàn)設(shè)計思想的問題。假設(shè)我們有一個 BaseProvider 父類,和多個 MyProviderA, MyProviderB 子類。我們希望所有子類都遵循統(tǒng)一的重試規(guī)則。
一個常見的錯誤想法是在父類的空方法上應(yīng)用裝飾器。當(dāng)子類重寫該方法時,父類上的裝飾器也隨之丟失。
正確的解決方案是模板方法設(shè)計模式 (Template Method Pattern)。
- 父類定義一個模板方法 (
_exec),它包含了不可變的算法框架(即我們的重試邏輯)。 - 這個模板方法會調(diào)用一個抽象的鉤子方法 (
_do_work)。 - 子類只需要實現(xiàn)這個鉤子方法,填充具體的業(yè)務(wù)邏輯即可。
讓我們用一個更完整的例子來構(gòu)建這個模式:
from openai import OpenAI, AuthenticationError, PermissionDeniedError
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type, RetryCallState
# 1. 定義一個通用的、可復(fù)用的異常處理類
class RetryRaise:
# 定義不應(yīng)重試的致命異常
NO_RETRY_EXCEPT = (AuthenticationError, PermissionDeniedError)
@classmethod
def _raise(cls, retry_state: RetryCallState):
ex = retry_state.outcome.exception()
if ex:
# 根據(jù)不同異常類型,進行日志記錄并拋出自定義的、更友好的 RuntimeError
# ... 此處可以添加更復(fù)雜的異常分類邏輯 ...
raise RuntimeError(f"重試 {retry_state.attempt_number} 次后最終失敗: {ex}") from ex
raise RuntimeError(f"重試 {retry_state.attempt_number} 次后失敗,但未捕獲到異常。")
# 2. 實現(xiàn)模板父類
class BaseProvider:
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(2),
retry=retry_if_not_exception_type(RetryRaise.NO_RETRY_EXCEPT),
retry_error_callback=RetryRaise._raise
)
def _exec(self) -> str:
"""這是模板方法,負(fù)責(zé)重試。子類不應(yīng)重寫它。"""
# 調(diào)用鉤子方法,由子類實現(xiàn)
return self._do_work()
def _do_work(self) -> str:
"""這是鉤子方法,子類必須實現(xiàn)它。"""
raise NotImplementedError("子類必須實現(xiàn) _do_work 方法")
# 3. 實現(xiàn)具體的子類
class DeepSeekProvider(BaseProvider):
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
self.model = OpenAI(api_key=self.api_key, base_url=self.base_url)
def _do_work(self) -> str:
"""這里只關(guān)心核心業(yè)務(wù)邏輯,完全不用考慮重試。"""
response = self.model.chat.completions.create(
model="deepseek-chat",
messages=[{'role': 'user', 'content': '你是誰?'}]
)
if response.choices:
return response.choices[0].message.content.strip()
raise RuntimeError(f"API未返回有效結(jié)果: {response}")
# --- 如何使用 ---
provider = DeepSeekProvider(api_key="...", base_url="...")
try:
# 我們調(diào)用的是 _exec,它包含了重試邏輯
result = provider._exec()
print("執(zhí)行成功:", result)
except RuntimeError as e:
# 如果最終失敗,會捕獲到 RetryRaise 拋出的友好異常
print("執(zhí)行失敗:", e)
通過這種方式,我們將重試策略(不變的部分)和業(yè)務(wù)邏輯(可變的部分)完美地分離開來,構(gòu)建了一個既健壯又易于擴展的框架。
tenacity 是一個看似簡單,實則功能強大的庫。它不僅能輕松應(yīng)對簡單的重試場景,更能通過巧妙的設(shè)計模式,解決復(fù)雜的、面向?qū)ο蟮膽?yīng)用程序中的可靠性問題。
到此這篇關(guān)于Python使用Tenacity一行代碼實現(xiàn)自動重試詳解的文章就介紹到這了,更多相關(guān)Python Tenacity自動重試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python分析和處理excel文件數(shù)據(jù)的詳細(xì)步驟
Python 提供了多種工具來分析和處理 Excel 文件數(shù)據(jù),最常用的庫包括 pandas、openpyxl 和 xlrd,本文將詳細(xì)介紹如何使用這些工具讀取、處理和分析 Excel 文件中的數(shù)據(jù),需要的朋友可以參考下2025-05-05
python的Crypto模塊實現(xiàn)AES加密實例代碼
這篇文章主要介紹了python的Crypto模塊實現(xiàn)AES加密實例代碼,簡單介紹了實現(xiàn)步驟,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-01-01
pandas DataFrame where的實現(xiàn)示例
pandas.DataFrame.where方法提供了一種靈活的方式來根據(jù)條件篩選和替換DataFrame中的元素,本文主要介紹了pandas DataFrame where的實現(xiàn)示例,感興趣的可以了解一下2025-04-04
對python 中re.sub,replace(),strip()的區(qū)別詳解
今天小編就為大家分享一篇對python 中re.sub,replace(),strip()的區(qū)別詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07

