tenacity -- Python中一個(gè)專門用來retry的庫

原文鏈接

動(dòng)機(jī)

很多時(shí)候,我們都喜歡為代碼加入retry功能。比如oauth驗(yàn)證,有時(shí)候網(wǎng)絡(luò)不太靈,我們希望多試幾次。

這些retry應(yīng)用的場(chǎng)景看起來不同,其實(shí)又很類似。都是判斷代碼是否正常運(yùn)行,如果不是則重新開始。

那么,有沒有一種通用的辦法來實(shí)現(xiàn)呢?

簡(jiǎn)介

Tenacity1是一個(gè)通用的retry庫,簡(jiǎn)化為任何任務(wù)加入重試的功能。

它還包含如下特性:

  • 通用的裝飾器API
  • 可以設(shè)定重試停止的條件(比如設(shè)定嘗試次數(shù))
  • 可以設(shè)定重試間的等待時(shí)間(比如在嘗試之間使用冪數(shù)級(jí)增長(zhǎng)的wait等待)
  • 自定義在哪些Exception進(jìn)行重試
  • 自定義在哪些返回值的情況進(jìn)行重試
  • 協(xié)程的重試

用法
基本用法

from tenacity import *

# 基礎(chǔ)的用法,會(huì)一直重試下去,直到函數(shù)沒有拋出異常,正常返回值
@retry
def never_give_up_never_surrender():
    print("一直重試,忽略exceptions,重試間沒有等待時(shí)間")
    raise Exception

何時(shí)停止

讓我們加入停止的條件.

例如,在達(dá)到嘗試次數(shù)后停下來:

@retry(stop=stop_after_attempt(7))
def stop_after_7_attempts():
    print("嘗試7次后停下")
    raise Exception

在10秒后,如果仍然沒有成功,則停下:

@retry(stop=stop_after_delay(10))
def stop_after_10_s():
    print("10秒后停止")
    raise Exception

可以使用|操作符,來組合多種條件:

@retry(stop=(stop_after_delay(10) | stop_after_attempt(5)))
def stop_after_10_s_or_5_retries():
    print("10秒后,或者嘗試5次后,停下來")
    raise Exception

嘗試間的等待

很多事并不是越快越好。所以,讓我們?cè)谥卦嚨膰L試之間加入一些間隔時(shí)間:

@retry(wait=wait_fixed(2))
def wait_2_s():
    print("每次重試間都有2秒間隔")
    raise Exception

間隔可以是隨機(jī)的:

@retry(wait=wait_random(min=1, max=2))
def wait_random_1_to_2_s():
    print("重試間隔1-2秒")
    raise Exception

還可以加入指數(shù)曲線形式的間隔:

@retry(wait=wait_exponential(multiplier=1, min=4, max=10))
def wait_exponential_1():
    print("開始的時(shí)候等待 2^x * 1 秒,最少等待4秒,最多10秒,之后都是等待10秒")
    raise Exception

多核在競(jìng)爭(zhēng)一個(gè)共享的資源,使用指數(shù)間隔可以將沖突最小化:

@retry(wait=wait_random_exponential(multiplier=1, max=60))
def wait_exponential_jitter():
    print("隨機(jī)等待 2^x * 1 秒,最多60秒,之后都是等待60秒")
    raise Exception

可以自定義每次等待時(shí)長(zhǎng):

@retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] +
                           [wait_fixed(7) for i in range(2)] +
                           [wait_fixed(9)]))
def wait_fixed_chained():
    print("前三次等待3秒,后兩次等待7秒,最后一次等待9秒")
    raise Exception

何時(shí)retry

默認(rèn)情況下,只有函數(shù)拋出異常時(shí)才會(huì)retry。

你可以設(shè)置在制定的異常才進(jìn)行retry:

@retry(retry=retry_if_exception_type(IOError))
def might_io_error():
    print("只有在IOError的時(shí)候進(jìn)行retry,其它時(shí)候照常拋出錯(cuò)誤")
    raise Exception

可以在判斷返回值是否是需要的情況下進(jìn)行retry:

def is_none_p(value):
        return value is None

@retry(retry=retry_if_result(is_none_p))
def might_return_none():
    print("因?yàn)榉祷刂凳荖one,所以這個(gè)函數(shù)會(huì)一直retry")
        
# 這樣寫也是可以的,不用修改原來的代碼
retry_version_func = retry(retry=retry_if_result(is_none_p))(might_return_none)    

當(dāng)然,這里也可以組合多個(gè)條件:

def is_none_p(value):
    return value is None

@retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type()))
def might_return_none():
    print("在拋出任何異常,或者返回值是None的情況下,進(jìn)行retry")

其它

在函數(shù)體內(nèi),你可以手動(dòng)拋出TryAgain錯(cuò)誤,進(jìn)行重試:

@retry
def do_something():
   result = something_else()
   if result == 23:
      raise TryAgain

通過參數(shù)reraise=True,可以拋出函數(shù)最后一次拋出的異常。如果沒有設(shè)定,會(huì)拋出RetryError:

@retry(reraise=True, stop=stop_after_attempt(3))
def raise_my_exception():
    raise MyException("Fail")

try:
    raise_my_exception()
except MyException:
    print('MyException會(huì)被拋出')

在重試的前后,記錄日志:

import logging

logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

logger = logging.getLogger(__name__)

# 重試前記錄
@retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG))
def raise_my_exception():
    raise MyException("Fail")
    
# 重試后記錄
@retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG))
def raise_my_exception():
    raise MyException("Fail")

你可以獲取retry的相關(guān)統(tǒng)計(jì)數(shù)據(jù):

@retry(stop=stop_after_attempt(3))
def raise_my_exception():
    raise MyException("Fail")

try:
    raise_my_exception()
except Exception:
    pass

print(raise_my_exception.retry.statistics)

熱度分析

這個(gè)庫已經(jīng)6歲了,截止2019.5.4日已累計(jì)獲取1478star, 75fork.
源碼分析

這個(gè)庫在代碼和項(xiàng)目方面都是典范,同時(shí)API設(shè)計(jì)的也是相當(dāng)漂亮。

這個(gè)庫對(duì)python裝飾器的用法已經(jīng)爐火純青,基本所有的情景都有用到。有興趣的同學(xué)可以通過下面幾個(gè)點(diǎn)去看:

  • retry裝飾器為什么可以無參數(shù)版本/有參數(shù)版本混合使用
  • retry裝飾器為什么可以作用函數(shù)和方法
  • retry裝飾器為什么可以作用于asyncio協(xié)程,tornado協(xié)程,普通函數(shù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容