例外となかよく

# はじめに
Pythonコードを書いていて「ここでエラーを発生させてぇ」って思うことありますよね。私はあります。
今回はエラー対処ではなく、エラーそのものについて書いてみます。


# 2種類の「エラー」
「エラー」と口語的書いているが、Pythonの「エラー」は大雑把に2種類に分類できる。
ひとつは構文エラー (syntax error) 、もうひとつは 例外 (exception) 

:::note info
2種類の「エラー」
* 構文エラー (syntax error)
いわゆる文法エラー、Pythonの文法そのものが間違っている場合に発生
構文解析エラー (parsing error) とも言うらしい

* 例外 (exception)
文法自体は正しいが、stack tracebackを残してプログラムが終了する
:::

今回の記事は「例外 (exception)」について記載している。

# 組み込み例外
Python純正の組み込み例外は、次のようなヒエラルキー構造になっている

:::note info
組み込み例外一覧
> 組み込み例外のクラス階層は以下のとおりです:
> ```
> BaseException
>  ├── BaseExceptionGroup
>  ├── GeneratorExit
>  ├── KeyboardInterrupt
>  ├── SystemExit
>  └── Exception
>       ├── ArithmeticError
>       │    ├── FloatingPointError
>       │    ├── OverflowError
>       │    └── ZeroDivisionError
>       ├── AssertionError
>       ├── AttributeError
>       ├── BufferError
>       ├── EOFError
>       ├── ExceptionGroup [BaseExceptionGroup]
>       ├── ImportError
>       │    └── ModuleNotFoundError
>       ├── LookupError
>       │    ├── IndexError
>       │    └── KeyError
>       ├── MemoryError
>       ├── NameError
>       │    └── UnboundLocalError
>       ├── OSError
>       │    ├── BlockingIOError
>       │    ├── ChildProcessError
>       │    ├── ConnectionError
>       │    │    ├── BrokenPipeError
>       │    │    ├── ConnectionAbortedError
>       │    │    ├── ConnectionRefusedError
>       │    │    └── ConnectionResetError
>       │    ├── FileExistsError
>       │    ├── FileNotFoundError
>       │    ├── InterruptedError
>       │    ├── IsADirectoryError
>       │    ├── NotADirectoryError
>       │    ├── PermissionError
>       │    ├── ProcessLookupError
>       │    └── TimeoutError
>       ├── ReferenceError
>       ├── RuntimeError
>       │    ├── NotImplementedError
>       │    └── RecursionError
>       ├── StopAsyncIteration
>       ├── StopIteration
>       ├── SyntaxError
>       │    └── IndentationError
>       │         └── TabError
>       ├── SystemError
>       ├── TypeError
>       ├── ValueError
>       │    └── UnicodeError
>       │         ├── UnicodeDecodeError
>       │         ├── UnicodeEncodeError
>       │         └── UnicodeTranslateError
>       └── Warning
>            ├── BytesWarning
>            ├── DeprecationWarning
>            ├── EncodingWarning
>            ├── FutureWarning
>            ├── ImportWarning
>            ├── PendingDeprecationWarning
>            ├── ResourceWarning
>            ├── RuntimeWarning
>            ├── SyntaxWarning
>            ├── UnicodeWarning
>            └── UserWarning
> ```
> 
> Python公式( https://docs.python.org/ja/3/library/exceptions.html#exception-hierarchy )から引用
:::


# 例外を回避したい(`try-except`文)
本来であればstack tracebackを見てなぜ例外が発生したのか？どうすれば例外を回避できるか？を本質的に考えるべきである。
が、戦略的撤退をすることも時として必要である。その場合`try-except`文を使用する。

:::note info
`try-except`文
```python
try:
    # 例外が起こりそうな処理
except:
    # 例外が起きたときの処理
```
:::

まずは`try`文を実行する。
何かしらの例外が発生したら`try`文の処理を中断、`except`文の処理を開始する。この時tracebackは出力されない。
`try`文で例外が発生しなければ`except`文の処理は行われない。

必ずしもすべてのエラーから逃げる必要がない場合は、下記のように`except`文に避けたい例外だけを列挙すればOK

:::note info
特定の例外だけを明記してよけたい場合
```python
try:
    # 例外が起こりそうな処理
except (RuntimeError, TypeError, NameError) as e:
    # 例外が起きたときの処理
```
:::

`as e`は書かなくても良いが、書いておくと色々遊べる（後述）

上位階層の例外名を書くと、下位の例外もまとめて処理することができる。
例えば`ArithmeticError`は下位に`FloatingPointError,OverflowError,ZeroDivisionError`を持つのでこれらもまとめて`except`文で捕まえることができる。

```:組み込み例外抜粋
ArithmeticError
├── FloatingPointError
├── OverflowError
└── ZeroDivisionError
```




# 例外を起こしたい(`raise`文)
例外を起こすには`raise`文を使う。これで好きな例外を好きなときに好きなだけ発生させることができる。
例外の名前と、そのときに表示するメッセージを書けば良い。

「例外名」は上記の例外クラス階層から選ぶ。（名前を間違えると`NameError`になってしまう）

:::note info
`raise`文
```python
raise 例外名("メッセージ")
```
:::

例えば0で割り算をしたときのエラー`ZeroDivisionError`を発生させたいときは以下のように書けば良い。
```python
raise ZeroDivisionError("ゼロ除算が発生したで")

# 出力
# Traceback (most recent call last):
#   有象無象のメッセージ
#     raise ZeroDivisionError("ゼロ除算が発生したで")
# 
# ZeroDivisionError: ゼロ除算が発生したで
```

ただし`BaseExceptionGroup`や`ExceptionGroup`などは`raise`できない



# 自作の例外を作りたい！
作り方は簡単で、たった一行でオリジナルの例外を作成できる。

:::note info
`Exception`クラスを継承して作成する。例外名を自分で設定できる。
```python
class 例外名(Exception): ...
```
:::

`Exception`部分は別の例外クラスを指定することも可能だが、公式には「`Exception`クラスから派生させるべきである」と記載されています。
> exception Exception
> システム終了以外の全ての組み込み例外はこのクラスから派生しています。全てのユーザ定義例外もこのクラスから派生させるべきです。
> 
> Python公式( https://docs.python.org/ja/3/library/exceptions.html#Exception )から引用

あとは`if`文などで適当に導火線を設定し、オリジナルの例外を`raise`で起爆すれば良い。
```python
# 例外の定義
class OriginalError(Exception): ...

# 例外を発生
raise OriginalError("オリジナルのエラー")

# 出力
# Traceback (most recent call last):
#   有象無象のメッセージ
#     raise OriginalError("オリジナルのエラー")
# 
# OriginalError: オリジナルのエラー
```

# その他tips
stack tracebackで遊びたい人向け

## `except`文の`as e`の意味
:::note info
前述の通り、`as e`はなくても良い。しかし受け取っておくことでこんな遊びもできる。
```python
try:
    raise Exception("エラーやで")
except Exception as e:
    print(e)
    print(e.__class__)
    print(e.__class__.__name__)

# 出力
# エラーやで
# <class 'Exception'>
# Exception
```
:::
## tracebackメッセージを短くする
:::note info
`sys`モジュールをimportし、`tracebacklimit`を設定することでstack tracebackの打ち切りラインを設定できる。
デフォルトは`sys.tracebacklimit=1000`
```python
import sys
sys.tracebacklimit=整数
```
:::
`sys.tracebacklimit=0`とすると「どこで発生したか」の情報を一切言わなくなる。
```python
import sys
sys.tracebacklimit=0

raise Exception("エラーやで")

# 出力
# Exception: エラーやで
```

## tracebackメッセージをstrで受け取る
こういうことをしたいときに便利

https://qiita.com/yulily/items/a22f7494c5a9c4b9fa36

:::note info
`traceback`モジュールをimportし、`traceback.format_exc()`を使用するとtracebackメッセージをstrで受け取れる。
引数の`limit`に整数を指定することで`tracebacklimit`と同じように、どこまで遡るかを指定できる。

変数errstrにtracebackメッセージを文字列として受け取ることができる
```python
import traceback
try:
    raise Exception("エラーやで")
except Exception as e:
    errstr = traceback.format_exc(limit=None)
```
:::

# 参考
tracebackをGUI表示

https://qiita.com/yulily/items/a22f7494c5a9c4b9fa36

エラーと例外

https://docs.python.org/ja/3/tutorial/errors.html

組み込み例外

https://docs.python.org/ja/3/library/exceptions.html