奇妙なエラーメッセージ、何が起きている?
とあるマシンのとあるディレクトリにて、対話モードの Python (CPython) に eval("1/0")
と入力しました。
Python 3.5.3 (default, Jan 19 2017, 14:11:04)
[GCC 6.3.0 20170118] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> eval("1/0")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
そろそろ寝れば?
File "<string>", line 1, in <module>
もう限界でしょ
ZeroDivisionError: division by zero
なるほど、ゼロ除算で実行時エラー、これは予想通りです。
しかし、
「そろそろ寝れば?」「もう限界でしょ」
これは何なのでしょうか?
このマシンの Python は改造されていませんし、標準ライブラリなども同様です。
この現象を起こすために、プログラムは一行たりとも書かれていません。
どのような原因が考えられるでしょうか。
ソースファイルの実行でエラーが発生すると、該当のコードが表示される
奇妙なメッセージが出るマシンから離れて、エラー発生時に表示される Traceback の性質を確認します。
まずは、ゼロ除算を起こすソースファイル hello.py
を実行してみます。
print(1/0)
$ python3 hello.py
Traceback (most recent call last):
File "hello.py", line 1, in <module>
print(1/0)
ZeroDivisionError: division by zero
Traceback には、print(1/0)
という行でエラーが起きたことが示されています。
これが期待される動作ですね。
エラー箇所のコードはエラーが起きた後に取得されている
上のように、Traceback にエラーを起こしたソースコードが表示されるのは便利ですが、Python はソースコードをバイトコードに変換してから実行しますから、実行中はソースコードを保持していないはずです。
どうなっているのでしょうか。
実は、Python は実行時エラーが起きた後に改めてソースコードを読み込んで、該当箇所のコードを得ています。
これを確認するために、hello.py
の上に一行足した hello2.py
を作りました。
input("Please input> ")
print(1/0)
ゼロ除算が行なわれる前に、input
で入力待ちになります。
この間に hello2.py
自身のソースコードをエディタで書き換えてみることにします。
まずは実行を開始し、入力待ちの状態にします。
$ python3 hello2.py
Please input>
2行目を適当なテキストに書き換えます。
input("Please input> ")
眠いですか?
入力待ちのプロセスに適当な入力を与え、処理を進めます。
$ python3 hello2.py
Please input> 123[Enter]
Traceback (most recent call last):
File "hello2.py", line 2, in <module>
眠いですか?
ZeroDivisionError: division by zero
print(1/0)
というコードで起きたはずのゼロ除算エラーですが、Traceback には書き換えた後のテキスト、「眠いですか?」が表示されました。
エラー発生の後にソースコードが読み込まれているのが解ります。
ソースファイルが無ければエラー箇所のコードは表示されない
では、標準入力や文字列の実行でエラーが起きた場合はどうなるでしょうか。
コードはファイルに保存されていませんから、エラーが起きた後に読むべき物が有りません。
先ほどの hello.py
を標準入力として Python に与えてみます。
$ python3 < hello.py
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
<stdin>
の一行目であることは示されていますが、実際のエラー箇所のコードは表示されません。
では、コードをコマンドライン引数として与えるとどうでしょう。
$ python3 -c 'print(1/0)'
Traceback (most recent call last):
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
同じく、<string>
の一行目であることは示されていますが、エラー箇所のコードは表示されません。
<stdin>
であれ <string>
であれ、実際に存在するファイルではありませんから、ソースコードが取得できないのです。
<stdin>
や <string>
とは何か
上の Traceback では File "<stdin>"
や File "<string>"
といった表示がされていましたが、これは何なのでしょうか。
compile 関数のドキュメントに、
filename 引数には、コードの読み出し元のファイルを与えなければなりません; ファイルから読み出されるのでなければ、認識可能な値を渡して下さい (
'<string>'
が一般的に使われます)。
とあります。
ソースファイルが無い場合は、適当にソースコードの由来を示す文字列を「ファイル名」として設定することになっている訳です。
Python はバイトコードの実行でエラーが生じると、ここで指定された「ファイル名」を信用して Traceback を作成します。
<stdin>
や <string>
といったファイルは存在しませんから、これらの場合はエラー箇所を示すコードは取得できず、そこには何も表示されません。
無いはずのファイルが有ったら?
しかし、<stdin>
や <string>
といったファイルは絶対に無いのでしょうか?
有ったらどうなるでしょう。
冒頭で紹介した、とあるマシンのとあるディレクトリには、これらのファイルが用意されています。
$ ls
<stdin> <string>
$ cat '<stdin>'
そろそろ寝れば?
$ cat '<string>'
もう限界でしょ
Python の対話モードへの入力は、標準入力からなされるので、バイトコードに設定される「ファイル名」は <stdin>
となります。
>>> eval("1/0")
さらに、そのコードから eval
関数が呼ばれますから、文字列からバイトコードへのコンパイルが行なわれます。
そのバイトコードの「ファイル名」は <string>
とされます。
いざエラーが起きると、Python はこれらの「ファイル名」を信用し Traceback を作成しますから、用意されたファイルのそれぞれ一行目が表示されてしまいます。
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
そろそろ寝れば?
File "<string>", line 1, in <module>
もう限界でしょ
ZeroDivisionError: division by zero
これが奇妙な現象の原因です。
この際、ソースファイルは sys.path
から探されるようなので、<stdin>
と <string>
が sys.path
に含まれるディレクトリにあれば、この現象が起きます。
参考
奇妙なエラーメッセージは Twitter で紹介されていました。
バイトコードに関しては dis
モジュールのドキュメントが参考になります。