Python

【Python】18.例外処理

今回は例外処理です。
例えば整数を受け付けている関数に対し、文字列を入力してしまうと、そのままだとエラーになってしまいます。
これを処理は続行しつつ、メッセージを表示したり次の処理を続行するために例外処理というのを行います。

try-exceptを使ってエラーを防ぐ

プログラムのエラーエラーメッセージには、構文エラーと例外があります。
構文エラーはそのままエラー。動作できないプログラムです。
これに対して例外というのは、例えば数値を待っているのに文字列を入れられた、などの予期せぬ出来事を言います。

ここで使う主な例外処理は以下になります。(他にもたくさんあります)

ZeroDivisionErrorゼロ除算
NameError定義されていない変数名
AttributeError属性が定義されていない
Exceptionクラスはすべての例外全ての例外

各例外に対処する命令もあります

故意に例外を起こすraise
例外が発生しても処理を行わないpass
例外が発生しなかった場合に処理を行うelse
例外に関わらず処理を行うfinally

まずは実行できる例からです

a = 1
b = 100 / a
print(b)

次に、実行できないエラーとなる例です。

a = 0
b = 100 / a
print(b)
print("これは実行できません")

0で除算はできないのでZeroDivisionErrorとなってしまいます。
これを例外処理を使って実行できるようにしましょう。
例外処理にはtry-except文というコードを使います。
try-exceptは
try(例外の発生しうる処理)
except(例外Aに対する処理)
で成り立ちます。

try:
    a = 0
    b = 100 / a
    print(ansewer)
except ZeroDivisionError as e:
    print(e)
print("これは実行できません")

実行結果
division by zero
これは実行できません

tryした後に例外が発生したのでexceptが実行されたのがわかります。
例外が発生した場合も実行したい処理がある場合、上記の「finally」を使います。
ゼロ除算なので処理はできませんが、その後に「これは実行できません」と処理が続行できたのがわかります。

すこし難しくして、魏軍の軍師が呉軍と戦うコードを書いてみましょう。

gi = ["荀彧", "郭嘉", "司馬懿"]

a = 0
print("呉軍の侵攻!")
print("呉は" + gi[2 / a] + "万の兵で戦った")
print("魏軍は敵を撃退した")

これもゼロ除算なのでZeroDivisionErrorになります。
では、try-exceptを使って例外後の処理でもできるようにしましょう。

gi = ["荀彧", "郭嘉", "司馬懿"]

a = 0
print("呉軍の侵攻!")
try:
    a = 0
    b = 20 / a
    print("呉は" + gi[2 / a] + "万の兵で戦った")
except ZeroDivisionError as a:
    print(a)
print("魏軍は敵を撃退した")

リスト魏を定義。アイテムは荀彧、郭嘉、司馬懿の軍師オールスターですね
変数aを定義。これはゼロ除算にしてしまうので0にします。
呉軍の侵攻と表示
try開始
aは0
bは20/a(中身は0)
出力。呉軍は2/a万の兵で戦った
exceptブロック。ZeroDivisionErrorをaとします。
exceptの結果のaを出力します。
例外に関わらず魏軍は敵を撃退します

実行結果:
呉軍の侵攻!
division by zero
魏軍は敵を撃退した

ゼロ除算でメッセージが表示されても処理を続行し、魏軍は敵を撃退する事ができました。
さてこのエラーメッセージですが、ログなどに出力したいですが一般ユーザーに見えるようにするのは好ましくない事が多いかと思います。
そこで出力結果ではなく標準エラー出力に表示させましょう。

エラーメッセージを標準エラー出力に表示させる

どこでエラーや例外が発生したのかを特定したい場合、スタックトレースという方法を使います。これにはtracebackというモジュールをインポートします。
また実行時エラーに表示させるのはシステム関連なので、sysもインポートします。

import traceback, sys
print("呉軍の侵攻!")
try:
    a = 0
    b = 100 / a
    print(b)
except ZeroDivisionError as a:
    print("0除算です")
    sys.stderr.write(traceback.format_exc())
finally:
    print("魏軍は敵を撃退した")

トレースバックとシステムをインポート
出力「呉軍の侵攻!」
try開始
変数aを定義しますが、ゼロ除算にするため0
変数bは100/aにします
出力b
exceptブロック。ZeroDivisionErrorをaに入れます
ZeroDivisionErrorに引っかかった場合は「0除算です」と出力
sysの中にあるstderr(標準エラー出力)のwrite(書き出し)を実行します。内容はtraceback内のformat_exc()という関数を呼び出します。
try-exceptで例外が発生してもしなくても行う処理をfinallyという命令で行えます。
tryの結果に関わりないようにfinally内で「魏軍は敵を撃退した」と出力します

実行結果
呉軍の侵攻!
0除算です
魏軍は敵を撃退した

標準エラー出力
Traceback (most recent call last):
File “errortest.py”, line 7, in
b = 100 / a
ZeroDivisionError: division by zero

これで標準エラー出力に表示できました。
スタックトレースが表示され、7行目で「division by zero」(0で割り算しようとした事)がわかります。
次に、エラーメッセージでなく任意のメッセージを表示させたい場合はどうしましょう。
これは簡単でsys.stderr.writeに文字列を入れるだけです。

import traceback, sys
print("呉軍の侵攻!")
try:
    a = 0
    b = 100 / a
    print(b)
except ZeroDivisionError as a:
    sys.stderr.write("ゼロ除算をしようとしています")
finally:
    print("魏軍は敵を撃退した")

実行結果:
呉軍の侵攻!
魏軍は敵を撃退した

標準エラー出力
ゼロ除算をしようとしています

開発者向けのメッセージを工夫する事は少ないかも知れませんね。

ネームエラー

これまではZeroDivisionErrorだけでしたが、次は別のエラーを捕まえてみましょう。

try:
    a = 1
    b = 100 / a
    print(c)
except NameError as a:
    print("未定義の名前です")
    print(a)
finally:
    print("なんてことをするんだ")

出力結果:
未定義の名前です
name ‘c’ is not defined
なんてことをするんだ

これはaとbには変数が入っているが、定義されていないcを出力しようとしています。
この場合は未定義の名前を呼ばれているのでNameErrorとなります。

複数の例外をカバーする

ここまでにZeroDivisionErrorとNameErrorが出てきました。
それでは、多段構えでどの例外が発生しても補足できるようにしてみましょう。

print("これはエラーが2回出てきます")
try:
    a = 0
    b = 100 / a
    print(c)

except ZeroDivisionError as e:
    print("それはゼロ除算だろ")
    print(e)
except NameError as e:
    print("そんな変数は無い!")
    print(e)
except Exception as e:
    print("その他のエラーだ!")
    print(e)
finally:
    print("申し訳ございません。つい荒ぶってしまいました。")

aを0のままにした場合は「それはゼロ除算だろ」、
aを変更してゼロ除算でなくした場合は1つめのexceptを通過し、次のexceptで変数cを呼ぼうとするので「そんな変数は無い!」、
その他の場合は2つめも通過し「その他のエラーだ!」がそれぞれ表示されます。
いずれの場合も、1行目のプリントとfinallyのテキストは表示され、プログラム自体はエラーにならない事がわかります。

前回の標準エラー出力と組み合わせ、魏軍が呉に攻め込み、値が違う場合は標準エラーに出力する形にしましょう。

import sys
gi = ["荀彧", "郭嘉", "司馬懿"]

try:
    go = 0
    print("呉軍の侵攻!")
    print("呉軍は" + gi[10] + "と対戦した")
except ZeroDivisionError as e:
    sys.stderr.write("兵数ではない値が入力されました")
except NameError as e:
    sys.stderr.write("その軍師はいません")
finally:
    print("呉軍は兵5000の損害を受けた")

出力結果
呉軍の侵攻!
呉軍は兵5000の損害を受けた

標準エラー
Traceback (most recent call last):
File “Main.py”, line 10, in
print(“呉軍は” + gi[10] + “と対戦した”)
IndexError: list index out of range

呼び出す魏のリストアイテムを0〜2にするとその軍師が呉軍と対戦し、
無いアイテムを呼ぼうとすると軍師が出ず標準エラーにTracebackgが表示されます。

故意に例外を起こす

上の表に戻ると、故意に例外を起こす「raise」という命令があります。
これを使うと、コードが正しくても例外を起こす事ができます。これは例外オブジェクトなどと呼びます。

print("さあ例外を起こすぞ")
try:
    print("最初の命令です")
    raise Exception("これは意図した例外です")
    print("この部分は出力されません")
except Exception as e:
    print("予期せぬエラーが発生しました")
    print(e)
finally:
    print("最後の命令です")

出力結果:
さあ例外を起こすぞ
最初の命令です
予期せぬエラーが発生しました
これは意図した例外です
最後の命令です

処理の順番

先程のコードで、tryの中で発生した例外オブジェクトは先にexceptブロックに渡り、「予期せぬエラーが発生しました」を処理した後にtryブロックに戻って文字出力をします。にexceptブロックに渡った後の処理はしないので、「この部分は出力されません」は処理されません。最後のfinallyは例外の有無に関わりなく出力されます。
例外は伝わっている事がわかります。
では、どういう順番で処理されるか見てみましょう。

def a(b):
    print("2番めです")
    try:
        print("3番めはここです")
        answer = 12 / b
        return c
        print("ここは処理されません")
    except ZeroDivisionError as e:
        print("5番目です")
        raise e
    print("6です")

print("最初に処理されます")
try:
    c = a(0)
    print("7です")
except ZeroDivisionError as e:
    print("最後の8です")

出力結果:
最初に処理されます
2番めです
3番めはここです
5番目です
最後の8です

最初の実行の命令は「最初に処理されます」なので、ここは最初です。
この後関数aの中を処理していきますが、returnが返ったらcはゼロ除算で例外なので、すぐexceptブロックが処理されるので、4番目の「ここは処理されません」は実行されません。

もう一つ例を出します。

def a(b):
    print("3です")
    c = 100 / b
    return d

print("最初に処理されます")
try:
    c = a(0)
    print("ここは処理されません")
except ZeroDivisionError as e:
    print("4です")
    print(e)

出力結果:
最初に処理されます
3です
4です
division by zero

tryの中で関数が呼び出されたら処理は3に移りますが、最後がreturnなので、直後の「ここは処理されません」は処理されません。
ゼロ除算が発生したので次はexceptブロックとなり4が出力され、ZeroDivisionErrorがeに入れられているので「division by zero」の表示になります。

もう少し試して、tryを入れ子にしてみましょう。

def a(b):
    print("関数内で最初に処理されます")
    try:
        print("try内で最初に処理されます")
        c = 100 / b
        return c
        print("return直後なので処理されません")
    except ZeroDivisionError as e:
        print("exceptブロック内の最初の処理です")
        raise e
    print("raiseで例外が発生したのでここは処理されません")

print("最初に処理されます")
try:
    answer = a(0)
    print("try内ですが関数で例外が発生しているのでexceptに渡るため処理されません")
except ZeroDivisionError as e:
    print("exceptブロックで最初に処理されます")

出力結果:
最初に処理されます
関数内で最初に処理されます
try内で最初に処理されます
exceptブロック内の最初の処理です
exceptブロックで最初に処理されます

このように、例外が発生するとexceprに渡されているのがわかります。
ちなみに、finallyを使うと処理されるようにできます。
「raiseで例外が発生したのでここは処理されません」をfinallyに入れると以下のようになります。

def a(b):
    print("関数内で最初に処理されます")
    try:
        print("try内で最初に処理されます")
        c = 100 / b
        return c
        print("return直後なので処理されません")
    except ZeroDivisionError as e:
        print("exceptブロック内の最初の処理です")
        raise e
    finally:
        print("raiseで例外が発生したのでここは処理されません")

print("最初に処理されます")
try:
    answer = a(0)
    print("try内ですが関数で例外が発生しているのでexceptに渡るため処理されません")
except ZeroDivisionError as e:
    print("exceptブロックで最初に処理されます")

実行結果:
最初に処理されます
関数内で最初に処理されます
try内で最初に処理されます
exceptブロック内の最初の処理です
raiseで例外が発生したのでここは処理されません
exceptブロックで最初に処理されます

処理されないはずの「raiseで例外が発生したのでここは処理されません」が表示されています。

処理の順番がわかりましたでしょうか。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です