2019/06/03更新

[Python] 例外処理のやり方と、スタックトレース取得の実装方法

このエントリーをはてなブックマークに追加            

こんにちは、@yoheiMuneです。
久しぶりのブログ、書きたいことがいっぱいです。プログラムでエラーが発生した時に重宝されるスタックトレース。Python言語におけるエラーハンドリングの方法と、そこでのスタックトレースの取得方法を、今日はブログに書きたいと思います。



目次




スタックトレースとは

スタックトレースは、プログラムで例外が発生した際に、どのように関数が呼び出されたのか、どこでエラーが発生したのか、を特定できる情報です。不具合発生時の原因究明に、重宝されます。例えば以下のプログラムがあるとします。
def a():
    """b関数を呼び出す"""
    b()

def b():
    """c関数を呼び出す"""
    c()

def c():
    """例外が発生する"""
    char = None
    char.format('hello')  # ここで例外発生

# a関数から呼び出してみる
a()
ここではa() → b() → c()と呼び出され、c()の中で例外が発生します。このコードを実行すると、以下のスタックトレースが表示されます。
$ python3 sample.py 

Traceback (most recent call last):
  File "sample.py", line 14, in <module>
    a()
  File "ssample.py", line 5, in a
    b()
  File "ssample.py", line 8, in b
    c()
  File "ssample.py", line 12, in c
    char.format('hello')
AttributeError: 'NoneType' object has no attribute 'format'
英語がたくさん表示されるのでギョッとしますが、よく読むと、sample.pyの12行目(line 12)で、c()関数in c)にある、char.format('hello')でエラーが発生し、エラー内容は'NoneType' object has no attribute 'format'であることがわかります。

上記のプログラムではtry 〜 exceptを用いたエラーハンドリングをしていないので、Python実行環境が自動的に上記のスタックトレースを表示し、その後プログラムはエラー終了します。ただ、実際に本番で使うプログラムでは勝手にプログラムが終了されると困ることが多く、例えばWebアプリの場合にはユーザーフレンドリーなエラーメッセージを利用者に表示する必要があります。その際には上記のコードではなく、エラーハンドリングを行います。

このブログでは、エラーハンドリングをする際にもスタックトレースを取得して、不具合の原因究明ができるようにしたい、というお話です。



エラーハンドリング(try/except)を追加する

上記のプログラムにエラーハンドリングを追加しましょう。tryexceptを追加します。
def a():
    b()

def b():
    c()

def c():
    char = None
    char.format('hello')

# エラーハンドリングを追加.
try:
    a()
except Exception as e:
    # Exceptionをキャッチし、その内容を表示する.
    print(e)
エラーハンドリングを行なったので、今度はプログラムは正常終了します。エラー処理時にprint(e)と表示し、エラー内容を表示してみました。
$ python3 sample.py 

'NoneType' object has no attribute 'format'   # 「print(e)」の内容
実行してみると、エラーの内容が1行に減ってしまっていることがわかります。エラー原因はわかりますが、エラーがどこで発生したのかは分かりません。今回の小さなプログラムであればエラー個所も検討はつきますが、大きなプログラムになると、全く見当がつかいないこともしばしば。これだと困るので、スタックトレースを取得したいというお話です。



エラーハンドリング時にスタックトレースを取得する

スタックトレースを取得するために、tracebackモジュールを追加で読み込みます。traceback.format_exc()を使ってスタックトレースを取得できます。
import traceback

def a():
    b()

def b():
    c()

def c():
    char = None
    char.format('hello')

try:
    a()
except Exception as e:
    print(traceback.format_exc())
実行すると、以下のように表示されます。
$ python3 sample.py 

Traceback (most recent call last):
  File "sample.py", line 15, in <module>
    a()
  File "sample.py", line 5, in a
    b()
  File "sample.py", line 8, in b
    c()
  File "sample.py", line 12, in c
    char.format('hello')
AttributeError: 'NoneType' object has no attribute 'format'
最初に表示されたスタックトレースと同じ内容を表示することができました。最初と違うのは今回は、print()関数を使い、スタックトレースの出力を自分で制御した点です。自分で表示のコントロールできるので、例えばloggerパッケージを用いてログに出力することもできます。
これができれば、実務としてはめでたしめでたしです。



補足:スタックトレースに少し踏み込む

上記の実装で全く問題ないですが、少しだけ気になって詳細を調べてみました。
上記ではtraceback.format_exc()で整形された文字列でスタックトレースやエラー原因を取得しましたが、sys.exc_info()を呼び出し、個別に取得することも可能です。
import sys
import traceback

# ..省略.. # 

try:
    a()
except Exception as e:
    #  個別にデータを取得.
    type_, value, traceback_ = sys.exc_info()
    print(type_)
    print(value)
    print(traceback_)
以下は実行結果です。
$ python3 sample.py 

# print(type_)
<class 'AttributeError'>

# print(value)
'NoneType' object has no attribute 'format'

# print(traceback_)
<traceback object at 0x7f8f6002c188>
ただ、これだと中身を捉えるのが面倒なので、traceback.format_exception()を使って、内容を整形できます。
print(traceback.format_exception(type_, value, traceback_))
# 実行結果.
$ python3 sample.py 

['Traceback (most recent call last):\n', '  File "sample.py", line 15, in <module>\n    a()\n', '  File "sample.py", line 5, in a\n    b()\n', '  File "sample.py", line 8, in b\n    c()\n', '  File "sample.py", line 12, in c\n    char.format(\'hello\')\n', "AttributeError: 'NoneType' object has no attribute 'format'\n"]
ここでは、スタックトレースを1行ずつ配列で取得できました。
これを使っても良いですが、traceback.format_exc()を使う方が楽でいいですね〜。



最後に

今日は、Pythonにおける例外処理の方法と、スタックトレースの表示についてブログを書きました。エラー処理は、本番環境で不具合が起きた時や問い合わせを受けた時に、その原因を究明するために非常に重要です。原因究明で時間がかかったな〜という時には、スタックトレースを出力する実装を追加してもいいかもしれません。

最後になりますが本ブログでは、Python、フロントエンド、インフラ、サーバー、PHP、Swift、Node.js、Java、Linux、機械学習、などの技術トピックを発信をしていきます。「プログラミングで困ったその時の、解決の糸口に!」そんな目標でブログを書き続けています。ぜひ、本ブログのRSSTwitterをフォローして貰えたら嬉しいです ^ ^

最後までご覧頂きましてありがとうございました!





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。