Pythonの組み込み例外の木構造を見てみる

# はじめに

自モジュール用のカスタム例外を実装する際、Pythonの2系と3系で組み込み例外の木構造がどう違うかに興味を持ちました。そこで各バージョンの木構造の差を調べてみることにしました。

結論としては「そもそも2系と3系という区別ではなく、3系でもバージョンによって例外の木構造が大きく異なる」ということが分かりました。3系で例外の木構造が同じだと思っていると、思わぬトラブルにあうかもしれませんので念の為ご注意ください。

# 今回の調査に使用するコード

```py:print_exc_tree.py
#!/usr/bin/env python

from __future__ import print_function
import platform


def classtree(cls, depth=0):
    if depth == 0:
        prefix = ''
    else:
        prefix = '.' * (depth * 3) + ' '
    if cls.__name__.lower() == 'error':
        print('{0}{1} ({2})'.format(prefix, cls.__name__, cls))
    else:
        print('{0}{1}'.format(prefix, cls.__name__))
    for subcls in sorted(cls.__subclasses__(), key=lambda c: c.__name__):
        classtree(subcls, depth+1)


if __name__ == '__main__':
    print('Python Version: {0}'.format(platform.python_version()))
    print()
    classtree(BaseException)
```

# 2.7.12 V.S. 3.5.2

執筆時点で最新の2強を比較します。基本的にはこの木構造の違いを意識すればOKなはずです。

```shell-session
$ pyenv shell 2.7.12
$ python print_exc_tree.py
Python Version: 2.7.12

BaseException
... Exception
...... Error (<class 'locale.Error'>)
...... StandardError
......... ArithmeticError
............ FloatingPointError
............ OverflowError
............ ZeroDivisionError
......... AssertionError
......... AttributeError
......... BufferError
......... EOFError
......... EnvironmentError
............ IOError
............... ItimerError
............ OSError
......... ImportError
............ ZipImportError
......... LookupError
............ CodecRegistryError
............ IndexError
............ KeyError
......... MemoryError
......... NameError
............ UnboundLocalError
......... ReferenceError
......... RuntimeError
............ NotImplementedError
......... SyntaxError
............ IndentationError
............... TabError
......... SystemError
............ CodecRegistryError
......... TypeError
......... ValueError
............ UnicodeError
............... UnicodeDecodeError
............... UnicodeEncodeError
............... UnicodeTranslateError
...... StopIteration
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ pyenv shell 3.5.2
$ python print_exc_tree.py
Python Version: 3.5.2

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... OSError
......... BlockingIOError
......... ChildProcessError
......... ConnectionError
............ BrokenPipeError
............ ConnectionAbortedError
............ ConnectionRefusedError
............ ConnectionResetError
......... FileExistsError
......... FileNotFoundError
......... InterruptedError
......... IsADirectoryError
......... ItimerError
......... NotADirectoryError
......... PermissionError
......... ProcessLookupError
......... TimeoutError
......... UnsupportedOperation
...... ReferenceError
...... RuntimeError
......... BrokenBarrierError
......... NotImplementedError
......... RecursionError
......... _DeadlockError
...... StopAsyncIteration
...... StopIteration
...... StopTokenizing
...... SubprocessError
......... CalledProcessError
......... TimeoutExpired
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... ResourceWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

`sre_constants.error`が少し気になりますが、これは正規表現が曖昧だったりする場合に使われるようです ([参考](http://stackoverflow.com/questions/12390238/))

```py3
Python 3.5.2 (default, Jul 29 2016, 11:13:25)
[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.31)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import re
>>> re.compile("*kittens*")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/re.py", line 224, in compile
    return _compile(pattern, flags)
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/re.py", line 293, in _compile
    p = sre_compile.compile(pattern, flags)
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/sre_compile.py", line 536, in compile
    p = sre_parse.parse(p, flags)
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/sre_parse.py", line 829, in parse
    p = _parse_sub(source, pattern, 0)
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/sre_parse.py", line 437, in _parse_sub
    itemsappend(_parse(source, state))
  File "/Users/dmiyakawa/.pyenv/versions/3.5.2/lib/python3.5/sre_parse.py", line 638, in _parse
    source.tell() - here + len(this))
sre_constants.error: nothing to repeat at position 0
```

# 古いPython 3系の例外体系

調べていくと、実は3系でもバージョンによってこの木構造が大きく変化してきたことが分かります。
やや冗長な感もありますが、3.0, 3.1, 3.2, 3.3, 3.4について順番に見ていきます。

```shell-session
$ pyenv shell 3.0.1
$ python print_exc_tree.py
Python Version: 3.0.1

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... EnvironmentError
......... IOError
............ BlockingIOError
............ ItimerError
............ UnsupportedOperation
......... OSError
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... ReferenceError
...... RuntimeError
......... NotImplementedError
...... StopIteration
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... error (<class '_thread.error'>)
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ pyenv shell 3.1.5
$ python print_exc_tree.py
Python Version: 3.1.5

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... EnvironmentError
......... IOError
............ BlockingIOError
............ ItimerError
............ UnsupportedOperation
......... OSError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... ReferenceError
...... RuntimeError
......... NotImplementedError
...... StopIteration
...... StopTokenizing
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ pyenv shell 3.2.6
$ python print_exc_tree.py
Python Version: 3.2.6

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... CalledProcessError
...... EOFError
...... EnvironmentError
......... IOError
............ BlockingIOError
............ ItimerError
............ UnsupportedOperation
......... OSError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... PickleError
......... PicklingError
......... UnpicklingError
...... PickleError
......... PicklingError
......... UnpicklingError
...... ReferenceError
...... RuntimeError
......... NotImplementedError
...... StopIteration
...... StopTokenizing
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... ResourceWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... _Stop
...... error (<class 'sre_constants.error'>)
...... error (<class '_thread.error'>)
...... error (<class 'select.error'>)
...... error (<class 'struct.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ pyenv shell 3.3.6
$ python print_exc_tree.py
Python Version: 3.3.6

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... OSError
......... BlockingIOError
......... ChildProcessError
......... ConnectionError
............ BrokenPipeError
............ ConnectionAbortedError
............ ConnectionRefusedError
............ ConnectionResetError
......... FileExistsError
......... FileNotFoundError
......... InterruptedError
......... IsADirectoryError
......... ItimerError
......... NotADirectoryError
......... PermissionError
......... ProcessLookupError
......... TimeoutError
......... UnsupportedOperation
...... ReferenceError
...... RuntimeError
......... NotImplementedError
......... _DeadlockError
...... StopIteration
...... StopTokenizing
...... SubprocessError
......... CalledProcessError
......... TimeoutExpired
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... ResourceWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ pyenv shell 3.4.3
$ python print_exc_tree.py
Python Version: 3.4.3

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... OSError
......... BlockingIOError
......... ChildProcessError
......... ConnectionError
............ BrokenPipeError
............ ConnectionAbortedError
............ ConnectionRefusedError
............ ConnectionResetError
......... FileExistsError
......... FileNotFoundError
......... InterruptedError
......... IsADirectoryError
......... ItimerError
......... NotADirectoryError
......... PermissionError
......... ProcessLookupError
......... TimeoutError
......... UnsupportedOperation
...... ReferenceError
...... RuntimeError
......... BrokenBarrierError
......... NotImplementedError
......... _DeadlockError
...... StopIteration
...... StopTokenizing
...... SubprocessError
......... CalledProcessError
......... TimeoutExpired
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... ResourceWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

細かい点については筆者も調べたばかりですので置いておきますが、ここではとくにPython 3.3で`IOError`がなくなった点、`OSError`周りの継承構造がガラッと変わった点に注目します。現在の2系と3系の間での大きな違いの一つでもあります

# Python 3.3でIOErrorがなくなりOSErrorの整理が行われた件

これは「[PEP 3151 -- Reworking the OS and IO exception hierarchy](https://www.python.org/dev/peps/pep-3151/)」に基づく変更です。詳細は同PEPを読んでいただけると分かりますが、ファイル操作時のエラーが`IOError`と`OSError`で分裂していた、というのが一つ大きな要因のようです。

```
>>> os.remove("fff")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 2] No such file or directory: 'fff'

>>> open("fff")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: 'fff'
```

なお、3.3, 3.4, 3.5にはこのレベルでの大きな差はありません。木構造のレベルでは新しい例外クラスが追加されている (3.3 → 3.4で`BrokenBarrierError`, 3.4 → 3.5で`RecursionError`と`StopAsyncIteration`)くらいに見えます。

ただし、各関数の挙動変更については木構造では分かりませんので、気になる方はマイナーバージョンの差異も含めてChangelog等をしっかり読んで下さい。

# 3.6.2 での実行結果

補足 (2017-08-05): 以前のバージョンでは3.6.0b2 でしたが、 3.6 でも正式なバージョンが出ましたので 3.6.2 とします。文章全体のリバイズは今はしないでおきます。

上記の流れに乗って現在開発中の3.6.2の木構造も調べてみました。

```shell-session
Python Version: 3.6.2

BaseException
... Exception
...... ArithmeticError
......... FloatingPointError
......... OverflowError
......... ZeroDivisionError
...... AssertionError
...... AttributeError
...... BufferError
...... EOFError
...... Error (<class 'locale.Error'>)
...... ImportError
......... ModuleNotFoundError
......... ZipImportError
...... LookupError
......... CodecRegistryError
......... IndexError
......... KeyError
...... MemoryError
...... NameError
......... UnboundLocalError
...... OSError
......... BlockingIOError
......... ChildProcessError
......... ConnectionError
............ BrokenPipeError
............ ConnectionAbortedError
............ ConnectionRefusedError
............ ConnectionResetError
......... FileExistsError
......... FileNotFoundError
......... InterruptedError
......... IsADirectoryError
......... ItimerError
......... NotADirectoryError
......... PermissionError
......... ProcessLookupError
......... TimeoutError
......... UnsupportedOperation
...... ReferenceError
...... RuntimeError
......... BrokenBarrierError
......... NotImplementedError
......... RecursionError
......... _DeadlockError
...... StopAsyncIteration
...... StopIteration
...... StopTokenizing
...... SubprocessError
......... CalledProcessError
......... TimeoutExpired
...... SyntaxError
......... IndentationError
............ TabError
...... SystemError
......... CodecRegistryError
...... TokenError
...... TypeError
...... ValueError
......... UnicodeError
............ UnicodeDecodeError
............ UnicodeEncodeError
............ UnicodeTranslateError
......... UnsupportedOperation
...... Verbose
...... Warning
......... BytesWarning
......... DeprecationWarning
......... FutureWarning
......... ImportWarning
......... PendingDeprecationWarning
......... ResourceWarning
......... RuntimeWarning
......... SyntaxWarning
......... UnicodeWarning
......... UserWarning
...... _OptionError
...... error (<class 'sre_constants.error'>)
... GeneratorExit
... KeyboardInterrupt
... SystemExit
```

```shell-session
$ diff /tmp/352.txt /tmp/362.txt
1c1
< Python Version: 3.5.2
---
> Python Version: 3.6.2
14a15
> ......... ModuleNotFoundError
65a67
> ...... Verbose
```

例外が新たに2つ増えてますが、やはり本質的な変更ではなさそうです。

* `ModuleNotFoundError`
    * `ImportError`のサブクラスで、インポート失敗の理由が「見つからなかった」ことを特定できます
* `Verbose`
    * `sre_parse.Verbose`で、正規表現周りで使用されるクラスのようです。詳細は未調査です。

3.6についての情報はこちらからどうぞ: https://docs.python.org/3.6/whatsnew/3.6.html
(3.6の最新の情報に更新されるため、上述の3.6.2とは限りません！)

補足2 (2017-12-12): 3.6.2と更新時点で最新の3.6.7での差を以下に示します。

```
$ diff 362.txt 367.txt
1c1
< Python Version: 3.6.2
---
> Python Version: 3.6.7
```

# 3.7.1 での実行結果

```shell-session
$ diff 367.txt 371.txt
1c1
< Python Version: 3.6.7
---
> Python Version: 3.7.1
80c80
< ...... error (<class 'sre_constants.error'>)
---
> ...... error (<class 're.error'>)
```

大差はありません。

参考: https://docs.python.org/3.7/whatsnew/3.7.html

# 自分のモジュールで例外体系を作りたい場合

自モジュールでPythonランタイムの例外を直接使用してしまうと、Pythonランタイムのエラー (例えば`RuntimeError`や`OSError`) と自モジュールを区別できなくなります。基本的には自モジュールで基底例外を作成し、そこを起点に例外クラスの木を作るのが常套手段です。これは例外という考え方を導入している他の言語でもしばしば目にする考え方です。

自分で木構造の例外体系を作る場合、Pythonでは基底となる例外クラスとして `Exception` もしくはそのサブクラスを継承するのが推奨されています (http://docs.python.jp/3/library/exceptions.html) 。

> 組み込み例外クラスをサブクラス化して新たな例外を定義できます。新しい例外は、BaseException ではなく Exception かそのサブクラスから派生することをお勧めします。例外の定義について詳しくは、 Python チュートリアルの ユーザー定義例外 の項目にあります。

# 蛇足: Rubyとの比較

別の言語Rubyのことを一言。

『Effective Ruby』によるとカスタム例外を作る場合の基底クラスは `StandardError` が良いとされます。
2系に同名のクラスがありますがPython 3系にはそもそもこの例外クラスはありませんので、`Exception`とそのサブクラスを検討してください。

複数のプログラミング言語を行き来する際には微妙な慣習・ルール・規約の違いにご注意ください。

# 参考

* http://stackoverflow.com/questions/18296653/
    * 『Python Cookbook 3rd Edition』 (原著英語版)
* http://docs.python.jp/3/library/exceptions.html
    * 『Effective Ruby』

# アップデート
## 2016-09-14

* 2.7.12 と 3.5.2 を使用し、構成を少しだけわかりやすくしました。

## 2016-10-24

* PEP 3151 についての説明を追加しました
* 3.0等でも動作するよう、スクリプトを修正しました
* 例外クラスに「`error`」のような不鮮明なものが複数あるケースも考慮し (3.2)、 必要に応じてクラスの文字列表現を表示するようにスクリプトを修正しました

## 2016-10-25

* 3.6.0b2 の例外の木構造について後半にちょこっと言及しました。3.5と比較してあまり大差ないことが分かります

振り返って見てみると、上記の検証コードの分岐では `cls.__name__.lower() == 'error'` よりも `cls.__module__ != 'builtins'` を使った方が親切だったかもしれません。次回大規模修正時にはそうする予定です。

タイトルでは「組み込み例外の木構造」としていますが、表示される木構造には実際には`Exception`のように何らimportを必要としない`builtins`(組み込み)の例外クラスと`io.UnsupportedOperation`のように何らかのモジュールをインポートしないと読み取れない例外クラスが混ざっています。そのため、例えば「うーん、`Verbose`って何」と思って調べる際に

```shell-session
Python 3.6.0b2 (default, Oct 25 2016, 09:04:16)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> help(Verbose)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'Verbose' is not defined
>>> import sre_parse
>>> help(sre_parse.Verbose)
(ヘルプが表示される)
```

と、混乱を引き起こします。

## 2017-08-05

3.6.0b2 ではなく 3.6.2 を採用しました。参考まで 3.7-dev の結果を掲載しました。

## 2018-12-12

 * 3.7-devの結果を3.7.1の結果へ置き換えました。
 * 「`RuntimeError`から例外を継承することも検討候補にいれるべき」という趣旨の1文を入れてましたが削除しました。Pythonランタイムのエラーとエラー処理を分離しづらくなるのでやめた方が良いです。
 * この記事の方法だと木構造になっていない例外（ABCを用いたものなど）は正確には出てこないですね。とはいえある程度指針にはなると思うので記事はそのままにしておきます。
 * 3.8-dev を掲載しようと思いましたが開発中のものはむしろ混乱の元になりそうなのでやめることにしました。この項目を書いている時点では `StopTokenizing` `SubprocessError` が「消える」といった衝撃的な結果が出るのですが、むしろ調査に使ったスクリプトの問題の可能性が高いです。興味があればお手元で試してみてください。

## 2020-03-29

続編は別の記事にしました。

https://qiita.com/amedama/items/e1a605c004d1e2276778

合わせて、上記の「`StopTokenizing` `SubprocessError` が例外一覧から消える」件についても書きました。要は「ランタイム起動時に読み込まれていないので親が子供を知る方法がない」というだけで、実装から消えたわけではないようです。