エラーと例外処理

エラーと例外処理

エラーと例外処理

 コンピュータが何らかの理由でプログラムを正しく実行できないと判断して処理を中断してしまうことをエラー (error) とよびます。

 エラーの実体は BaseException とよばれる基底クラスから派生したサブクラスのインスタンスの一群です。システム終了以外のすべてのエラーは Exception クラスから派生しています。

 エラーをその発生段階によって分類すると全体の見通しがよくなります。以下ではエラーを構文エラー (syntax error) と例外 (exception) の 2 種類に分けて解説します。

構文エラー(syntax error)

 スクリプト言語である Python はコードを逐次機械語に翻訳しながら命令を実行していきますが、誤った構文で書かれた箇所を読み込もうとすると機械語への変換ができないので、エラーを告げて処理を中断します。

 このような種類のエラーを 構文エラー (syntax error) とよびます。ほとんどの場合、構文エラーは単純な記述ミスが原因で発生します。たとえば関数の引数を囲む括弧などを付け忘れると SyntaxError が発生し、問題のある箇所を "^" で指し示してくれます。

# 構文エラーの実例

print("Python"
  File "", line 3
    print("Python"
                  ^
SyntaxError: unexpected EOF while parsing

 正しくインデントされていない行があると、Indentation Error (インデントエラー) が発生します。これも構文エラーの一種です。

# インデントエラーの実例

x = 0

# xに1を9回足す
for j in range(10):
x = x + 1
print(x)
 File "", line 5
    x = x + 1
    ^
IndentationError: expected an indented block

 ほとんどのエディタや IDE には自動インデント機能が備わっているので、IndentationError を目にする機会はそれほど多くないはずです。
 

例外 (exception)

 構文に誤りがなくても、プログラム実行中にエラーが発生することもあります。
 この段階で検出されるエラーを 例外 (exception) とよびます。
 たとえば、定義されていない変数を使って演算を実行しようとすると、NameError が発生します。

# NameErrorの実例

print(y + 10)
NameError                                 Traceback (most recent call last)
 in 
      1 
----> 2 print(y + 10)

NameError: name 'y' is not defined

 文字列 (str) と整数 (int) を足し合わせようとすると、データ型が適合しないので、TypeError が発生します。

# TypeErrorの実例

print("10" + 1)
TypeError                                 Traceback (most recent call last)
 in 
      1 
      2 
----> 3 print("10" + 1)

TypeError: can only concatenate str (not "int") to str

 

例外の補足

 例外は常に致命的であるとは限らないので、あるブロックで例外が発生したときに、エラーを告げてプログラムを中断させる代わりに、別の処理を行なってプログラムを続行させたいケースもよくあります。

try ~ except

 ユーザーに 2 つの数値 a, b を入力してもらって、a/b を計算した後に「プログラムを終了します」と表示するプログラムを考えます。よく知られているように、除算にはゼロ除算とよばれるエラーの危険が常にあるので、次のような例外処理を全く考慮しないコードを書いてしまうと、b に 0 が入力された場合、a/b が実行される 6 行目で例外が送出されてプログラムは中断され、残りの部分は実行されません。

# PYTHON_TRY_EXCEPT_01

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")
a = float(a)
b = float(b)

print("a/b = {}".format(a/b))
print("プログラムを終了します")
数値aを入力してください:10
数値bを入力してください:0

ZeroDivisionError                         Traceback (most recent call last)
 in 
      4 b = float(b)
      5 
----> 6 print("a/b = {}".format(a/b))
      7 print("a*b = {}".format(a*b))

ZeroDivisionError: float division by zero

 a/b の実行時にゼロ除算エラーが発生した場合には「 0 では除算できません」という文字列を表示して、プログラムを続行させたい場合は以下のようなコードを書きます。

# PYTHON_TRY_EXCEPT_02

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")

try:
    a = float(a)
    b = float(b)
    print("a/b = {}".format(a/b))

except ZeroDivisionError:
    print("0では除算できません")

print("プログラムを終了します")
数値aを入力してください:1
数値bを入力してください:0
0では除算できません
プログラムを終了します

 try ~ except で囲まれたブロックで、except の横に記述したエラーが発生した場合には、直ちに except 以下のブロックに制御が移ります (下図)。

 [Python] try except 例外クラス

 たとえば、リスト EXC-2 にあるように、except の横に ZeroDivisionError を添えるとゼロ除算エラーを補足します。

 リスト EXC_02 はゼロ除算には備えていますが、a または b に文字列が入力されると、別の種類の例外が発生してプログラムが中断されてしまいます。

# PYTHON_TRY_EXCEPT_03

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")

try:
    a = float(a)
    b = float(b)
    print("a/b = {}".format(a/b))

except ZeroDivisionError:
    print("0では除算できません")
    
print("プログラムを終了します")
数値aを入力してください:Python
数値bを入力してください:3

ValueError                                Traceback (most recent call last)
 in 
      3 
      4 try:
----> 5     a = float(a)
      6     b = float(b)
      7     print("a/b = {}".format(a/b))

ValueError: could not convert string to float: 'Python'

 float(a) で文字列を浮動小数点数に変換できないために ValueError が発生しています。

 複数の例外をまとめて受ける場合は、括弧の中に例外クラスをカンマで区切って記述します。

# PYTHON_TRY_EXCEPT_04

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")

try:
    a = float(a)
    b = float(b)
    print("a/b = {}".format(a/b))

except (ZeroDivisionError, ValueError):
    print("不適切な値が入力されました")
    
print("プログラムを終了します")
数値aを入力してください:Python
数値bを入力してください:2
不適切な値が入力されました
プログラムを終了します

 リストEXC_04 はゼロ除算であっても文字列が入力されても「不適切な値が入力されました」と表示するので、ユーザーにとっては何が「不適切」であったのか分かりにくいですね。

 except を重ねてエラーの種類ごとに異なるメッセージを表示したほうが、よりユーザーフレンドリーなプログラムになります。

# PYTHON_TRY_EXCEPT_05

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")

try:
    a = float(a)
    b = float(b)
    print("a/b = {}".format(a/b))

except ZeroDivisionError:
    print("0では除算できません")

except ValueError:
    print("ボックスには数値を入力してください")

print("プログラムを終了します")
数値aを入力してください:Python
数値bを入力してください:2
ボックスには数値を入力してください
プログラムを終了します

 

except [例外クラス] as [変数]

 except節で as の後ろに変数を添えて例外を受けることができます:

except [例外クラス] as [変数]

 たとえば例外クラスに Exception を指定すると、すべての例外を補足しますが、as を使って例外を変数に保存しておくと、発生した例外の種類を確認することができます。

# PYTHON_TRY_EXCEPT_06

a = 10
b = 0

try:
    print("a/b = {}".format(a/b))

except Exception as err:
    print("エラーが発生しました!")
    print("エラーの種類: {}".format(err))
エラーが発生しました!
エラーの種類: division by zero

 

finally節

 finally節に書かれたコードは、エラーが発生してもしなくても必ず実行されます。たとえば、ゼロ除算を予測して以下のようなコードを書いたとします。

# PYTHON_TRY_EXCEPT_07

a = input("数値aを入力してください:")
b = input("数値bを入力してください:")

try:
    print(a/b)

except ZeroDivisionError:
    print("0では除算できません")

finally:
    print("プログラムを終了します")
数値aを入力してください:Python
数値bを入力してください:3
プログラムを終了します

TypeError                                 Traceback (most recent call last)
 in 
      3 
      4 try:
----> 5     print(a/b)
      6 
      7 except ZeroDivisionError:

TypeError: unsupported operand type(s) for /: 'str' and 'str'

 ユーザーが文字列を入力したので、5 行目でプログラマーにとっては予想外であった TypeError が発生しましたが、それでも「プログラムを終了します」というメッセージは表示されます。
 

raiseによる例外の送出

 raiseキーワードを使うと例外を意図的に発生させることができます。

# PYTHON_RAISE

raise NameError("変数が定義されていません")
NameError                                 Traceback (most recent call last)
 in 
----> 1 raise NameError("変数が定義されていません")

NameError: 変数が定義されていません

 raise の後ろには例外オブジェクトを添えます (コンストラクタで例外クラスのインスタンスを生成します)。コンストラクタの引数には例外が発生した原因を渡します。
 

Python 組み込み例外一覧

 Python の例外は、BaseException から継承されるサブクラスのインスタンスです。SystemExit, KeyboardInterrupt, GeneratorExit を除くすべての例外クラスは、Exception から継承されています。Python 公式ドキュメントでは、新しい例外クラスを設計する場合には、BaseException ではなく Exception から継承するように推奨しています。

 以下に Exception から継承されている主要な組み込み例外をまとめておきます。トレースバックについてはエラーの説明文のみを掲載し、他の部分は省略してあります。

NameError

 未定義の変数を参照すると NameError を発生します。

# PYTHON_NAMEERROR_01

print(x)
NameError: name 'x' is not defined

 インポートしていないモジュールを使おうとしたり、未定義の関数を呼び出そうとしたときにも NameError が送出されます。

# PYTHON_NAMEERROR_02

a = math.sqrt(5)
name 'math' is not defined

 

TypeError

 定義されていない形でオブジェクトを処理しようとすると TypeError が発生します。たとえば、文字列と数値を加えようとする場合です。

# PYTHON_TYPEERROR_01

print("10" + 1)
can only concatenate str (not "int") to str

 説明にあるように、文字列 (str) は文字列 (str) との間でのみ + 演算子による結合が定義されています:

print("10" + "1")
101

 連続整数を生成する range()関数には整数 (int) しか渡せません。たとえば浮動小数点数 (float) を渡すと TypeError となります。

# PYTHON_TYPEERROR_02

x = 0

for k in range(10.1):
    x += 1

print(x)
TypeError: 'float' object cannot be interpreted as an integer

 

IndexError

 リストやタプルなどのシーケンス型オブジェクトで、範囲を超えるインデックスで要素を参照しようとすると、IndexError が発生します。

# PYTHON_INDEXERROR

# リストを定義
my_list = ["NumPy", "SciPy", "pandas"]

# 4番目の要素を参照
x = my_list[3]
IndexError: list index out of range

 

KeyError

 辞書型オブジェクト (dict) で存在しないキーを参照しようとすると、KeyError を発生します。

# PYTHON_KEYERROR

# 辞書を定義
my_dic = {"日曜日":"晴れ",
          "月曜日":"雨",
          "火曜日":"曇り"}

# 4番目の要素を参照
x = my_dic["水曜日"]

print(x)
KeyError: '水曜日'

 

ImportError

 モジュールを読み込もうとしたときに問題があると、ImportError を送出します。次のコードはモジュールに存在しない関数を呼び出そうとしてエラーとなる実例です。

# PYTHON_IMPORTERROR

from numpy import myfunc
ImportError: cannot import name 'myfunc' from 'numpy'

 

ModuleNotFoundError

 ModuleNotFoundError は ImportError のサブクラスです。
 インポートしようとするモジュール自体が見つからないときに送出されます。

# PYTHON_MODULENOTFOUNDERROR

import nunpy
ModuleNotFoundError: No module named 'nunpy'

 

StopIteration

 イテレータが生成するアイテムがこれ以上ないときに、StopIteration を発生します。

# PYTHON_STOPITERATION

my_list = ["a", "b", "c"]

my_iter = iter(my_list)

for i in range(4):
    print(next(my_iter))
a
b
c
StopIteration:

 

OverflowError

 演算結果が許容範囲を超える大きさになると OverflowError となります。

# PYTHON_OVERFLOWERROR

import math

# e=2.718...の1000乗を計算
x = math.exp(1000)

print(x)
OverflowError: math range error

 ただし、演算結果が表現できない値になったときの処理方法は関数によって異なります。たとえば、NumPy の指数関数 np.exp() は OverflowError の代わりに無限大を表す浮動小数点数 inf を返します:

import numpy as np

# e=2.718...の1000乗を計算
x = np.exp(1000)

print(x)
inf

 

ValueError

 関数が不適切な引数を受け取ると ValueError を発生します。これは受け取ったオブジェクトの型が適切でないという単純な理由だけでは説明できないこともあります。たとえば、int() に文字列 "Python" を渡すと ValueError を発生します。

# PYTHON_VALUEERROR_01

a = int("Python")

print(a)
ValueError: invalid literal for int() with base 10: 'Python'

 しかし、int() に文字列 "100" を渡してもエラーは送出されずに、int 型オブジェクト 100 を返します。

a = int("100")

print(a)
100

 ところが、文字列 "100.5" は ValueError となります。

# PYTHON_VLUEERROR_02

a = int("100.5")

print(a)
ValueError: invalid literal for int() with base 10: '100.5'

 このように同じ型でも受け取った値によって結果が異なるのは、int() が浮動小数点数の端数を処理するだけでなく、文字列で書かれた整数を intオブジェクトに変換するという機能を併せ持つように設計された関数だからです。
 

AttributeError

 オブジェクトに存在しない属性 (変数やメソッド) を参照しようとすると AttributeError を発生します。たとえば、文字列には uper() というメソッドがないので、以下のコードは AttributeError となります。

# PYTHON_ATRIBUTEERROR

x = "Python".uper()

print(x)
AttributeError: 'str' object has no attribute 'uper'

 上のコードは文字列を大文字に変換するメソッド upper() を書き間違えたという例です。