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

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

# 外部モジュールを利用する（tenacityモジュール）
リトライ処理を実装するための便利なモジュールがあるそうです。
`tenacity`や`retry`や`retrying`などがあるそうですが、`tenacity`が一番市民権を得てそうです。
ただ、私は「できるんなら外部モジュールを使わずに済むならそれにこしたことはない教」に入信しているので、説明は他の記事に任せます。

https://github.com/jd/tenacity

https://zenn.dev/taroman_zenn/articles/dd0b33a3a37d1e

https://ohke.hateblo.jp/entry/2020/11/21/230000


# for-else構文とtry-except構文を組み合わせる
try-except構文は有名ですが、forループにもfor-else構文というのがあるのを最近知りました。
「forループが"最後"まで回り切った後」に実行する処理を記述できる便利な構文です。

https://www.headboost.jp/python-for-else/

# 実装
図で書くとこんな感じの処理がしたいです。流行のmermaidで書いてみたけど逆にわかりにくい。
```mermaid
flowchart TB
開始(開始)-->forループ
subgraph for-else構文
subgraph forループ内
except--次のループへ-->forループ
forループ--実行させたい処理-->try
try--正常終了-->else
try--異常発生-->except
end
forループ--最後まで実行し終わってしまった場合-->for:else処理
for:else処理-->エラー(エラー発砲)
end
else--breakでループから抜け出す-->終了(終了)
```

ソースコードは以下の通り。
forループ内の処理分岐が（ソースコードを読んだときに）分かりやすいように、try-except-else構文を使って以下の通りに分けています。
- try: 実行させたい処理（何回か試さないとうまく行かなさそうな処理）
- except: 異常発生時の処理（うまく行かなかったときの処理＋`time.sleep(1)`）
- else: 正常終了時の処理（うまく行った時の処理＋`break`）

```py
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分岐使ってもイイ。ただし、ネストが深くなって読みにくい気がする…。

```py
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
```


# （参考）デコレータを使う場合
いまさらになってデコレータについて勉強したので追記

https://www.lifewithpython.com/2016/09/python-decorator-with-arguments.html

<<<繰り返しさせたい処理>>>の部分を関数化できるのであれば、デコレータで挟むのでもよい。

デコレータとは以下みたいなもの
```py
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!  # <- <<<デコレートさせたい処理>>>
#------------  # <- デコレーターで記述した処理
```

上述のデコレータの記述方法を諸々含めて書き直すと以下のような感じ
```py
#%%
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: 最大試行回数に達しました。処理を中断します...
#
```