Last Updated: 2/6/2024, 5:44:57 AM

# 例外とエラーってなに?

# 1. エラーってなに?

簡単に言えば、エラーとは、よく操作を間違えたときにでてくる、 あの英語の文章がいっぱいでてくるやつです。

lst = [0, 1, 2]
lst[2]  # 2
lst[3]  # IndexError
>>> lst = [0, 1, 2]
>>> lst[2]
2
>>> lst[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> 

しばらく Python をさわっていると例外を 「間違いを教えてくれるため」ではなく 「何かを通知するため」にも使うようになります。 まず「間違いを教えてくれるため」の例外を振り返ります。 「何かを通知するため」に例外を利用する方法については 次のページで考えていきます。

例外は、私たちに Python の使い方が間違っていたことを教えてくれます。 しかし、ただでさえ不慣れな中で、よくわからない 英語の文字がいきなり大量に表示される例外には、物凄い圧迫感があります。

そこで「何かを通知するために」例外を利用する前に、 例外そのものを少し復習していきたいと思います。 よく遭遇する例外について、公式ドキュメントの文章を交えて触れるため、 4つの例外をピックアップしました。

  • SyntaxError
  • IndexError
  • ValueError
  • TypeError

# 1.1. SyntaxError - 書き方を間違えた

文法が間違ったときに発出される例外です。 SyntaxError は try 文を使っても捕まえることができず、 これ以上先には進めません。

# : コロンが無い
def f(x)
    return x * x
>>> def f(x)
  File "stdin", line 1
    def f(x)
           ^
SyntaxError: invalid syntax
>>>  

SyntaxError (opens new window)
パーザが構文エラーに遭遇した場合に送出されます。 この例外は import 文、組み込み関数 exec() や eval() 、 初期化スクリプトの読み込みや標準入力で (対話的な実行時にも) 起こる可能性があります。

このクラスのインスタンスは、例外の詳細に簡単にアクセスできるようにするために、 属性 filename, lineno, offset, text を持ちます。 例外インスタンスに対する str() はメッセージのみを返します。

Python の公式チュートリアルは SyntaxError とそれ以外のエラーを分けています。 これは何故でしょうか? いまいま思いつく違いは SyntaxError だけ構文解析時に投げられる例外で。 他は実行時に投げられる例外だということです。

8. エラーと例外 - Python チュートリアル (opens new window)
エラーには (少なくとも) 二つのはっきり異なる種類があります。 それは 構文エラー (syntax error) と 例外 (exception) です。

# 1.2. IndexError - リストの長さを間違えた

Python の習いたての頃は、 添字の数を 0 から始まるのを忘れて 1, 2, 3 で数えがちです。 何回も間違えました笑

# 添字の範囲外を参照してしまう
lst = [1, 2, 3]
lst[3]
>>> lst = [1, 2, 3]
>>> lst[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>>

IndexError (opens new window)
シーケンスの添字が範囲外の場合に送出されます。 ( スライスのインデクスはシーケンスの範囲に収まるように 暗黙のうちに調整されます; インデクスが整数でない場合、TypeError が送出されます。 )

# 1.3. TypeError - 型を間違えた

関数の実引数に間違えた型の値を渡すと TypeError が返されます。

import math
math.sqrt(2)    # 1.4142135623730951
math.sqrt('2')  # TypeError
>>> import math
>>> math.sqrt(2)
1.4142135623730951
>>> math.sqrt('2')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be real number, not str
>>> 

# 1.4. ValueError - 型はあってるけど値を間違えた

型はあっているけど内容が不適切な場合は ValueError が投げられます。 たとえば str から int にキャストする場合...

int('Hello, world!')
>>> int('Hello, world!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'Hello, world!'
>>> 

ValueError (opens new window)
組み込み演算ないし関数が型は正しいが値が不適切な引数を受け取り、 かつ IndexError のようなより詳細な例外によって記述されていない場合に発生します。
Raised when a built-in operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError. '

# 1.5. まとめ

次に慣れてくると「何かを伝えるため」に例外を利用する前に、 まず「間違いを教えてくれるため」の例外について簡単に復習しました。

# 2. エラーメッセージは怖くない

エラーが表示されるときに出力される文字列を エラーメッセージ (opens new window) と言います。

エラーメッセージがどのような形式で表示されているか、 ということを意識されたことはありますか? 僕はこの記事を書くまで意識したことはありませんでした。 エラーってあんまり見たくないですからね笑

はじめて例外が発生したとき、 よくわからない英語の文章が羅列されて怖い思いをします。 そして、大抵何が書いてあるか理解できません。 読み飛ばします。 おそらく、それは正しい判断です。

わからないことにいつまでも時間を費やすよりも、 先に進んだ方が良いことは往々にしてあります。 エラーを読めって怒られたりするのですが、 最初のうちはまずわからないことだらけで、 どこに着目すればいいかわからないですからね。

しかし、長大に見えるエラーメッセージも、 その構成は大き分ければ次のたった2つしかありません。

$ python sample.py
スタックトレース
エラー
$

エラーメッセージの先頭部分では、 例外が発生した実行コンテキスト (context) を、 スタックのトレースバック (stack traceback) の形式で示しています。 一般には、この部分にはソースコード行をリストしたトレースバックが表示されます。 しかし、標準入力から読み取られたコードは表示されません。
8. エラーと例外 - Python チュートリアル (opens new window)

これをもう1回展開すると次のようになります。 ほとんどがスタックトレースでエラーはたったの1行です。

$ python sample.py
Traceback (most recent call last):

  # 1) スタックトレースが延々と続く
  File ファイルパス, line 行数, in 関数の名前
    エラーが起きたコード
  File ファイルパス, line 行数, in 関数の名前
    エラーが起きたコード

  ... 繰り返し

  File ファイルパス, line 行数, in 関数の名前
    エラーが起きたコード


# 2) エラー本文はそんなに長くない(たいていの場合は...)
エラーの型の名前: エラーの説明
$

エラーメッセージの先頭部分では、 例外が発生した実行コンテキスト (context) を、 スタックのトレースバック (stack traceback) の形式で示しています。 一般には、この部分にはソースコード行をリストしたトレースバックが表示されます。 しかし、標準入力から読み取られたコードは表示されません。
8. エラーと例外 - Python チュートリアル (opens new window)

# 2.1. エラーの型名、説明

ここでのポイントは、投げられる例外はオブジェクトだということです。 そして例外が投げられたときに表示されるのは、 「例外オブジェクトの型の名前」と 「インスタンス化するときに実引数に与えられたオブジェクト」が表示されています。

# 変数に代入できるものはオブジェクト
value_error = ValueError('This is an error message.', 123)

# 型の名前
type(value_error).__name__  # ValueError

# 実引数
value_error.args[0]  # This is an error message.
value_error.args[1]  # 123

raise value_error
>>> raise value_error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: ('This is an error message.', 123)
>>> 

例外は クラスオブジェクト です。 ちなみに昔は、例外は文字列オブジェクトだったらしいのですが、 途中で設計が変わったそうです。

# 2.2. スタックトレース

スタックトレースは、呼び出した関数を遡った履歴です。 とてもわかりやすい説明があったので、 リンク先をご確認いただければと思います。

スタックトレース (stack trace)とは (opens new window)
エラーが発生したときに表示される内容で、 そのエラーが発生するまでの過程(どんな処理がどの順番で呼び出されたかの流れ)を、 ざっくりと表示したもの

# 2.2.1. 関数

「エラーが起きたコード」と「関数の名前」を紐づけていけば スタックトレースを簡単にたどることができそうです。

def h():
    raise Exception

def g():
    h()

def f():
    g()

f()
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in g
  File "<stdin>", line 2, in h
Exception
>>> 

スタックトレースを見ると f のなかで g が呼ばれ、 g のなかで h が呼ばれているのがわかります。

# 2.2.2. 高階関数

しかし、必ずしもそうはならないことを少しだけ頭に留めておいてください。 例えば、関数を引数に取る関数を使うときがそうです。

def h():
    raise Exception

def f(g):
    g()

f(h)
>>> f(h)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in h
Exception
>>> 

f のなかで h は呼び出されていません。 f に実引数として渡された g が呼び出されています。

このようにして関数を引数に取る関数を使われると ほんの少しだけ追いかけづらくなります。 結果として、あんまりエラー見たくないなという気持ちになります笑

関数を引数に取る関数のことを「高階関数」と呼びます。 また高階関数に引数として渡される関数を「コールバック」と呼びます。

スタックトレースとは関係ないのですが、 コールバック関数を多用して可読性が悪化したものを、 「コールバック地獄」と表現されているのを見かけます。

# 2.2.3. 関数閉包

またクロージャ, デコレータを使うときにも似たような問題が起こります。 スタックトレースを読んでも、どこから読み解けばいいかかなりわかりにくくなります。

def f(g):
    def h():
        g()
    return h

@f
def i():
    raise Exception

i()
>>> i()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in h
  File "<stdin>", line 3, in i
Exception
>>> 

ちなみにこれは func.toolswraps を使っても、 表示のされ方は変化しません。

import functools

def f(g):
    @functools.wraps(g)
    def h():
        print('h.__name__        ', h.__name__)
        print('h.__code__.co_code', h.__code__.co_name)
        g()
    return h

@f
def i():
    raise Exception

i()
>>> i()
h.__name__         i <--------- こっちは変化した
h.__code__.co_code h <--------- こっちは変化しない


Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in h
  File "<stdin>", line 3, in i
Exception
>>> 

スタックトレースに表示される「関数オブジェクトの名前」は __name__ 属性ではなく __code__.co_name 属性に代入されているからです。

ちなみに __code__.co_name は readonly attribute 読み込み専用でした。 代入してスタックトレースの表示を変更することはできませんでした、残念。

# 2.3. まとめ

例外が投げられたときには、 スタックトレースとエラーメッセージの2つが表示されています。 一瞬膨大な量の何かが表示されて戸惑ってしまいますが、 実際にはスタックトレースが表示されただけです。

次にスタックトレースの見方を簡単に確認しました。 スタックトレースとはあまり直接関係ありませんが、 高階関数を使うと、ほんの少し追いかけにくいコードになります。

まずコードの可読性を最適化しよう - POSTD (opens new window)

ディベロッパがコードを読んでいる時、 作業を大幅に遅らせる2つの根本的な要因があります。

  1. コードが理解しづらい
  2. コードが追いかけづらい

トレースバックはとっても圧迫感があります。 それでも Python のトレースバックは、他言語のトレースバックに比べれば、良いものであるようです。 以下はウェブアプリケーションフレームワーク Flask の作者 Armin Ronacher のツイートです。

私は Python のトレースバックはとても素晴らしいものだと思う。 Rust にも同じものを組み込みたい。 ...

# 3. おわりに

例外とエラーメッセージについて復習しました。 次は「何かを伝えるために」例外を利用することを try 文を通して見ていきたいと思います。