鈴木たかのりです。今月のPython Monthly Topicsでは、Python 3.
Better error messagesとは
Python 3.
例として、以下のようなリストの閉じカッコ]
)
nums = [1, 3, 2, 5, 4, 8, 10
min(nums)
このコードをPython 3.SntaxError
)min(nums)
を指し示します。このエラーメッセージを見てもmin(nums)
自体には問題がないため、とくに初心者は混乱しやすいと思います。
$ python3.9 example.py File "example.py", line 2 min(nums) ^ SyntaxError: invalid syntax
同様のコードをPython 3.[
)'[' was never closed
となり
$ python3.10 example.py File "example.py", line 1 nums = [1, 3, 2, 5, 4, 8, 10 ^ SyntaxError: '[' was never closed
このようにBetter error messagesによって、エラー発生時のエラーメッセージが
以下では、いくつかのサンプルコードを使用して、Python 3.
また、後半ではBetter error messagesがどのように実現されているかについて解説します。
Better error messagesの例
ここではいくつかBetter error messagesで改善されたエラーメッセージの例を紹介します。同じコードをPython 3.
コロン(:
)を忘れた場合
:
)以下のような、for
文if
文などの末尾のコロン:
)
for num in range(10)
print(num)
Python 3.
$ python3.9 no_colon.py File "no_colon.py", line 1 for num in range(10) ^ SyntaxError: invalid syntax
$ python3.10 no_colon.py File "no_colon.py", line 1 for num in range(10) ^ SyntaxError: expected ':'
==
と=
を間違えた
if
文の条件で==
を=
に間違えたコードを実行します。
if beer = "IPA":
print("I like it")
Python 3.=
ではなくて==
か:=
じゃないですか?」
% python3.9 single_equal.py File "single_equal.py", line 1 if beer = "IPA": ^ SyntaxError: invalid syntax
$ python3.10 single_equal.py File "single_equal.py", line 1 if beer = "IPA": ^^^^^^^^^^^^ SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
インデントを入れ忘れた
if
文などのブロックでインデントを忘れることがあります。
def is_beer(style):
if style in ("IPA", "Hazy", "Pilsner"):
print("This is beer")
Python 3.if
文の後の」
$ python3.9 no_indent.py File "no_indent.py", line 3 print("This is beer") ^ IndentationError: expected an indented block
$ python3.10 no_indent.py File "/Users/takanori/Books/gihyo-python-monthly/source/202212/no_indent.py", line 3 print("This is beer") ^ IndentationError: expected an indented block after 'if' statement on line 2
変数名などのtypo
変数名、関数名などの綴りの入力を間違える、いわゆるtypoはとてもよくあります。以下のように、宣言した変数名を間違えている場合を考えます。
bear = "IPA"
print(beer)
Python 3.beer
という名前は定義されていません」
Python 3.bear
のことですか?」beer
とbear
を間違えていることに気づきやすくなっています。
$ python3.9 typo_var_name.py Traceback (most recent call last): File "typo_var_name.py", line 2, in <module> print(beer) NameError: name 'beer' is not defined
$ python3.10 typo_var_name.py Traceback (most recent call last): File "typo_var_name.py", line 2, in <module> print(beer) NameError: name 'beer' is not defined. Did you mean: 'bear'?
このエラーメッセージは名前が似ている変数、関数などを探して提案しています。そのため、似た名前の変数や関数が存在しない場合には
以下の例ではPythonの対話モードで同じprint(beer)
を実行していますが、bear
変数を定義する前後で出力されるエラーメッセージが変わっていることがわかります。
>>> print(beer) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'beer' is not defined >>> bear = "IPA" # typoしたbear変数を定義 >>> print(beer) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'beer' is not defined. Did you mean: 'bear'?
Better error messagesをどう実現しているか
ここではBetter error messagesがどう実現されいてるかを説明しますが、その前にPythonのパーサーが変更された件について解説します。
LL(1)パーサーとPEGパーサー
PythonのパーサーとはPythonのプログラムを文法的に解析し、AST
LL(1)パーサーは左から順番に読んでいく構文解析器です。(1)は先読みするトークン
これらの問題に対処するために、上記のPEP 617でPEGパーサーの導入が提案されました。PEGパーサーではあいまいな処理は行われず、解析ツリーは必ず1通りとなります。
Python 3.-X oldparser
でLL(1)パーサーが使用できるようになっています。Python 3.
また、LL(1)パーサー、PEGパーサー、左再帰については以下のWikipediaのドキュメントが参考になります。
PythonのPEGパーサーについては、以下の開発者向けガイドに詳しい情報が書いてあります。
Python 3.
with (
CtxManager1(),
CtxManager2(),
):
....
match = "IPA" # ソフトキーワードなので変数に使用可能
match match: # Pilsner, IPA and others
case "Pilsner":
result = "First drink"
case "IPA":
result = "I like it"
case _: # Wildcard
result = "I like most beers"
構造化パターンマッチングの詳細については、以下のPython Monthly Topicsの過去記事も参考にしてください。
Better error messagesの動作(リストの閉じカッコ忘れ)
話をBetter error messagesに戻します。このエラーメッセージの改善も、PEGパーサーによって実現できるようになりました。
最初の例にも出てきた、リストの閉じカッコ]
)
nums = [1, 3, 2, 5, 4, 8, 10
min(nums)
まず、このエラーメッセージの改善は以下のGitHub Issueで対応しています。What's New In Python 3.bpo-42864
というリンクがあり、ここをクリックすると該当するIssueが確認できます。
このIssueでは3つPR
以下のコードraise_
でカッコが閉じていないことを示すエラーを生成しています。
_PyPegen_check_tokenizer_errors(Parser *p) {
〈省略〉
switch (PyTokenizer_Get(p->tok, &start, &end)) {
case ERRORTOKEN:
if (p->tok->level != 0) {
int error_lineno = p->tok->parenlinenostack[p->tok->level-1];
if (current_err_line > error_lineno) {
raise_unclosed_parentheses_error(p);
return -1;
}
}
break;
raise_
の中身は以下のようなコード"'%c' was never closed"
などを渡しています。
static inline void
raise_unclosed_parentheses_error(Parser *p) {
int error_lineno = p->tok->parenlinenostack[p->tok->level-1];
int error_col = p->tok->parencolstack[p->tok->level-1];
RAISE_ERROR_KNOWN_LOCATION(p, PyExc_SyntaxError,
error_lineno, error_col,
"'%c' was never closed",
p->tok->parenstack[p->tok->level-1]);
}
その結果、エラーメッセージとして正しく開きカッコの位置を差し示して、以下のようにエラーメッセージがわかりやすく出力されるようになりました。
$ python3.10 example.py File "example.py", line 1 nums = [1, 3, 2, 5, 4, 8, 10 ^ SyntaxError: '[' was never closed
このPRではテストコードを追加
def test_error_parenthesis(self):
for paren in "([{":
self._check_error(paren + "1 + 2", f"\\{paren}' was never closed")
for paren in ")]}":
self._check_error(paren + "1 + 2", f"unmatched '\\{paren}'")
Better error messagesの動作(属性名のtypo)
もう1つのBetter error messagesの動作例として、以下のようなコードでsplit()
メソッドの綴りを間違えた例を見てみます。
s = "i like ipa"
print(s.sprit())
このコードを実行すると、AttributeErrorのメッセージに間違えたと思われるメソッド名split
)
$ python3.10 attribute_error.py Traceback (most recent call last): File "attribute_error.py", line 3, in <module> l = s.sprit() AttributeError: 'str' object has no attribute 'sprit'. Did you mean: 'split'?
この改善に関するGitHub Issueはbpo-38530
で、以下のものです。
Issueのタイトルの通りAttributeErrorとNameErrorで名前の提案をするというものです。
このIssueにもたくさんのPRがありますが、最初にAttributeErrorに対して名前の提案機能が追加された以下のPRを見てみます。
まず、例外を出力するprint_
関数の中に、名前の提案suggestions
変数)Did you mean
を出力する処理が追加されています
static void
print_exception(PyObject *f, PyObject *value)
{
〈省略〉
PyObject* suggestions = _Py_Offer_Suggestions(value);
if (suggestions) {
// Add a trailer ". Did you mean: (...)?"
err = PyFile_WriteString(". Did you mean: ", f);
_Py_
関数の中身は以下の通りで、ここでは例外がAttributeErrorの場合にのみ、offer_
関数が実行されます
PyObject *_Py_Offer_Suggestions(PyObject *exception) {
PyObject *result = NULL;
assert(!PyErr_Occurred()); // Check that we are not going to clean any existing exception
if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) {
result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
}
assert(!PyErr_Occurred());
return result;
}
offer_
関数の中では、指定された名前をname
変数に、dir
変数に現在のスコープの名前空間を取り出して、calculate_
関数を呼び出します
static PyObject *
offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
PyObject *name = exc->name; // borrowed reference
PyObject *obj = exc->obj; // borrowed reference
〈省略〉
PyObject *dir = PyObject_Dir(obj);
〈省略〉
PyObject *suggestions = calculate_suggestions(dir, name);
Py_DECREF(dir);
return suggestions;
}
そしてcalculate_
関数の中では、dir
変数から1つずつ名前を取り出し、name
変数と似ているかをlevenshtein_
関数で調べます
レーベンシュタイン距離がMAX_
item
を、suggestion
に代入して提案に採用します。そのため、似た名前がない場合はDid you mean
のメッセージは表示されません。なお、コード中の日本語のコメントは筆者が追加したものです。
static inline PyObject *
calculate_suggestions(PyObject *dir,
PyObject *name) {
〈省略〉
Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
PyObject *suggestion = NULL;
for (int i = 0; i < dir_size; ++i) {
PyObject *item = PyList_GET_ITEM(dir, i); // 名前を1つ取り出し
〈省略〉
// nameとitemのレーベンシュタイン距離を計算
Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(item));
// 距離が0またはMAX_DISTANCE(3)より大きければ無視
if (current_distance == 0 || current_distance > MAX_DISTANCE) {
continue;
}
// suggestionが空または距離が他よりも小さければitemを提案として採用
if (!suggestion || current_distance < suggestion_distance) {
suggestion = item;
suggestion_distance = current_distance;
}
}
〈省略〉
return suggestion;
}
このように、エラーメッセージを改善するために、エラーの中身を意味的に解釈していることがわかると思います。
まとめとさらなるエラーメッセージの改善
Python 3.
エラーメッセージの改善は継続しており、Python 3.
これらの改善は、Python 3.
これらの改修の背景や内部的にどのように実装しているかについて、本人によるEuroPython 2022でのトークがあるので、興味がある方はそちらも参照してみてください。
また、このトークについても触れているEuroPythonのレポート記事が、gihyo.