『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

エラーと例外処理

【Python】エラーと例外処理

コンピュータが何らかの理由でプログラムを正しく実行できないと判断して処理を中断してしまうことをエラー (error) とよびます。
 
エラーの実体は BaseException とよばれる基底クラスから派生したサブクラスのインスタンスの一群です。システム終了以外のすべてのエラーは Exception クラスから派生しています。

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

構文エラー(syntax error)

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

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

# PYTHON_SYNTAX_ERROR

# In[1]

print("Python"

'''File "<ipython-input-9-67ae06d08027>", line 3
    print("Python"
                  ^
SyntaxError: unexpected EOF while parsing'''

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

# PYTHON_INDENTATION_ERROR

# In[1]

x = 0

# xに1を9回足す
for j in range(10):
x = x + 1
print(x)

'''File "<ipython-input-3-ba4724ccf86c>", line 5
    x = x + 1
    ^
IndentationError: expected an indented block'''

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

例外 (exception)

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

# PYTHON_EXCEPTION

# In[1]

print(y + 10)

'''NameError    Traceback (most recent call last)
<ipython-input-7-03fdb1e12910> in <module>
      1 
----> 2 print(y + 10)

NameError: name 'y' is not defined'''

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

# In[2]

print("10" + 1)
'''TypeError    Traceback (most recent call last)
<ipython-input-10-524f82b1c299> in <module>
      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

# In[1]

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)
<ipython-input-8-3314ea9f4d0e> in <module>
      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 では除算できません」という文字列を表示して、プログラムを続行させたい場合は以下のようなコードを書きます。

# In[2]

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 例外クラス
たとえば、In[2] にあるように、except の横に ZeroDivisionError を添えるとゼロ除算エラーを補足します。
 
In[2] はゼロ除算には備えていますが、a または b に文字列が入力されると、別の種類の例外が発生してプログラムが中断されてしまいます。

# In[3]

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)
<ipython-input-21-7cf46182195a> in <module>
      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 が発生しています。
 
複数の例外をまとめて受ける場合は、括弧の中に例外クラスをカンマで区切って記述します。

# In[4]

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
# 不適切な値が入力されました
# プログラムを終了します

In[4] はゼロ除算であっても文字列が入力されても「不適切な値が入力されました」と表示するので、ユーザーにとっては何が「不適切」であったのか分かりにくいですね。
 
except を重ねてエラーの種類ごとに異なるメッセージを表示したほうが、よりユーザーフレンドリーなプログラムになります。

# In[5]

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 を使って例外を変数に保存しておくと、発生した例外の種類を確認することができます。

# In[6]

a = 10
b = 0

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

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

# エラーが発生しました!
# エラーの種類: division by zero

finally節

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

# In[7]

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)
<ipython-input-27-90257921d3e2> in <module>
      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

# In[1]

raise NameError("変数が定義されていません")

'''NameError    Traceback (most recent call last)
<ipython-input-4-e73479de48c4> in <module>
----> 1 raise NameError("変数が定義されていません")

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

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

Python 組み込み例外一覧

Python の例外は、BaseException から継承されるサブクラスのインスタンスです。SystemExit, KeyboardInterrupt, GeneratorExit を除くすべての例外クラスは、Exception から継承されています。Python 公式ドキュメントでは、新しい例外クラスを設計する場合には、BaseException ではなく Exception から継承するように推奨しています。
 
以下に Exception から継承されている主要な組み込み例外をまとめておきます。トレースバックについてはエラーの説明文のみを掲載し、他の部分は省略してあります。

NameError

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

# PYTHON_NAMEERROR

# In[1]

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

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

# In[2]

a = math.sqrt(5)

# name 'math' is not defined

TypeError

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

# PYTHON_TYPE_ERROR

# In[1]

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

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

print("10" + "1")
# 101

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

# In[2]

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

# In[1]

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

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

 

KeyError

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

# PYTHON_KEYERROR

# In[1]

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

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

print(x)
# KeyError: '水曜日'

ImportError

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

# PYTHON_IMPORTERROR

# In[1]

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

ModuleNotFoundError

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

# PYTHON_MODULENOTFOUNDERROR

# In[1]

import nunpy
# ModuleNotFoundError: No module named 'nunpy'

StopIteration

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

# PYTHON_STOPITERATION

# In[1]

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

# In[1]

import math

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

print(x)
# OverflowError: math range error

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

# In[2]

import numpy as np

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

print(x)
# inf

ValueError

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

# PYTHON_VALUEERROR

# In[1]

a = int("Python")

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

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

# In[2]

a = int("100")

print(a)
# 100

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

# In[3]

a = int("100.5")

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

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

AttributeError

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

# PYTHON_ATRIBUTEERROR

# In[1]

x = "Python".uper()

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

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

コメント

  1. あとりえこばと より:

    【お知らせ】
     少しだけ記事をリライトしました。
     Jupyter Notebook 用のコード番号 #In[] を添えました。
    (古い記事にはコード番号がないので、少しずつ修正中です)