LoginSignup
4
4

Pythonでfor-else構文を使ってリトライ処理する

Last updated at Posted at 2022-05-17

はじめに

Pythonで「一定回数だけ繰り返して、うまく行けば続行、数回実行してもダメならエラーを吐く」みたいな処理を実装したいときがありました。「リトライ処理」とか言えば伝わるっぽいですが、どうすればいいのかすぐ忘れてしまうので書いておきます。

外部モジュールを利用する(tenancityモジュール)

リトライ処理を実装するための便利なモジュールがあるそうです。
tenacityretryretryingなどがあるそうですが、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: 最大試行回数に達しました。処理を中断します...
#
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4