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

## 奇妙なエラーメッセージ、何が起きている？
とあるマシンのとあるディレクトリにて、対話モードの Python (CPython) に `eval("1/0")` と入力しました。

```python
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` を実行してみます。

```python:hello.py
print(1/0)
```

```shell-session
$ python3 hello.py
```

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

```python:hello2.py
input("Please input> ")
print(1/0)
```

ゼロ除算が行なわれる前に、`input` で入力待ちになります。
この間に `hello2.py` 自身のソースコードをエディタで書き換えてみることにします。
まずは実行を開始し、入力待ちの状態にします。

```shell-session
$ python3 hello2.py 
Please input> 
```

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

```python:hello2.py（書き換え後）
input("Please input> ")
眠いですか？
```

入力待ちのプロセスに適当な入力を与え、処理を進めます。

```shell-session
$ python3 hello2.py 
Please input> 123[Enter]
```

```python
Traceback (most recent call last):
  File "hello2.py", line 2, in <module>
    眠いですか？
ZeroDivisionError: division by zero
```

`print(1/0)` というコードで起きたはずのゼロ除算エラーですが、Traceback には書き換えた後のテキスト、「眠いですか？」が表示されました。
エラー発生の後にソースコードが読み込まれているのが解ります。


## ソースファイルが無ければエラー箇所のコードは表示されない
では、標準入力や文字列の実行でエラーが起きた場合はどうなるでしょうか。
コードはファイルに保存されていませんから、エラーが起きた後に読むべき物が有りません。
先ほどの `hello.py` を標準入力として Python に与えてみます。

```shell-session
$ python3 < hello.py
```

```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
```

`<stdin>` の一行目であることは示されていますが、実際のエラー箇所のコードは表示されません。
では、コードをコマンドライン引数として与えるとどうでしょう。

```shell-session
$ python3 -c 'print(1/0)'
```

```python
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 関数のドキュメント](https://docs.python.jp/3/library/functions.html?highlight=compile#compile)に、

> filename 引数には、コードの読み出し元のファイルを与えなければなりません; ファイルから読み出されるのでなければ、認識可能な値を渡して下さい (`'<string>'` が一般的に使われます)。

とあります。
ソースファイルが無い場合は、適当にソースコードの由来を示す文字列を「ファイル名」として設定することになっている訳です。
Python はバイトコードの実行でエラーが生じると、ここで指定された「ファイル名」を信用して Traceback を作成します。
`<stdin>` や `<string>` といったファイルは存在しませんから、これらの場合はエラー箇所を示すコードは取得できず、そこには何も表示されません。


## 無いはずのファイルが有ったら？
しかし、`<stdin>` や `<string>` といったファイルは絶対に無いのでしょうか？
有ったらどうなるでしょう。
冒頭で紹介した、とあるマシンのとあるディレクトリには、これらのファイルが用意されています。

```shell-session
$ ls
<stdin>  <string>
$ cat '<stdin>'
そろそろ寝れば？
$ cat '<string>'
もう限界でしょ
```

Python の対話モードへの入力は、標準入力からなされるので、バイトコードに設定される「ファイル名」は `<stdin>` となります。

```python
>>> eval("1/0")
```

さらに、そのコードから `eval` 関数が呼ばれますから、文字列からバイトコードへのコンパイルが行なわれます。
そのバイトコードの「ファイル名」は `<string>` とされます。
いざエラーが起きると、Python はこれらの「ファイル名」を信用し Traceback を作成しますから、用意されたファイルのそれぞれ一行目が表示されてしまいます。

```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    そろそろ寝れば？
  File "<string>", line 1, in <module>
    もう限界でしょ
ZeroDivisionError: division by zero
```

これが奇妙な現象の原因です。
この際、ソースファイルは [`sys.path`](https://docs.python.jp/3/library/sys.html#sys.path) から探されるようなので、`<stdin>` と `<string>` が `sys.path` に含まれるディレクトリにあれば、この現象が起きます。


## 参考
奇妙なエラーメッセージは [Twitter で紹介されて](https://twitter.com/brandon_rhodes/status/843998606113890305)いました。
バイトコードに関しては [`dis` モジュールのドキュメント](https://docs.python.jp/3/library/dis.html)が参考になります。