Python使用Tenacity一行代碼實(shí)現(xiàn)自動重試詳解
在與AI大模型API服務(wù)交互時(shí),我們總會面對一個(gè)無法回避的現(xiàn)實(shí):網(wǎng)絡(luò)并不總是可靠。代理可能中斷,API會限制請求頻率,連接可能超時(shí),甚至網(wǎng)絡(luò)會短暫中斷。幸運(yùn)的是,這些問題通常是暫時(shí)的。如果第一次請求失敗,稍等片刻再試一次,往往就能成功。
這種“再試一次”的策略,就是重試。它不是什么高深的技術(shù),卻是構(gòu)建可靠、健壯應(yīng)用程序的關(guān)鍵一環(huán)。
一切始于一個(gè)簡單的 API 調(diào)用
讓我們從一個(gè)真實(shí)場景開始:調(diào)用 AI 模型的 API 來完成字幕翻譯。一段基礎(chǔ)的代碼可能長這樣:
# 一個(gè)基本的 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ā)生其他錯(cuò)誤: {e}")
raise
這段代碼能工作,但它很“脆弱”。一旦遇到網(wǎng)絡(luò)問題,它只會打印一條消息然后崩潰。我們當(dāng)然可以手動寫一個(gè) for 循環(huán)和 time.sleep 來實(shí)現(xiàn)重試:
# 手動實(shí)現(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ù)邏輯混雜在一起,而且如果我們需要在多個(gè)地方重試,就不得不編寫大量重復(fù)、易錯(cuò)的代碼。
這時(shí),tenacity 庫就派上用場了。
Tenacity 入門:一行代碼實(shí)現(xiàn)優(yōu)雅重試
tenacity 是一個(gè)專為 Python 設(shè)計(jì)的通用重試庫。它的核心理念就是用簡單、清晰的方式,為任何可能失敗的操作添加重試能力。
安裝:pip install tenacity
我們可以用 @retry 裝飾器輕松改造上面的函數(shù):
from tenacity import retry
@retry
def translate_text(text: str) -> str:
# ... 內(nèi)部邏輯和之前完全一樣,無需任何改動 ...
僅僅加了一行 @retry,這個(gè)函數(shù)就煥然一新。現(xiàn)在,如果 translate_text 函數(shù)內(nèi)部拋出任何異常,tenacity 都會自動捕獲它,并立即重新調(diào)用該函數(shù)。它會一直重試,永不停止,直到函數(shù)成功返回一個(gè)值。
精細(xì)控制:讓重試按我們的意愿行事
“永遠(yuǎn)重試”通常不是我們想要的。我們需要設(shè)定一些邊界。tenacity 提供了豐富的參數(shù)來實(shí)現(xiàn)精細(xì)的控制。
1. 設(shè)置停止條件 (stop)
我們不希望無限次地重試。最常見的需求是“最多嘗試 N 次”,這可以通過 stop_after_attempt 實(shí)現(xiàn)。
from tenacity import retry, stop_after_attempt
# 總共嘗試 3 次
@retry(stop=stop_after_attempt(3))
def translate_text(text: str) -> str:
# ...
注意:一個(gè)重要的認(rèn)知細(xì)節(jié) stop_after_attempt(N) 指的是總共的嘗試次數(shù),而不是“重試次數(shù)”。
stop_after_attempt(1)意味著:執(zhí)行 1 次,如果失敗,立即停止。它根本不會重試。stop_after_attempt(3)意味著:總共執(zhí)行 3 次,即首次嘗試 + 2 次重試。
記住這個(gè)簡單的規(guī)則:如果你希望在失敗后能額外重試 Y 次,那么你應(yīng)該設(shè)置 stop_after_attempt(Y + 1)。
我們也可以按時(shí)間來限制,比如 stop_after_delay(10) 表示“10秒后停止”。更棒的是,你可以用 | (或) 操作符將它們組合起來,哪個(gè)條件先滿足就停止。
from tenacity import stop_after_delay
# 總次數(shù)達(dá)到 5 次或總耗時(shí)超過 30 秒,就停止
@retry(stop=(stop_after_attempt(5) | stop_after_delay(30)))
def translate_text(text: str) -> str:
# ...
2. 設(shè)置等待策略 (wait)
連續(xù)不斷地快速重試可能會壓垮服務(wù)器或達(dá)到頻率限制。在兩次重試之間加入等待是明智之舉。最簡單的是固定等待,使用 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í),更推薦指數(shù)退避 (wait_exponential)。它會隨著重試次數(shù)的增加,逐漸拉長等待時(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. 決定何時(shí)重試 (retry)
默認(rèn)情況下,tenacity 會在遇到任何異常時(shí)都進(jìn)行重試。但這并不總是對的。
比如,APIConnectionError (網(wǎng)絡(luò)問題) 或 RateLimitError (請求太頻繁) 是典型的可恢復(fù)錯(cuò)誤,重試很有可能會成功。但 AuthenticationError (密鑰錯(cuò)誤) 或 PermissionDeniedError (無權(quán)限) 則是致命錯(cuò)誤,重試多少次都注定失敗。
我們可以通過 retry_if_not_exception_type 來告訴 tenacity 遇到某些致命錯(cuò)誤時(shí)不要重試。
注意:一個(gè)常見的語法陷阱 當(dāng)指定多個(gè)異常類型時(shí),你可能會直覺地寫成 AuthenticationError | PermissionDeniedError。
# 錯(cuò)誤的方式!這無法按預(yù)期工作 @retry(retry=retry_if_not_exception_type(AuthenticationError | PermissionDeniedError))
在現(xiàn)代 Python 中,A | B 創(chuàng)建的是一個(gè) UnionType 對象,而 tenacity 的這個(gè)函數(shù)期望接收一個(gè)包含異常類型的元組 (tuple)。
正確的寫法是:
from openai import AuthenticationError, PermissionDeniedError # 正確的方式!使用元組 @retry(retry=retry_if_not_exception_type((AuthenticationError, PermissionDeniedError)))
這個(gè)小小的括號,至關(guān)重要。
當(dāng)重試最終失敗時(shí)
如果 tenacity 在用盡所有嘗試后依然失敗,它會怎么做?默認(rèn)情況下,它會拋出一個(gè) RetryError,其中包含了最后一次失敗時(shí)的原始異常。
但有時(shí)我們不希望程序崩潰,而是想執(zhí)行一些自定義的收尾工作,比如記錄一條詳細(xì)的錯(cuò)誤日志,并返回一個(gè)友好的錯(cuò)誤提示。這就是 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é)果或錯(cuò)誤提示"
@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ù)里的一個(gè)微妙陷阱 在回調(diào)函數(shù)中,我們?nèi)绾伟踩孬@取最后一次的異常信息?
def return_last_value(retry_state: RetryCallState):
# 危險(xiǎn)!這會重新拋出異常!
return "失?。? + retry_state.outcome.result()
retry_state.outcome 代表了最后一次嘗試的結(jié)果。如果那次嘗試是失敗的,調(diào)用 .result() 方法會重新拋出那個(gè)異常,導(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} 次嘗試后失敗。最后一次錯(cuò)誤是: {last_exception}"
當(dāng) Tenacity 遇到面向?qū)ο?/h2>
隨著代碼庫的增長,我們通常會把邏輯封裝在類里。這時(shí),我們會遇到兩個(gè)更深層次的問題:作用域和繼承。
1. 回調(diào)函數(shù)如何訪問 self
假設(shè)我們的回調(diào)函數(shù)需要訪問類的實(shí)例變量(比如 self.name)。我們可能會很自然地這樣寫:
class TTS:
def __init__(self, name):
self.name = name
def _my_callback(self, retry_state):
print(f"實(shí)例 {self.name} 的任務(wù)失敗了。")
# ...
# 這會失敗!NameError: name 'self' is not defined
@retry(retry_error_callback=self._my_callback)
def run(self):
# ...
這會立即報(bào)錯(cuò),因?yàn)?@retry 裝飾器是在定義類的時(shí)候執(zhí)行的,那時(shí)還沒有任何類的實(shí)例,自然也就沒有 self。
最優(yōu)雅的解決方案是**“內(nèi)部函數(shù)閉包”**模式。我們將裝飾器應(yīng)用在一個(gè)定義于實(shí)例方法內(nèi)部的函數(shù)上:
class TTS:
def __init__(self, name):
self.name = name
def run(self):
# 在這里,self 是可用的!
@retry(
# 因?yàn)檠b飾器在 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):
# ...
這是一種非常強(qiáng)大且 Pythonic 的模式,完美解決了作用域問題。
2. 如何在父類中定義重試策略,讓所有子類繼承
這是我們討論的最后一個(gè),也是最體現(xiàn)設(shè)計(jì)思想的問題。假設(shè)我們有一個(gè) BaseProvider 父類,和多個(gè) MyProviderA, MyProviderB 子類。我們希望所有子類都遵循統(tǒng)一的重試規(guī)則。
一個(gè)常見的錯(cuò)誤想法是在父類的空方法上應(yīng)用裝飾器。當(dāng)子類重寫該方法時(shí),父類上的裝飾器也隨之丟失。
正確的解決方案是模板方法設(shè)計(jì)模式 (Template Method Pattern)。
- 父類定義一個(gè)模板方法 (
_exec),它包含了不可變的算法框架(即我們的重試邏輯)。 - 這個(gè)模板方法會調(diào)用一個(gè)抽象的鉤子方法 (
_do_work)。 - 子類只需要實(shí)現(xiàn)這個(gè)鉤子方法,填充具體的業(yè)務(wù)邏輯即可。
讓我們用一個(gè)更完整的例子來構(gòu)建這個(gè)模式:
from openai import OpenAI, AuthenticationError, PermissionDeniedError
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type, RetryCallState
# 1. 定義一個(gè)通用的、可復(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ù)不同異常類型,進(jìn)行日志記錄并拋出自定義的、更友好的 RuntimeError
# ... 此處可以添加更復(fù)雜的異常分類邏輯 ...
raise RuntimeError(f"重試 {retry_state.attempt_number} 次后最終失敗: {ex}") from ex
raise RuntimeError(f"重試 {retry_state.attempt_number} 次后失敗,但未捕獲到異常。")
# 2. 實(shí)現(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)用鉤子方法,由子類實(shí)現(xiàn)
return self._do_work()
def _do_work(self) -> str:
"""這是鉤子方法,子類必須實(shí)現(xiàn)它。"""
raise NotImplementedError("子類必須實(shí)現(xiàn) _do_work 方法")
# 3. 實(shí)現(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)建了一個(gè)既健壯又易于擴(kuò)展的框架。
tenacity 是一個(gè)看似簡單,實(shí)則功能強(qiáng)大的庫。它不僅能輕松應(yīng)對簡單的重試場景,更能通過巧妙的設(shè)計(jì)模式,解決復(fù)雜的、面向?qū)ο蟮膽?yīng)用程序中的可靠性問題。
到此這篇關(guān)于Python使用Tenacity一行代碼實(shí)現(xiàn)自動重試詳解的文章就介紹到這了,更多相關(guān)Python Tenacity自動重試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
pytorch 如何使用batch訓(xùn)練lstm網(wǎng)絡(luò)
這篇文章主要介紹了pytorch 如何使用batch訓(xùn)練lstm網(wǎng)絡(luò)的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-05-05
python關(guān)于變量名的基礎(chǔ)知識點(diǎn)
在本篇文章里小編給大家整理的是關(guān)于python關(guān)于變量名的基礎(chǔ)知識點(diǎn),需要的朋友們可以參考下。2020-03-03
Python實(shí)現(xiàn)將藍(lán)底照片轉(zhuǎn)化為白底照片功能完整實(shí)例
這篇文章主要介紹了Python實(shí)現(xiàn)將藍(lán)底照片轉(zhuǎn)化為白底照片功能,結(jié)合完整實(shí)例形式分析了Python基于cv2庫進(jìn)行圖形轉(zhuǎn)換操作的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-12-12
Pytorch使用VGG16模型進(jìn)行預(yù)測貓狗二分類實(shí)戰(zhàn)
VGG16是Visual Geometry Group的縮寫,它的名字來源于提出該網(wǎng)絡(luò)的實(shí)驗(yàn)室,本文我們將使用PyTorch來實(shí)現(xiàn)VGG16網(wǎng)絡(luò),用于貓狗預(yù)測的二分類任務(wù),我們將對VGG16的網(wǎng)絡(luò)結(jié)構(gòu)進(jìn)行適當(dāng)?shù)男薷?以適應(yīng)我們的任務(wù),需要的朋友可以參考下2023-08-08
Python cv2 圖像自適應(yīng)灰度直方圖均衡化處理方法
今天小編就為大家分享一篇Python cv2 圖像自適應(yīng)灰度直方圖均衡化處理方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-12-12
Python實(shí)現(xiàn)獲取照片拍攝日期并重命名的方法
這篇文章主要介紹了Python實(shí)現(xiàn)獲取照片拍攝日期并重命名的方法,涉及Python針對文件屬性及文件名相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
Python基礎(chǔ)教程之錯(cuò)誤和異常的處理方法
程序在運(yùn)行時(shí),如果python解釋器遇到一個(gè)錯(cuò)誤,會停止程序的執(zhí)行,并且提示一些錯(cuò)誤信息,這就是異常,下面這篇文章主要給大家介紹了關(guān)于Python基礎(chǔ)教程之錯(cuò)誤和異常的處理方法,需要的朋友可以參考下2022-05-05

