例外 — 壊れたものへの対処
`try` / `except` で1つの特定のエラーを捕まえる方法、なぜ `except Exception: pass` がほぼ必ず間違いなのか、`else` と `finally`、自分で投げる例外、そして家系から大きさの合ったエラーを選ぶこと。
どうやって学びますか?
解決する問題
前の章の最後の問題では、プログラムが 3 行を正しく処理して、4 行目で止まるところまで進みました。小さくすると、こうなります:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(エラー行の前にも Python はトレースバックの行をいくつか表示しますが、この章では通常、最後の行だけを示しています。)
10 は正しく加算されました。30 は一度も加算されませんでした。そして print(total) は一度も実行されませんでした。
悪い 1 行が、仕事そのものを止めてしまいました。今までの選択肢は 2 つしかありませんでした — データのすべてが完璧であるか、プログラムが止まるかのどちらかです。
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
403 つ目の選択肢があります:問題は起きるままにしておき、それから対応する。
この章を終える頃には
try/exceptで 1 つの特定のエラーを捕まえる- 素の
except:やexcept Exception: passがほぼ必ず間違いである理由を言える as errでエラーを手に取り、そのメッセージを読めるelseとfinallyの働きを言える- 自分でエラーを
raiseし、いつそれをすべきか判断できる - エラーの家系を認識し、大きさの合った 1 つを捕まえる
前提条件: ファイルの読み込みと書き込み.
エラーもまたオブジェクト
as を書くと、エラーがあなたの手の中に入ります:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'今までターミナルで見ていた赤い文字は、これらのオブジェクトそのもので、それを表示するとメッセージが得られます。
実務的な帰結があります:エラー自体が、いちばん良い説明を持っています。 err を表示することは、自分の言葉で「何かがうまくいかなかった」と書くより、ほぼ常に役に立ちます。
正しいエラーを捕まえる
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'except は、名前を挙げた種類だけを捕まえます。ここでは TypeError を挙げていますが、起きたのは ValueError です — つまりこの except ブロックは、なかったも同じです。
これは引っかかるように感じるかもしれませんが、そこが有用な部分です。except の行は、あなたが準備していた間違いを宣言しています — 準備していなかったものは、相変わらずプログラムを止めます。それが正しい挙動です。
だから、これは書くべきではありません:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40答えは正しくても、コードはまだ悪いものです。except Exception: pass はすべてを飲み込みます — 考えた失敗も、考えなかった失敗も。明日 numbers がたまたま辞書になったとき、あるいは中の名前が 1 文字タイプミスしたとき、このプログラムは黙って間違った答えを出します。
ルール:対策を持っているエラーだけを、正確に捕まえること。 そして捕まえたら、何かをする — 最低限、何が起きたかを報告すること。
複数の種類
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionexcept 節はいくつも続けられます。そして最初に一致したものが動きます — 残りは見られることすらありません。
答えが同じときは、一緒に書けます:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ はエラーの種類の名前を返します — ログを書くときに便利です。
エラーには家族関係がある
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
Trueエラーは木のように並んでいて、親を捕まえれば、その子も捕まります:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: gone覚えておくと便利な関係をいくつか:FileNotFoundError と PermissionError はどちらも OSError、KeyError と IndexError はどちらも LookupError、そしてほぼすべてのものは Exception です。
そこからルールが導けます:いちばん小さいエラーを捕まえること。 except Exception はいちばん書きやすく、いちばん役に立ちません。
else と finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Nonetry ブロックの中で何も壊れなかったときに else が動きます。 そうすると try の中身を小さく保てます — 失敗しうる行だけを置いて、成功のあとの作業は else に置くのです。try に多くを入れすぎると、つもりもなかったものまで捕まえてしまいます。
finally はいつでも動きます、何が起きても。return のあとも:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryreturn は値をすでに決めていますが、finally は関数を抜ける前に動きます。掃除はここでやる場所です — ファイルについては with がその仕事をすでにやってくれますが。
自分でエラーを投げる
投げることも、捕まえるほど重要です。
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1raise しなかったら何が起きたでしょうか? None を返す、あるいは 0 を返す — そして間違いは旅を始めます。どこかの合計に紛れ込み、ずっとあとになって、由来のたどれない変な数字として姿を現します。
エラーは間違いが生まれた場所でそれを止めます。 そして理由を、メッセージに書きます。
メッセージを書くときは、問題の値をその中に含めてください。"invalid quantity" と "quantity cannot be negative: -1" — 作業を始められるのは 2 つ目だけです。
エラーは捕まえて、より良いメッセージでもう一度投げることもできます。parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'今回は出力全体を示します。最後の行だけでは、この場合は物語のすべてが分からないからです。(お使いのマシンでは File の行にはファイルのフルパスが入ります。ここでは ... に短縮しています。) Python はトレースバックを 2 つ出しました:1 つ目は int() からの元のエラーで、2 つ目は私たちの新しい ValueError です。真ん中の行 During handling of the above exception, another exception occurred: は、2 つ目のエラーが 1 つ目を処理している最中に起きたことを示しています。情報は失われていません — ただ、except ブロックの中にバグがあるように読めてしまいます。
意図して新しいエラーを投げるときは、そう伝えてください:except ValueError as err: と書き、次に raise ValueError(f"not a number: {text!r}") from err と書きます。そうすると真ん中の行は The above exception was the direct cause of the following exception: に変わります — 「1 つ目が 2 つ目の原因だった」という意味で、それがまさに私たちの言いたいことです。
{text!r} は値を repr 経由で挿入するので、引用符が見えます — not a number: 'ten' と not a number: ten の違いは、値が本当に空文字列だったり、余分な空白 1 個だったりするときに効いてきます。
先に確認するか、あとで謝るか
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedどちらも正しいです。2 つ目の方が Python ではよく使われ、理由があります:1 つ目は 2 回見る — in で 1 回、[...] で 1 回 — そしてその間に何かが変わっていれば、それでも壊れます。
ただし、ふつうに起こることなら if の方が読みやすいでしょう。例外は例外のためのもの — 名前がそう言っています。
完全な例
orders.txt、わざと悪い行を 2 つ入れてあります:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2見る価値のある 4 点です。
parse_line が投げ、read_order が捕まえます。 下の関数は、プログラム全体として何をすべきかなど知りません — この行が使えないことだけを知っています。それについてどうするかを決めるのは、上の関数の仕事です。エラーは下で投げ、上で捕まえる — この分担こそが、例外システムの仕事のすべてです。
行番号はメッセージの中に入っています。 could not convert string to float: 'eight' はどこを見ればいいか教えてくれませんが、line 2: ... は教えてくれます。捕まえた瞬間に知っていて、それから先は知らないことは、メッセージに入れるべきものです。
read_order は決して壊れません。 2 つのものを返します — 使えたものと、使えないものです。ファイルがなくても、形の同じ答えが返るので、main は 2 本の別々の経路を用意する必要はありません。
内側の try ブロックは、ちょうど 1 行だけです。 float(price) と int(quantity) — 失敗しうる部分だけが中に入っています。parts = raw.split(",") は外にあります。壊れるはずがないし、壊れたら知りたいからです。
うまくいかないとき
except があるのに、エラーが捕まえられていない 種類が合っていません。メッセージの最後の行の最初の単語が、エラーの名前です — その名前を挙げるか、その親を捕まえましょう。
except があるのに、プログラムが止まる 失敗している行が try ブロックの中にありません。インデントを確認しましょう。
SyntaxError: expected 'except' or 'finally' block except のない try を書きました。try はひとりでは立てません。
すべて「動いている」のに、答えが間違っている どこかに except Exception: pass があります。探しましょう — 問題はそこにあります。
自分でエラーを投げたのに、メッセージが出ない raise ValueError("...") ではなく raise ValueError と書きました。括弧とメッセージを足しましょう。
finally の中の return はエラーを落としてしまう そのとおりです。だからそこに書くべきではありません。finally は掃除のためだけの場所です。
ステップ 4 / 6 — 予想する
理解度をチェック
except があります。プログラムはどうなるでしょう?
try:
int("twenty")
except TypeError:
print("caught")- A`caught` が表示される
- B何も表示されず、プログラムは静かに終わる
- C`TypeError` で止まる
- Dプログラムは `ValueError` で止まる — `except` はなかったも同じ
ループ全体が try の中に入っています。何が表示されるでしょう?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
try にも except にも return があります。何が表示されるでしょう?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
前の章の summary.py に戻ってください。people.txt を読んでいたあのファイルです。
では、それを決してクラッシュせず、最後にどの行をなぜ飛ばしたかを報告して終わるようにしましょう。悪い行に対しては、行番号を中に含む ValueError を投げる parse_line(raw, number) を書きましょう。
次に、ファイルをわざと壊します — ある行からはカンマを 1 つ取り、別の行では数字のところに文字を入れ、1 つの行は完全に空にします。例えば:
rafi,12
nadia 30
sami,ten
lina,25空行は不具合ではありません — 前の章と同様、黙って飛ばされます。だからプログラムは最後まで実行し、2 件の不具合を報告するはずです。メッセージを完全な例と同じように書けば (expected 2 fields, got ... と line {number}: {err})、出力の末尾はちょうどこのようになります:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'そして 4 つの実験です:
except ValueErrorをexcept Exception: passに置き換えます。出力は何で、なぜそれがより危険でしょう?parse_lineのメッセージから行番号を外します。悪い行を見つけるのに、今ではどれくらいかかりますか?tryブロックを大きくします — ループの本体をまるごと中に入れます。今度は悪い行があると何が起きるでしょう?- 各行のあとに何かを表示する
finallyを足し、よい行でも悪い行でも同じように動くことを確かめます。
3 つ目の実験が、この章でいちばん有用な習慣を教えてくれます:try ブロックが小さいほど、捕まえ方は正直になる。