Tenacity(Python的坚韧重试库)

概述

Tenacity 是一个基于 Apache 2.0协议 的通用重试库,用Python编写,旨在简化向任何代码添加重试逻辑的过程。它起源于已停止维护的 retrying 库的分叉版本。Tenacity 不兼容 retrying 的API,但新增了大量功能并修复了长期存在的错误。

文档:Tenacity — Tenacity documentation

主页:https://github.com/jd/tenacity

核心功能

  • 当函数因异常(Exception,也适用于其它各种异常)失败时自动重试,直到成功返回结果。

基础示例

在程序单元(函数定义之前)声明 @retry 装饰器。

# 导入random模块,用于生成随机数
import random
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_attempt

@retry  # 默认行为:无限重试,不等待
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被视作失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

程序运行结果如下:

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=3,失败!
randomValue=9,失败!
randomValue=2,失败!
randomValue=3,失败!
randomValue=9,失败!
randomValue=1,成功了!
成功!

函数do_something_unreliable()会被反复执行,直到满足要求的随机数产生位置。以此段代码模拟反复执行程序,直到成功为止。


安装

$ pip install tenacity

功能示例

在默认情况下,当异常发生时,将无限重试,直到程序正确执行完毕为止,反复尝试时不等待(没有时间间隔)。(见上面的“基础示例”)

1、停止条件

(1) 以尝试次数为条件

如果在指定的尝试次数后仍存在异常(程序仍没有得到想要的结果),将强行停止这种反复的尝试。在 @retry 装饰器中用 stop=stop_after_attempt(尝试次数) 来指定尝试次数。

# 导入random模块,用于生成随机数
import random
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(5))  # 最多尝试5次后停止
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

程序执行结果1(程序在指定的次数内获得了成功。以下是程序在第4次运行时获得了成功!):

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=3,失败!
randomValue=9,失败!
randomValue=9,失败!
randomValue=0,成功了!
成功!

 程序执行结果2(程序在指定的次数内没能获得成功。以下是程序在5次反复尝试后仍没有获得成功,最终被强行终止),被强行终止,将抛出tenacity.RetryError。

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=4,失败!
randomValue=10,失败!
randomValue=3,失败!
randomValue=6,失败!
randomValue=3,失败!
Traceback (most recent call last):
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 478, in __call__
    result = fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^
  File "D:\pyproject\pa001\AAP01\test1.py", line 13, in do_something_unreliable
    raise IOError("randomValue不满足要求,被是做失败!")
OSError: randomValue不满足要求,被是做失败!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\pyproject\pa001\AAP01\test1.py", line 18, in <module>
    print(do_something_unreliable())  # 输出:成功!(经过若干次重试)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 336, in wrapped_f
    return copy(f, *args, **kw)
           ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 475, in __call__
    do = self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 376, in iter
    result = action(retry_state)
             ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 419, in exc_check
    raise retry_exc from fut.exception()
tenacity.RetryError: RetryError[<Future at 0x1d5f9e55110 state=finished raised OSError>]

(2) 以总耗时为条件

在指定的时间长度内,反复的尝试,直到程序成功执行或时间耗尽为止。

以下代码将在2秒内对因异常退出的程序单元(函数do_something_unreliable)反复尝试,直到时间耗尽或程序单元得到正确的结果而退出。在 @retry 装饰器中用 stop=stop_after_delay(尝试总耗时秒数) 来指定尝试最多总时长。

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_delay

@retry(stop=stop_after_delay(2))  # 2秒后停止重试
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

程序结果1(程序单元在2秒内反复尝试了4此,直到时间耗尽都没能成功的完成任务):

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=8,失败!
randomValue=8,失败!
randomValue=5,失败!
randomValue=9,失败!
Traceback (most recent call last):
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 478, in __call__
    result = fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^
  File "D:\pyproject\pa001\AAP01\test1.py", line 19, in do_something_unreliable
    raise IOError("randomValue不满足要求,被是做失败!")
OSError: randomValue不满足要求,被是做失败!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\pyproject\pa001\AAP01\test1.py", line 24, in <module>
    print(do_something_unreliable())  # 输出:成功!(经过若干次重试)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 336, in wrapped_f
    return copy(f, *args, **kw)
           ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 475, in __call__
    do = self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 376, in iter
    result = action(retry_state)
             ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 419, in exc_check
    raise retry_exc from fut.exception()
tenacity.RetryError: RetryError[<Future at 0x206c52e3150 state=finished raised OSError>]

进程已结束,退出代码为 1

 程序结果2(程序单元在2秒内的第3次运行时得到了想要的结果而结束):

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=3,失败!
randomValue=2,失败!
randomValue=0,成功了!
成功!

(3) 组合条件

 将“尝试次数”和“总耗时”两个条件组合在一起,满足二者之一时停止尝试。在 @retry 装饰器中用 stop=(stop_after_delay(总耗时秒数) | stop_after_attempt(尝试次数)) 来指定耗时和尝试次数的符合条件。

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time

# 从tenacity库中导入retry、stop_after_delay和stop_after_attempt装饰器
from tenacity import retry, stop_after_delay, stop_after_attempt

# 使用retry装饰器定义一个重试策略,最多重试5次或等待2秒
@retry(stop=(stop_after_delay(2) | stop_after_attempt(5)))
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

 上面代码中的do_something_unreliable()函数,无论尝试的总耗时超过2秒,或尝试次数超过5次,都将停止反复尝试循环。

2、等待策略

我们往往需要在上一次尝试结束和下一次尝试之前设置时间间隔。其目的可能是需要为系统设置冷静时间或为了欺骗私服系统(例如:网络爬虫为了更好的拟人而在多次尝试之间,采用不同于机器系统的固定时间间隔,也就是随机时间间隔)。这些“等待”需求,都可以利用Tenacity来方便的满足。

(1) 固定间隔等待

@retry 装饰器中用 wait=wait_fixed(间隔秒数) 来指定两次重复尝试之间的时间间隔(等待时间)

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time

# 从tenacity库中导入retry、stop_after_delay和stop_after_attempt装饰器
from tenacity import retry, wait_fixed

# 使用retry装饰器定义一个重试策略,每次重试间隔2秒
@retry(wait=wait_fixed(2))
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

上面的代码会不断的反复尝试执行do_something_unreliable()函数,直到小于等于1的随机值产生,正确的返回(结束)为止。每次尝试执行do_something_unreliable()函数的时间间隔为2秒。

(2) 随机间隔等待

@retry 装饰器中用 wait=wait_random(min=最小时间间隔秒数, max=最大时间间隔秒数) 来指定两次重复尝试之间的随机时间间隔(随机等待时间)

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time

# 从tenacity库中导入retry、stop_after_delay和stop_after_attempt装饰器
from tenacity import retry, wait_random

# 使用retry装饰器定义一个重试策略,每次重试等待1到2秒的随机时间
@retry(wait=wait_random(min=1, max=2)) 
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

上面的代码会不断的反复尝试执行do_something_unreliable()函数,直到小于等于1的随机值产生,正确的返回(结束)为止。每次尝试执行do_something_unreliable()函数的时间间隔为1~2秒之间的随机时间长度。

(3) 指数时间等待

@retry 装饰器中用 wait=wait_exponential(multiplier=倍数, exp_base=基数,min=最小毫秒数, max=最大毫秒数) 来指定两次重复尝试之间的间隔。时间间隔随着循环尝试次数成指数递增。时间间隔的公式如下:

间隔时间 = 倍数 * ( 基数 ^ 循环次数 )

 循环次数从零开始,顺序值为 0,1,2,......。所以分析如下装饰器内容代码:

wait=wait_exponential(multiplier=1, exp_base=2,min=5, max=128)

带入公式:时间间隔 =  1 *( 2 ^ 循环次数)

按照循环次数分别为(0,1,2,3......),得出的时间间隔序列分别为:

        1,2,4,8,16,32,64,128;

由于最小毫秒数=5最大毫秒数=128,所以最终的间隔时间序列为:

        5,5,5,8,16,32,64,128;

指数时间等待有其独特的用途,例如快速摸排、锁定网络服务器的反爬时间阈值等。

示例代码:

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time

# 从tenacity库中导入retry、stop_after_delay和stop_after_attempt装饰器
from tenacity import retry, wait_exponential

@retry(
    # 使用retry装饰器定义一个重试策略,每次重试等待时间按照指数增长,初始等待时间为1秒,指数基数为2,最小等待时间为1秒,最大等待时间为128秒
    wait=wait_exponential(multiplier=1, exp_base=2,min=5, max=128)
)
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """

    # time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(1, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

代码会依照时间间隔序列(5,5,5,8,16,32,64,128,128,128......)来分配每次重试之间的时间间隔。

(4) 复合方式指定时间间隔

根据实际需求,可以将以上三种方式进行随意组合,如:

ait=wait_fixed(3) + wait_random(0, 2)

起到了在固定3秒间隔的基础上增加了0~2秒随机的作用,与3~5秒随机间隔的作用相同。

复合方式非常具有灵活度,如:

wait=(wait_fixed(30) + wait_random(5, 15)+ wait_exponential(multiplier=1,exp_base=2,min=1, max=128))

 上面代码既具有了指数递增,又具有随机抖动,又具有稳固值,在现实中有着极强的应用性。

3、异常过滤

可以只对某些异常产生响应(既:当指定的某些异常发生时进入重试模式,而对其它异常正常的抛出)。在 @retry 装饰器中用 retry=retry_if_exception_type() 来指定要过滤(响应)的异常。如下代码:

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time
from json import JSONDecodeError

# 从tenacity库中导入retry、stop_after_delay和stop_after_attempt装饰器
from tenacity import retry, retry_if_exception_type

@retry(
    # 使用retry装饰器定义一个重试策略,当捕获到ValueError、IOError、EOFError或JSONDecodeError异常时进行重试
    retry=retry_if_exception_type(
        ValueError | IOError | EOFError | JSONDecodeError
    )
)
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """

    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue == 1: # 模拟失败后返回异常ValueError
        print("randomValue="+str(randomValue) + ",失败!")
        raise ValueError("randomValue不满足要求,被是做失败!")
    elif randomValue ==2: # 模拟失败后返回异常IOError
        print("randomValue=" + str(randomValue) + ",失败!")
        raise IOError("randomValue不满足要求,被是做失败!")
    elif randomValue ==3: # 模拟失败后返回异常EOFError
        print("randomValue=" + str(randomValue) + ",失败!")
        raise EOFError("randomValue不满足要求,被是做失败!")
    elif randomValue ==4: # 模拟失败后返回异常JSONDecodeError
        print("randomValue=" + str(randomValue) + ",失败!")
        raise JSONDecodeError("randomValue不满足要求,被是做失败!")
    elif randomValue >= 5: # 模拟除了以上各异常之外的其它异常 Exception
        print("randomValue=" + str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else: # 模拟成功后返回结果,只有当randomValue==0时才会成功
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

上面代码模拟了程序可能抛出ValueError、IOError、EOFError和JSONDecodeError四种异常,当这四种异常出现时,系统将进入重试状态;而当这四种异常之外的其它Exeption出现时,则不处理,原样抛出(交由上级程序去处理)。

 4、返回值过滤

在不抛出异常Exception的情况下,也可以依据程序单元(函数)的返回值来做出应对,以决定是否进入重试状态。通过 retry=retry_if_result()  来对返回值做出响应。

示例代码:

# 导入random模块,用于生成随机数
import random
# 导入time模块,用于处理时间相关的操作
import time
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, retry_if_result

def is_not_success(result):
    """
    检查结果是否不是成功的。
    如果结果不是"成功!",则返回True,否则返回False。
    """
    return result != "成功!"

# 使用retry装饰器定义一个重试策略,当结果不是"成功!"时进行重试
@retry(retry=retry_if_result(is_not_success))
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    time.sleep(0.5) # 模拟0.5秒的运行耗时
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        return "失败!"
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

程序执行效果如下:

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=3,失败!
randomValue=5,失败!
randomValue=2,失败!
randomValue=0,成功了!
成功!

与通过异常来进入重试状态类同,只是不再利用异常Exception,而是仅仅利用程序单元(函数)的返回值。

5、错误处理

默认抛出RetryError,但可通过reraise=True显示原始异常,代码如下:

# 导入random模块,用于生成随机数
import random
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_attempt

@retry(
    reraise=True,  # 重试后抛出原始异常
    stop=stop_after_attempt(5)
)  # 最多尝试5次后停止
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

在5次没能获取成功结果后,其抛出异常结果如下:

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=7,失败!
randomValue=2,失败!
randomValue=10,失败!
randomValue=5,失败!
randomValue=10,失败!
Traceback (most recent call last):
  File "D:\pyproject\pa001\AAP01\test1.py", line 23, in <module>
    print(do_something_unreliable())  # 输出:成功!(经过若干次重试)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 336, in wrapped_f
    return copy(f, *args, **kw)
           ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 475, in __call__
    do = self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 376, in iter
    result = action(retry_state)
             ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 418, in exc_check
    raise retry_exc.reraise()
          ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 185, in reraise
    raise self.last_attempt.result()
          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\concurrent\futures\_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\concurrent\futures\_base.py", line 401, in __get_result
    raise self._exception
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 478, in __call__
    result = fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^
  File "D:\pyproject\pa001\AAP01\test1.py", line 18, in do_something_unreliable
    raise Exception("randomValue不满足要求,被是做失败!")
Exception: randomValue不满足要求,被是做失败!

可以看到最后一行的显示了原始异常“Exception: randomValue不满足要求,被是做失败!

但如果将 @retry 装饰器中的 reraise=True 更改为 reraise=False(或删除reraise项),则在5次没能获取成功结果后,其抛出异常结果如下:

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=7,失败!
randomValue=2,失败!
randomValue=8,失败!
randomValue=8,失败!
randomValue=2,失败!
Traceback (most recent call last):
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 478, in __call__
    result = fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^
  File "D:\pyproject\pa001\AAP01\test1.py", line 20, in do_something_unreliable
    raise Exception("randomValue不满足要求,被是做失败!")
Exception: randomValue不满足要求,被是做失败!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\pyproject\pa001\AAP01\test1.py", line 25, in <module>
    print(do_something_unreliable())  # 输出:成功!(经过若干次重试)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 336, in wrapped_f
    return copy(f, *args, **kw)
           ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 475, in __call__
    do = self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 376, in iter
    result = action(retry_state)
             ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\gyfin\.conda\envs\pa001\Lib\site-packages\tenacity\__init__.py", line 419, in exc_check
    raise retry_exc from fut.exception()
tenacity.RetryError: RetryError[<Future at 0x177cb774f50 state=finished raised Exception>]

可以看到最后一行显示了异常为:tenacity.RetryError: RetryError... ,并没有暴露原始异常。现实情况下,要根据具体需求,决定是否暴露原始异常。

6、日志回调

在重试机制中,将信息输出到日志中,是调试、优化和维护程序的必备手段。在 @retry修饰器 中用 before=before_log() 或 after=after_log() 来写入log日志。

  • before=before_log() 是在“重试”前记录日志
  • after=after_log() 是在“重试”后记录日志

示例代码如下:

# 导入random模块,用于生成随机数
import random
import logging

# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_attempt, after_log, before_log

# 配置日志记录的基本设置,设置日志级别为 INFO
logging.basicConfig(level=logging.INFO)
# 创建一个名为 "retry_test" 的日志记录器
log = logging.getLogger("retry_test")

@retry(
    reraise=True,
    stop=stop_after_attempt(5), # 最多尝试5次
    before=before_log(log, logging.INFO)  # 在重试之前记录日志
)  # 最多尝试5次后停止
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

运行结果如下:

C:\Users\gyfin\.conda\envs\pa001\python.exe D:\pyproject\pa001\AAP01\test1.py 
randomValue=10,失败!
randomValue=9,失败!
randomValue=1,成功了!
成功!
INFO:retry_test:Starting call to '__main__.do_something_unreliable', this is the 1st time calling it.
INFO:retry_test:Starting call to '__main__.do_something_unreliable', this is the 2nd time calling it.
INFO:retry_test:Starting call to '__main__.do_something_unreliable', this is the 3rd time calling it.

上面结果的后三行是日志输出内容。

7、高级用法

(1) 动态修改重试参数

在通过 @retry 装饰器定义之后,也可以通过代码的方式动态修改重试参数,其效果与 @retry 的定义效果是一样的。

语法:

曾被@retry修饰器修饰过的函数().retry_with(参数修改代码)()

示例代码:

# 导入random模块,用于生成随机数
import random
# 从tenacity库中导入retry和stop_after_attempt装饰器
from tenacity import retry, stop_after_attempt, after_log, before_log

@retry(
    reraise=True,
    stop=stop_after_attempt(5) # 最多尝试5次
)  # 最多尝试5次后停止
def do_something_unreliable():
    """
    做一些可能会失败的操作,直到成功为止。
    获取一个0~10的随机整数,如果大于1,则抛出异常。
    """
    randomValue = random.randint(0, 10)
    if randomValue > 1:
        print("randomValue="+str(randomValue) + ",失败!")
        raise Exception("randomValue不满足要求,被是做失败!")
    else:
        print("randomValue=" + str(randomValue) + ",成功了!")
        return "成功!"

# 使用retry_with方法重新配置重试策略,设置最多尝试10次
do_something_unreliable.retry_with(stop=stop_after_attempt(10))()

print(do_something_unreliable())  # 输出:成功!(经过若干次重试)

 上面的代码在@retry修饰器中,通过 stop=stop_after_attempt(5) 将最大重复次数设置为5,随后又通过 do_something_unreliable.retry_with(stop=stop_after_attempt(10))() 代码将最大重复次数重新动态的设置为10次(覆盖了原来的5次设置)。

(2) tenacity上下文管理器

 用tenacity的上下文管理器来动态开发重试逻辑,但这却增加了程序开发的复杂度,不如修饰器方式清晰,笔者认为这是一个鸡肋的选择。使用tenacity的最重要原因是其简化了重试逻辑,而不是开发出更复杂的重试逻辑。

基础用法示例:

from tenacity import Retrying, stop_after_attempt

try:
    # 创建重试器,最多尝试3次
    for attempt in Retrying(stop=stop_after_attempt(3)):
        with attempt:  # 进入上下文即开始重试
            print("执行可能失败的操作")
            raise ConnectionError("网络连接失败")
except RetryError:
    print("所有重试尝试均失败")

访问重试状态示例(通过 attempt.retry_state 获取当前重试的详细信息):

for attempt in Retrying(stop=stop_after_attempt(2)):
    with attempt:
        print(f"第 {attempt.retry_state.attempt_number} 次尝试")
        raise TimeoutError
# 输出:
# 第 1 次尝试
# 第 2 次尝试

动态修改结果(在重试过程中更新结果,需配合 retry_if_result 策略):

from tenacity import retry_if_result

def is_negative(value):
    return value < 0

retryer = Retrying(retry=retry_if_result(is_negative))

for attempt in retryer:
    with attempt:
        result = -1  # 模拟计算结果
    if not attempt.retry_state.outcome.failed:
        attempt.retry_state.set_result(result)  # 手动设置结果用于条件判断

异步支持(通过 AsyncRetrying 处理协程代码):

from tenacity import AsyncRetrying, stop_after_attempt
import asyncio

async def async_task():
    async for attempt in AsyncRetrying(stop=stop_after_attempt(3)):
        with attempt:
            await asyncio.sleep(1)
            raise RuntimeError("异步操作失败")

asyncio.run(async_task())

混合控制流(结合循环和条件判断实现复杂逻辑):

from tenacity import RetryError, wait_fixed

try:
    for attempt in Retrying(wait=wait_fixed(1)):
        with attempt:
            data = fetch_data_from_api()
            if data.status == "partial":
                print("获取部分数据,继续重试...")
                raise TryAgain  # 手动触发重试
            break  # 成功获取完整数据时退出循环
except RetryError:
    print("无法获取完整数据")

tenacity上下文管理器方式有利于最小化重试范围,仅包裹真正可能失败且可重试的代码,避免重试无关操作。在上下文管理器外初始化资源,在 finally 块中释放:

db = Database.connect()
try:
    for attempt in Retrying(stop=stop_after_attempt(3)):
        with attempt:
            db.execute("UPDATE table SET value=1")
finally:
    db.close()

 tenacity上下文管理器,更有利于性能监控,通过 retry_state 统计重试次数和耗时,集成到监控系统:

for attempt in Retrying():
    with attempt:
        do_something()
    print(f"本次重试耗时: {attempt.retry_state.idle_for}秒")

tenacity上下文管理器的典型应用场景

场景示例
网络请求重试HTTP API调用、WebSocket连接
资源竞争处理数据库锁竞争、文件写入冲突
外部服务熔断第三方服务不可用时按策略重试
条件性重试根据返回内容(如订单未完成状态)决定是否重试
分布式系统协调在ETCD/ZooKeeper操作中处理临时节点冲突

尽管通过tenacity上下文管理器实现上面的应用场景是适宜的,但并不意味着通过@retry修饰器的方式无法完成上述的应用场景。

笔者认为:如果您自身或开发团队无惧 tenacity上下文管理器开发方式所带来的不利因素(程序复杂度带来的成本增加),可以选择这种方式。而如果您和您的开发团队没有大规模且熟练的应用tenacity上下文管理器的经验,那么灵活的使用@retry修饰器来适应上述的应用场景就是您最明智的选择,尽管这同样需要您有一定的创造力,但这样做的直接好处就是:简化重复尝试执行的程序复杂度,使代码更加清晰可读。

通过@retry修饰器可以直接或变相的替代tenacity上下文管理器。如下:

特性装饰器模式 (@retry)上下文管理器
代码侵入性封装函数可灵活的通过函数参数实现直接作用于代码块
动态配置需通过 .retry_with()直接修改 Retrying 实例参数
上下文共享需通过参数传递直接访问外部变量
局部重试可灵活的通过函数参数实现精准控制重试范围
异步支持通过装饰异步函数来实现使用 AsyncRetrying 上下文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值