はじめに
Pythonで「一定回数だけ繰り返して、うまく行けば続行、数回実行してもダメならエラーを吐く」みたいな処理を実装したいときがありました。「リトライ処理」とか言えば伝わるっぽいですが、どうすればいいのかすぐ忘れてしまうので書いておきます。
外部モジュールを利用する(tenancityモジュール)
リトライ処理を実装するための便利なモジュールがあるそうです。
tenacity
やretry
やretrying
などがあるそうですが、tenacity
が一番市民権を得てそうです。
ただ、私は「できるんなら外部モジュールを使わずに済むならそれにこしたことはない教」に入信しているので、説明は他の記事に任せます。
for-else構文とtry-except構文を組み合わせる
try-except構文は有名ですが、forループにもfor-else構文というのがあるのを最近知りました。
「forループが"最後"まで回り切った後」に実行する処理を記述できる便利な構文です。
実装
図で書くとこんな感じの処理がしたいです。流行のmermaidで書いてみたけど逆にわかりにくい。
ソースコードは以下の通り。
forループ内の処理分岐が(ソースコードを読んだときに)分かりやすいように、try-except-else構文を使って以下の通りに分けています。
- try: 実行させたい処理(何回か試さないとうまく行かなさそうな処理)
- except: 異常発生時の処理(うまく行かなかったときの処理+
time.sleep(1)
) - else: 正常終了時の処理(うまく行った時の処理+
break
)
import time # タイマー用
import traceback # 例外検知用
for _ in range(3): # 最大3回実行
try:
<繰り返しさせたい処理>
except Exception as e:
print("失敗しました。もう一度繰り返します", _)
print(traceback.format_exc()) # 例外の内容を表示
time.sleep(1) # 適当に待つ
else:
print("成功しました。ループを終了します。")
break
else:
print("最大試行回数に達しました。処理を中断します")
raise <適当なエラー>
※上では、説明のために、わざわざ正常終了時の処理をelse文として分けて記述しています(まぁ、逆にわかりにくくなってる気もするけど…)。別にelse文の中に記述しなくても、break
でforループ抜けた後、以降そのままふつうに処理が行われていくので、try文の中にそのままbreak文も書けば十分だと思います。
おわりに
for-else構文を使った方法があまり見つからなかったのと、メモがてら記入。
デコレータにすればもうちょっと見た目よくなりそう。まぁ、デコレータにするぐらいだったらtenacity
モジュール入れたほうがいい気もする。
※追記
検索したら全然普通にありました。記事にするまでもなかった。恥ずかしい…。
https://teratail.com/questions/77111
※if分岐を使う場合
別にif分岐使ってもイイ。ただし、ネストが深くなって読みにくい気がする…。
import time
max_retry=10
for _ in range(max_retry): # 最大10回実行
try:
<繰り返しさせたい処理>
except Exception as e:
if _==max_retry-1:
print("最大試行回数に達しました。処理を中断します")
raise <適当なエラー>
print("失敗しました。もう一度繰り返します", _)
time.sleep(1)
else:
print("成功しました。ループを終了します")
break
(参考)デコレータを使う場合
いまさらになってデコレータについて勉強したので追記
<<<繰り返しさせたい処理>>>の部分を関数化できるのであれば、デコレータで挟むのでもよい。
デコレータとは以下みたいなもの
def func_decorator(func_decorated):
def wrapper(*args, **kwargs): # おまじない
print("----------")
func_decorated(*args, **kwargs)
print("----------")
return wrapper # おまじない
@func_decorator
def decorated_func(name):
print(f"Hello, {name}!")
decorated_func("Alice")
#実行結果
#------------ # <- デコレーターで記述した処理
#Hello, Alice! # <- <<<デコレートさせたい処理>>>
#------------ # <- デコレーターで記述した処理
上述のデコレータの記述方法を諸々含めて書き直すと以下のような感じ
#%%
import time
import traceback
def retry_func(func):
def wrapper(*args):
for _ in range(3): # 最大10回実行
try:
func(*args)
except Exception as e:
print("失敗しました。もう一度繰り返します.", _)
time.sleep(1)
print(traceback.format_exc())
else:
print("成功しました。ループを終了します。")
break
else:
print("最大試行回数に達しました。処理を終了します")
raise Exception("最大試行回数に達しました。処理を中断します.")
return wrapper
@retry_func
def zero_divide(x: int) -> float:
return x/0
zero_divide(1)
#
#失敗しました。もう一度繰り返します... 0
#Traceback (most recent call last):
# File "<ipython-input-39-44c03dfad47f>", line 8, in wrapper
# func(*args)
# File "<ipython-input-39-44c03dfad47f>", line 22, in zero_divide
# return x/0
#ZeroDivisionError: division by zero
#
#<<<中略>>>
#
#最大試行回数に達しました。処理を終了します
#---------------------------------------------------------------------------
#Exception Traceback (most recent call last)
#c:\Users\<<HOGEHOGE>>>\aaa.py in <cell line: 24>()
# 94 @retry_func
# 95 def zero_divide(x: int) -> float:
# 96 return x/0
#---> 98 zero_divide(1)
#
#c:\Users\<<HOGEHOGE>>>\aaa.py in retry_func.<locals>.wrapper(*args)
# 90 else:
# 91 print("最大試行回数に達しました。処理を終了します")
#---> 92 raise Exception("最大試行回数に達しました。処理を中断します...")
#
#Exception: 最大試行回数に達しました。処理を中断します...
#