61
34

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-19

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

とあるマシンのとあるディレクトリにて、対話モードの 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 モジュールのドキュメントが参考になります。

61
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
34