CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 未処理の例外が上がったときの処理をオーバーライドする

今回はだいぶダーティーな手法に関する話。 未処理の例外が上がったときに走るデフォルトの処理をオーバーライドしてしまう方法について。 あらかじめ断っておくと、どうしても必要でない限り、こんなことはやらない方が望ましい。 とはいえ、これによって助けられることもあるかも。

使った環境は次の通り。

$ sw_vers                               
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V                    
Python 3.7.5

もくじ

下準備

下準備として、Python のインタプリタを起動しておく。

$ python

デフォルトの挙動をオーバーライドする

try ~ except で捕捉されない例外があると、次のように例外の詳細とトレースバックが出力される。

>>> raise Exception('Oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

このときの挙動は sys.excepthook でフックされているので、このオブジェクトを上書きすることでオーバーライドできる。 例えば、実用性は皆無だけどただメッセージを出力するだけの処理に置き換えてみよう。

>>> import sys
>>> def myhook(type, value, traceback):
...     print('Hello, World!', file=sys.stderr)
... 
>>> sys.excepthook = myhook

例外を上げてみると、次のようにメッセージが表示されるだけになる。

>>> raise Exception('Oops!')
Hello, World!

関数のシグネチャについて

フックの関数のシグネチャについて、もうちょっと詳しく見てみよう。 以下のようにデバッグ用の関数をフックに指定する。

>>> def debughook(type, value, traceback):
...     print(type, value, traceback, file=sys.stderr)
... 
>>> sys.excepthook = debughook

試しに例外を上げてみると、次のようになった。 例外クラスの型、引数、トレースバックのオブジェクトが渡されるようだ。

>>> raise Exception('Oops!')
<class 'Exception'> Oops! <traceback object at 0x1024a6910>

スレッドを使うときの問題点について

なお、このフックはスレッドを使っているときに有効にならないという問題がある。

実際に試してみよう。 先ほどのデバッグ用のフックが有効な状態で、別のスレッドを起動する。 そして、スレッドの中で例外を上げるように細工してやろう。 すると、次のように普通のトレースバックが表示されてしまう。

>>> import threading
>>> def f():
...     raise Exception('Oops!')
... 
>>> threading.Thread(target=f).start()
>>> Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "<stdin>", line 2, in f
Exception: Oops!

上記のように、スレッドを使った場合にはフックが有効にならない。 この問題は Python 3.8 で追加された API によって解決できる。

$ python -V
Python 3.8.0

Python 3.8 では threading モジュールに excepthook というオブジェクトが追加されている。 このオブジェクトを上書きすることで処理をオーバーライドできるようになった。

>>> def threading_hook(args):
...     print('Hello, World!', args)
... 
>>> threading.excepthook = threading_hook
>>> 
>>> threading.Thread(target=f).start()
Hello, World! _thread.ExceptHookArgs(exc_type=<class 'Exception'>, exc_value=Exception('Oops!'), exc_traceback=<traceback object at 0x1033f4900>, thread=<Thread(Thread-2, started 123145518649344)>)

デフォルトの挙動に戻す

デフォルトのフックへの参照は sys.__excepthook__ にあるため、これを使えば挙動を元に戻せる。 なお、sys.__excepthook__ の方は絶対に変更しないこと。

>>> sys.excepthook = sys.__excepthook__
>>> raise Exception('Oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

試してないけど Jupyter とかでエラーになったときチャットに通知を送る、なんて用途に使えるかもね。