概述
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 上下文 |