エラー,例外処理とトレースバックオブジェクト

【 目次 】

例外の処理の構文

例外の処理に関する公式ドキュメントを探してみると。

pythonの例外処理は、細かい違いは存在するが、try構文を使うjava等の他の言語とあまり変わらない。
catchというキーワードがexceptに変更するだけ。
そして、例外が発生しなかった場合の処理をelse節の後に記述できる事。
また、例外を投げる場合はthrowでは無くraiseを使う。

Javaのtry構文を以下に示す。

javaの例外処理

try {
    throw new Exception();
    System.out.println("ここに来る以前に例外が発生すと実行されない");
} catch (IOError e) {
    System.out.println("IOErrorが発生した時の処理");
} catch (Exception e) {
    System.out.println("IOError以外の例外が発生");
}finally {
    System.out.println("最後に必ず実行される。");
}

これに相等するpythonのtry構文は

pythonの例外処理

try:
    raise Exception
    print u"ここに来る以前に例外が発生すと実行されない"
except IOError, e:
    print u"IOErrorが発生した時の処理"
except Exception, e:
    print u"IOError以外の例外が発生"
else:
    print u"例外が発生しなかった場合の処理"
finally:
    print u"最後に必ず実行される。"

pythonにはJavaには無いelse節がサポートされている。
上記のコードを比較すれば、try構文の意味は一目瞭然、特に説明の必要は無いだろう。

一応、try構文をご存じない方のために他のサイトの参考記事を以下に。

except節かfinally節のどちらかは必ず必要だが、elseはあっても無くても良い。
else節は全ての except 節よりも後ろに置かなければならない。

except節の記述方法のいろいろ

except節の記述方法にはいろいろとバリエションがある。

except:

exceptのみを記述した場合は「ワイルドカードの except 節」と呼び、どんな例外もキャッチしてしまう。

except 例外型:

exceptの後に例外型のみを記述した場合は、その例外型のエラーのみをキャッチできるが例外の原因となった情報を保持する例外型のインスタンスを取得する事ができない。

except 例外型, 例外型のインスタンス:

その例外型のエラーのみをキャッチし、例外のインスタンスを取得する。
「例外のインスタンス」の部分には、例外のインスタンスを受け取るための変数名を記述する。
この構文は古い構文で推奨されない
かわりに以下の構文を使う事が推奨されている。

except 例外型 as 例外型のインスタンス:

モダンなPythonでは、例外型のインスタンスを取得にはasを使う。

以下に、例外型のインスタンスを取得してインスタンスのargs属性を表示する例を示す。

try:
    raise Exception("xxx","yyy","zzz")
except Exception as e:
    print e.args

実行結果

('xxx', 'yyy', 'zzz')
except (RuntimeError, TypeError, NameError):

複数の例外を同じexcept節で処理するには、例外型を上記のようにタプルで記述する。
タプルを示すカッコを省略する事はできない。

except (RuntimeError, TypeError, NameError) ,e:

例外型のインスタンスも取得する。(古い構文)

except (RuntimeError, TypeError, NameError) as e:

例外型のインスタンスも取得するモダンなPythonの構文。

例外の情報を取得する

現在処理中の例外に関する大切な3つの情報はsysモジュールのexc_info関数により取得できる。

sys.exc_info関数はtype,value,tracebackの3つの要素を持つタプル値を返す。

type
処理中の例外の型(クラスオブジェクト)
value
例外パラメータ(処理中の例外の型のインスタンス)
traceback
トレースバックオブジェクト

これらの値はraise文において3つの引数を指定する場合の値になるようだ。

以下はsys.exc_info関数によりエラー情報を取得する例である。

import sys

print sys.exc_info()
try:
    raise IOError("An error occured.cause is xxx.")
except:
    exc_type, exc_value, exc_traceback = sys.exc_info()
    print exc_type
    print type(exc_value), exc_value
    print exc_traceback
(None, None, None)
<type 'exceptions.IOError'>
<type 'exceptions.IOError'> An error occured.cause is xxx.
<traceback object at 0x0000000002556148>

raise文が実行される前までは、sys.exc_info関数の戻り値であるtype,value,tracebackの3つの要素がすべてNoneであったのが、IOErrorの発生により変化しているのがわかる。

例外の情報はsys.exc_info関数の他にもsys.last_type, sys.last_value, sys.last_tracebackによっても取得できる。
しかしこれは対話セッション中にエラーが発生したときにのみ取得できるようで、通常は使用できないようだ。

使う事は無いと思うが、sysモジュールには他にも例外情報をクリヤーするsys.exc_clear関数も含まれている。

トレースバックオブジェクト - 例外のスタックトレースを示すオブジェクト

sys.exc_info関数により得られる3番目のエラー情報tracebackすなわちトレースバックオブジェクトとは何であろうか?

トレースバックオブジェクトには例外のスタックトレースの状態が保存されている。
ではスタックトレースとは何であろうか?

コールスタックが関数呼び出しの実行フレームのフレームオブジェクトを格納するためのスタックなのに対して、スタックトレースは例外発生時のトレースバックオブジェクトを格納するためのスタックという事ができる。

蛇足ながらスタックとは、

スタックトレースをたどる事により例外のおきた原因を詳細に知る事ができる。

typesモジュールにトレースバックオブジェクトの型としてTracebackTypeが定義されている。

公式ドキュメントによるとトレースバックオブジェクトには以下の属性が格納されている。

属性 説明
tb_frame このレベルのフレームオブジェクト
tb_lasti 最後に実行しようとしたバイトコード中のインストラクションを示すインデックス
tb_lineno 現在の Python ソースコードの行番号
tb_next このオブジェクトの内側 (このレベルから呼び出された) のトレースバックオブジェクト

トレースバックオブジェクトには、現在のソースコードの行番号やその時のフレームオブジェクトが保存されており、 スタックフレームに積まれたトレースバックオブジェクトをtb_next属性によりたどる事により、より深く例外の原因をつきとめる事ができる。
また、tb_frame属性によりその時の実行フレームの状態についても知る事ができる。

以下のコードは
関数func1からfunc2,func3を呼び出してfunc3でErrorが発生,
このエラーをfunc1で捕捉して、トレースバックオブジェクトのtb_next属性により内側のトレースバックオブジェクトをたどって、トレースバックオブジェクトの属性を表示する例である。

# coding: UTF-8

import sys
import inspect

def func1():
    try:
        func2()
    except:
        traceback_object = sys.exc_info()[2]
        while traceback_object:
            print "---------------------"
            print traceback_object.tb_frame
            print traceback_object.tb_lasti
            print traceback_object.tb_lineno
            print traceback_object.tb_next
            print inspect.getframeinfo(traceback_object.tb_frame)

            traceback_object = traceback_object.tb_next

def func2():
    func3()

def func3():
    raise IOError("An error occured.cause is xxx.")

func1()

トレースバックオブジェクトのtb_frame属性により、その時のフレームオブジェクトの情報をinspect.getframeinfo関数を使って表示することにより、更に詳しい情報を得る事ができる。

実行結果

---------------------
<frame object at 0x00000000025CA200>
6
8
<traceback object at 0x00000000025587C8>
Traceback(filename='ソースファイル名(フルパス)', lineno=17, function='func1', code_context=['            print inspect.getframeinfo(traceback_object.tb_frame)\n'], index=0)
---------------------
<frame object at 0x0000000001D801E8>
3
22
<traceback object at 0x0000000002558708>
Traceback(filename='ソースファイル名(フルパス)', lineno=22, function='func2', code_context=['    func3()\n'], index=0)
---------------------
<frame object at 0x0000000002651048>
9
25
None
Traceback(filename='ソースファイル名(フルパス)', lineno=25, function='func3', code_context=['    raise IOError("An error occured.cause is xxx.")\n'], index=0)

inspectモジュールのトレースバックオブジェクトに関する関数

前のサンプルプログラムでは、標準モジュールinspectのgetframeinfo関数を使ってフレームオブジェクトの情報を表示していたが、 inspectモジュールには、他にもスタックトレースのフレームレコードを取得する有益な関数が用意されている。

getinnerframes関数

getinnerframes関数を用いてトレースバックオブジェクトを使って指定したフレームと、その内側の全フレームのフレームレコードを一度に取得する事ができる。

getinnerframes関数を使ってトレースバックオブジェクトよりその内側の全フレームレコードの情報を表示する例を以下に示す。

# coding: UTF-8

import sys
import inspect

def func1():
    try:
        func2()
    except:
        traceback_object = sys.exc_info()[2]
        frame_records = inspect.getinnerframes(traceback_object)
        for frame_object,filename,lineno,function,code_context,index in frame_records:
            print "=================="
            print "filename=",filename
            print "lineno=",lineno
            print "function=",function
            print "code_context=",code_context
            print "index",index

def func2():
    func3()

def func3():
    raise IOError("An error occured.cause is xxx.")

func1()

実行結果

==================
filename= C:\Rd\Python\pleiades-e4.4-python-jre_20140926\pleiades\workspace\Python2Test\src\tmp.py
lineno= 8
function= func1
code_context= ['        func2()\n']
index 0
==================
filename= C:\Rd\Python\pleiades-e4.4-python-jre_20140926\pleiades\workspace\Python2Test\src\tmp.py
lineno= 21
function= func2
code_context= ['    func3()\n']
index 0
==================
filename= C:\Rd\Python\pleiades-e4.4-python-jre_20140926\pleiades\workspace\Python2Test\src\tmp.py
lineno= 24
function= func3
code_context= ['    raise IOError("An error occured.cause is xxx.")\n']
index 0

trace関数

公式ドキュメントよりtrace関数の説明を引用すると

実行中のフレームと処理中の例外が発生したフレームの間のフレームレコードのリストを返します。最初の要素は呼び出し元のフレームレコードで、末尾の要素は例外が発生した位置を示します。

trace関数を使って前項の関数func1の部分を書き直した例を以下に示す。

def func1():
    try:
        func2()
    except:
        frame_records = inspect.trace()
        for frame_object,filename,lineno,function,code_context,index in frame_records:
            print "=================="
            print "filename=",filename
            print "lineno=",lineno
            print "function=",function
            print "code_context=",code_context
            print "index",index

tracebackモジュール

標準モジュールtracebackには、トレースバックオブジェクトよりスタックトレースの情報をフォーマットして表示.出力する有益な関数が用意されている。

以下のコードは、tracebackモジュールのprint_exc関数を利用してスタックトレースの情報を表示する例である。

# coding: UTF-8

import sys
import traceback

def func1():
    try:
        func2()
    except:
        print "Exception in user code:"
        print '-'*60
        traceback.print_exc(file=sys.stdout)
        print '-'*60
def func2():
    func3()

def func3():
    raise IOError("An error occured.cause is xxx.")

func1()

raise

raise文を使って、特定の例外を発生させることができる。

raise文の引数

raise文は省略可能な3つの引数をとる。
これは、sys.exc_info関数で述べたtype,value,traceback、すなわち、例外の型,例外の値,トレースバックオブジェクトに相等する。

引数を1つ指定する。

raise文は通常、引数を1つ指定する。
この引数には例外クラス(Exception を継承したクラス)か、例外クラスのインスタンスを指定する。

例外クラスを指定して

import sys

try:
    raise IOError
except:
    print sys.exc_info()

実行結果

(<type 'exceptions.IOError'>, IOError('An error occured.cause is xxx.',), <traceback object at 0x0000000002546148>)

もしくは、例外クラスのインスタンスを指定して

import sys

try:
    raise IOError("An error occured.cause is xxx.")
except:
    print sys.exc_info()

実行結果

(<type 'exceptions.IOError'>, IOError(), <traceback object at 0x0000000002546188>)

raise文の引数の指定はいろいろとあって...

すべての引数を省略する。

raise文の引数をすべて省略した場合は、現在のスコープで最終的に有効になっている例外を再送出する。
しかしそのような例外が現在のスコープでアクティブでない場合、引数の無いraise文自体が例外の型が指定されていないというエラーになってTypeError例外になってしまう。

これは何を言っているかと言うと
例えば、次のコードを実行すると

import sys

def func0():
    raise IOError("An error occured.cause is xxx.")

def func():
    try:
        func0()
    except:
        print sys.exc_info()
        raise Exception("other excepthon")

try:
    func()
except:
    print sys.exc_info()

関数funcが別の関数func0をコールしてそこでIOErrorが発生。
このエラーfuncで捕捉してfuncが別のエラーExceptionを呼び出し元に投げることでfuncの呼び出し元ではfuncがすげかえた別のエラーExceptionをキャッチすることになる。

実行結果

(<type 'exceptions.IOError'>, IOError('An error occured.cause is xxx.',), <traceback object at 0x0000000002426648>)
(<type 'exceptions.Exception'>, Exception('other excepthon',), <traceback object at 0x0000000002426648>)

ところが、上記のコードを変更して以下のようにfuncでraise文の引数を省略すると、

import sys

def func0():
    raise IOError("An error occured.cause is xxx.")

def func():
    try:
        func0()
    except:
        print sys.exc_info()
        raise
try:
    func()
except:
    print sys.exc_info()

実行結果

(<type 'exceptions.IOError'>, IOError('An error occured.cause is xxx.',), <traceback object at 0x0000000002436648>)
(<type 'exceptions.IOError'>, IOError('An error occured.cause is xxx.',), <traceback object at 0x0000000002436688>)

funcの呼び出し元ではfunc0で発生した例外と同じ例外を受け取る事になる。

そして以下のように、もともと例外が発生していなかって場合にraise文の引数を省略すると

import sys

try:
    raise
except:
    print sys.exc_info()

raise文の引数を省略自体がエラーなので例外型が違うよという事で

実行結果

(<type 'exceptions.TypeError'>, TypeError('exceptions must be old-style classes or derived from BaseException, not NoneType',), <traceback object at 0x00000000025161C8>)

TypeErrorが発生して、例外の型は旧スタイルのクラスかBaseExceptionから派生したクラスでなければダメだよとpythonに怒られてしまうという事になる。

引数を2つ指定する。

引数を2つ指定する場合には、最初の引数に例外の型,2番目の引数に例外の値を指定する。

import sys

try:
    raise IOError,IOError("An error occured.cause is xxx.")
except:
    print sys.exc_info()

しかし、他にもいろいろ引数の指定方法があって、公式ドキュメントによると

最初のオブジェクトがクラスの場合、例外の型になります。第二のオブジェクトは、例外の値を決めるために使われます : 第二のオブジェクトがインスタンスならば、そのインスタンスが例外の値になります。第二のオブジェクトがタプルの場合、クラスのコンストラクタに対する引数リストとして使われます ; None なら、空の引数リストとして扱われ、それ以外の型ならコンストラクタに対する単一の引数として扱われます。このようにしてコンストラクタを呼び出して生成したインスタンスが例外の値になります。

2番目の引数に例外クラスのコンストラクタに対する引数リストをタプルで指定することもできるようだ。

引数を3つ指定する。

引数を3つ指定する場合には引数を2つの場合に加えて、最後の引数にトレースバックオブジェクトを指定する。
しかしトレースバックオブジェクトの型がtypes.TracebackTypeだからといってtypes.TracebackTypeをコンストラクタとしてトレースバックオブジェクトを生成してもエラーとなるようだ。

引数を3つ指定するraise文は、既に例外が発生している場合にその例外のトレースバックオブジェクトを取得して、例外を再送出する場合に使われるようだ。

import sys

def func0():
    raise IOError("An error occured.cause is xxx.")

def func():
    try:
        func0()
    except:
        tarceback_object = sys.exc_info()[2]
        e = MemoryError("other excepthon")
        raise e.__class__, e , tarceback_object
try:
    func()
except:
    print sys.exc_info()

実行結果

(<type 'exceptions.MemoryError'>, MemoryError('other excepthon',), <traceback object at 0x00000000024966C8>)

定義済みの例外

例外の種類は?


BaseExceptionは全ての組み込み例外のルートクラスとなっていて、argsという属性に例外クラスのコンストラクタの引数がタプル値で格納されている。

Exceptionに引数を複数指定してargs属性の値を調べてみると

try:
    raise Exception("arg1","arg2","arg3")
except BaseException as e:
    print e.args

実行結果

('arg1', 'arg2', 'arg3')

確かに引数の値がタプルで格納されているのがわかる。

しかし、不思議な事にIOErrorの場合に引数を3個指定すると最後の引数の値がなぜかしらarg属性に格納されない。

    raise IOError("arg1","arg2","arg3")

実行結果

('arg1', 'arg2')

IOErrorの場合は引数に対して特別な処理を施しているようでeclise+PyDevのデバッガーで例外の値eを調べてみるとarg3はfilename属性に割り当てられている。

IOErrorの例外の値

pyhton2には、例外クラスのインスタンスにmessage属性が定義されていて例外のメッセージが格納されていたようだが、python3ではこのmessage属性は削除されている。


必要なら、例外を送出する前にインスタンス化して、args属性を変更したり,任意の属性を追加したりできる。

def func(): 
    try:
        raise Exception("arg1","arg2","arg3")
    except BaseException as e:
        print e.args
        e.args=e.args+("add arg1","add arg2")
        e.xxx="add attribute"
        raise e

try:
    func()
except Exception as e:
    print e.args
    print e.xxx

実行結果

('arg1', 'arg2', 'arg3')
('arg1', 'arg2', 'arg3', 'add arg1', 'add arg2')
add attribute

例外クラスのargs属性は__getitem__属性と結び付けられているようで、例外クラスのコンストラクタの引数は例外クラスのインスタンスの配列要素のようにアクセスできる。

try:
    raise Exception("arg1","arg2","arg3")
except BaseException as e:
    for i in xrange(len(e.args)):
        print e[i]
    arg1,arg2,arg3=e
    print arg1,arg2,arg3

実行結果

arg1
arg2
arg3
arg1 arg2 arg3

例外クラスIOErrorの継承関係についても調べてみた。

def print_classtree(cls, indent=0):
    print ' ' * indent, cls.__name__
    for base_cls in cls.__bases__:
        print_classtree(base_cls, indent + 2)

print_classtree(IOError) 

実行結果

 IOError
   EnvironmentError
     StandardError
       Exception
         BaseException
           object

確かに、BaseExceptionから派生しているのが確認できる。

Pythonのエラーメッセージの意味は?

ユーザ定義の例外

「ユーザ定義の例外」を定義する事もできる。
先にBaseExceptionは全ての組み込み例外のルートクラスとなっていると述べたが、ユーザ定義の例外はBaseExceptionからでは無く、ExceptionクラスかExceptionから派生したクラスを継承することが推奨されている。
また、 例外のクラス名は“Error” で終わる名前を付けるのが良いようだ。

ユーザ定義の例外の実装例は以下の公式ドキュメントのサンプルコードをみれば充分であろう。

例外に関するその他の話題

python3における例外処理は

with構文によるクリーンアップ動作

ページのトップへ戻る