Python基礎でスキップしていたこと：「例外処理」について学ぶ

##  あらすじ

### 例外処理ってif分で判定するだけじゃないの？という大きな勘違い
業務で使用するツールを作成したり、pythonを使用することがしばしばあります。とはいえ、ちょっとした効率化ツールっだったり社内向けのツール開発がほとんどで、例外処理については`if分で判定しておけばいいんでしょ`くらいの感覚でした。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562770/5756306d-0948-f655-3689-3a5b7e9da047.png)
ただこの場合エラーが発生してしまうと必ずプログラムが終了してしまいます。
小さなツールだと「そういう癖があるのね」くらいで済みますが、これを参照してほかのプログラムが動いたりする場合はしっかり処理しておかないと、何が原因で落ちてるのかわかりにくくなります。
また、コメントをしっかり残さないと何のためのif文なのかもよくわからなくなりがちです。

### きっかけ
Web開発をする際に学んだJavascriptやPHPを学んだ際にサーバー接続やDBの処理を伴う部分で、`try-catch`構文というものを学びました。
> Pythonでも似たようなものがあったな…スルーしてたわ

そう、`try-except`です。
別の言語で使用例を見たことで、なんとなく意味が分かったところで、Pythonで出てくるこいつを完全理解してやろうと思い立ちました。ついでに別の言語で用いられる`try-except`の理解も進むという1度にn度美味しい展開に！！

## try...except...について理解する
### これなに…？
プログラム実行時にエラーが発生する可能性のある箇所を囲って、例外が発生するまで処理し、例外の有無によって処理を分岐させることができる便利な箱みたいなものです。
これをうまく作ると、実行時エラーで強制終了することなく処理することが可能なうえ、どのような例外が発生しているか、ユーザー側にわかりやすく状況を伝えることも可能です。
**下図[3]の例外発生→適切な処理と書いてある部分に相当します。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562770/e43c0d7f-dc85-bc6d-5a66-99372a8b7eaf.png)

### そもそも例外とは…？
エラーの話かと思いきや例外って何なんでしょう？
私もずっとよくわかってませんでした。わざわざ「例外」って言いかえるのはどうしてなのか。
いろいろと読み漁ってみたところ、例外とは要するに、`思ってたんと違う…`な結果になるものです。(厳密な定義はあると思うが、イメージの話)
エラーや警告のほか、文法的には動作はするものの開発者の想定と異なる形式になっているデータを例外として扱うことができます。

### 処理の流れ
まずは基本形はこちら
```python
try:
    例外が起こると想定される処理を書く
    ||
    # 条件に合致したら例外を発生させる
    raise <例外のクラス>

# 例外のクラス別に複数ブロックかいてもOK
except <例外のクラス>: 
    ↑に書いたクラスの例外が発生した場合の処理を書く
# 複数の例外クラスをsetにまとめてもOK
except (<例外のクラス1>,<例外のクラス2>) as e:  # as   
    ↑に書いたクラスの例外が発生した場合の処理を書く

finally :  # 書かなくてもOK
    例外の発生の有無によらず処理される内容を書く
```

まずはtry内が実行されます。try内で例外が発生した場合はそれ以降は処理されず、例外のクラスに応じたexceptに処理が移ります。わざわざこんな構文を使わないといけないのかというと必ずしもそうではないのですが、このexceptの処理を強制できる点が最大のメリットになります。
また、これのほか最後に処理されるfinallyというブロックを追加することも可能です。`ファイルは開いたらちゃんと閉じようね?`等のお約束の処理を例外の有無によらず行いたい場合に使用します。
- **except (\*\*\*, \*\*\*, ...) に該当する例外がでた場合**
  1. except内が実行される
  2. finally内が実行される
※この時、exceptにreturnを書いてもfinallyは実行される
返り値も最後に実行されたものになる
- **except (\*\*\*, \*\*\*, ...) に該当しない例外が出た場合(想定できていなかった場合)**
  1. finally内が実行される（あれば）
  2. エラーが出て止まる

仕組みはこんな感じですが、解説記事の中にはプリント文が書かれているだけの使用例もよく見かけるので、
> これだけのために、こんないろいろ書くなら、if でよくない？

となる人も少なくないのではと感じました。（自分がそうだった）

### 書いて挙動を見てみよう
動作を確認しながら自分でも使用例を書いてみました。
ベースになるのは下記のようなスーパーシンプルな関数です。
```python
def without_try_except_sample(word_1, word_2):
    return word_1 + word_2

print(without_try_except_sample("udon-", "daisuki!!!"))
# >>> udon-daisuki!!!
```
このとき、開発者は
> 文字列の合体に使ってほしいなぁ…

と考えているとします。👈この前提が何気に大事  
ところがこの関数では以下のようなことが発生します。

1. **引数に`None`が入ると エラーが出てしまう**
2. **文字列結合意外にも使えたり使えなかったりする**

#### 1. **引数に`None`が入ると エラーが出てしまう**
まず、引数の少なくとも一方に`None`が入ると`＋の演算ができないよ`とエラーが出ます。
> \>\>\> TypeError: can only concatenate str (not "NoneType") to str

とか
> \>\>\> TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'

です。
上記の例では`TypeError`が発生することがわかりました。
この`TypeError`をキャッチしてイイ感じに処理したいところ。例えば、`False`を返すことで、エラーあったよと伝えたい。そこで、`try…except`を使用して下記のように書き換えます。

```diff_python
def try_except_sample1(word_1, word_2):
+   try:
        return word_1 + word_2  # ここで例外発生
+   except TypeError:
+       return False

print(try_except_sample1("udon-", None))
# >>> False
```
これで実行エラーを出すことなく、例外を処理することができました。  

#### 2. **文字列結合意外にも使えたり使えなかったりする**
ただ、この関数は文字列結合意外の処理もできてしまいます。
例えば、
```python
print(try_except_sample1(1, 2))
# >>> 3
print(try_except_sample1([1,2,3], [4,5]))
# >>> [1,2,3,4,5]
```
 ついでに使えて便利じゃん！と思える反面、想定外の使われ方は想定外の不具合につながる可能性もあるでしょう。
 そこで、`raise`を使用することで例外を発生させます。
```diff_python
def try_except_sample2(word_1, word_2):
    try:
+       # str型でない値が代入されていたらTypeErrorとして扱う
+       if not isinstance(word_1,str) or not isinstance(word_2,str):
+           raise TypeError("Type error: use 'str' type!!\n"
+                           f"word_1 = {type(word_1)} , "
+                           f"word_2 = {type(word_2)}"
+           )
        return word_1 + word_2
-   except TypeError:
+   except TypeError as e:  # as で例外の内容を表示できるようにする
+       print(e)
        return False

print(try_except_sample2("udon-", None))
# >>> Type error: use 'str' type!!
#     word_1 = <class 'str'> , word_2 = <class 'NoneType'>
#     False
```
エラーで中断されることなく、`文字つかえや！`と`False`を返すことができました。このようにして、想定される例外をユーザー側に伝えながら動作させることができるのです！  
これが例外処理…見える景色が少し変わった気がします。
そのほかにもexceptでの処理を強制することで、下記のようなことを行えます。
- **例外発生のログを記録する処理を行う**
- **不要になったリソースを開放する処理を行う**
- **すべての処理を適切に中断する（上記含む）**

## 例外処理は結局どのタイミングで書くもの…？
ここまで来て一つ疑問が。  
> 例外は先周りして対処すべき？例外が出てから潰すべき？

- 先回りする場合は発生する例外クラスを明示する必要があります。例外の種類に応じて処理が変わるのであればなおさらです。ただ、例外なんていろいろありすぎて良くわかっていないのが現在地。したがって、例外が具体的に発生したことを確認してから例外の種類に特化した処理を入れると良いのかもしれません。よく使われると思われるケースを調べてみました。処理に応じてよく出てくるものは把握しておくと良いと思います。

|使用例|観点|例外クラス|
|:-:|:--|:--|
|関数の引数のバリデーション|- 型<br>- 値の範囲|- TypeError<br>- ValueError|
|ファイル操作|- ファイル有無<br>- 権限設定|- FileNotFoundError<br>- PermissionError|
|数値計算上の禁足事項|- 型<br>- ゼロで割る|- ZeroDivisionError|

- それ以外は後から対処するのが良いでしょう。`except Exception as e`のように汎用的な例外クラスを使用して、いったん補足することも可能です。その後絞り込みを行うのが良いと思われます。

## まとめ
Pythonの`try...except`について学びました。完全理解！は言い過ぎかもしれませんが、自分の中ではまぁまぁ腹落ちしたので今後はこれまでよりもずっと適切に例外処理ができるようになりそうです！
「他にもこんな使い方があるよ！」などあればぜひ教えてください!