Edited at

Python の奇妙なエラーメッセージ —— そのコードは本当に実行されたもの?

More than 1 year has passed since last update.


奇妙なエラーメッセージ、何が起きている?

とあるマシンのとあるディレクトリにて、対話モードの 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 を実行してみます。


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 を作りました。


hello2.py

input("Please input> ")

print(1/0)

ゼロ除算が行なわれる前に、input で入力待ちになります。

この間に hello2.py 自身のソースコードをエディタで書き換えてみることにします。

まずは実行を開始し、入力待ちの状態にします。

$ python3 hello2.py 

Please input>

2行目を適当なテキストに書き換えます。


hello2.py(書き換え後)

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 モジュールのドキュメントが参考になります。