Python 2系と3系のsys.stdinの違い
2018-08-28
早速ですが、次のコードを見てください
stdin_loop.py
|
|
これをPython 2.7と3.6でそれぞれ実行してみると、それぞれ次のような結果になります。
Python 2.7での実行結果
hoge
fuga
^D
stdin: hoge
stdin: fuga
Python 3.6での実行結果
hoge
stdin: hoge
fuga
stdin: fuga
^D
hoge
やfuga
は入力で、^D
はCtrl+Dを入力してEOFを送っていることを意味しています。
結果を見ると2系と3系の違いは明らかで、3系は1行ずつ処理されているにも関わらず、2系ではEOFが送られるまで(正確には一定の大きさのバッファがいっぱいになるまで)処理が始まりません。 インタラクティブなプログラムを作ろうと思って、2系で上記のコードを書いてしまうと、処理が進まずに、大変困ったことになります。
解決策
先に解決策を書いておくと、Python 2系においては以下のようにして標準入力を受け取りましょう。
- iter関数を使って、毎回
sys.stdin.readline
を呼び出す
for x in iter(sys.stdin.readline, ''):
...
sys.stdin
をio
モジュールを使ったバージョンで書き換える(後述)- イテレーターを使うのをあきらめる(
raw_input
などを使う?)
なぜこの問題が発生するのか
ここから先はcpythonのコードを徐々に深くまで見ていきます。
Python 2.7にはcpythonの2.7のブランチの現時点でのHEADを、Python 3.6には3.6のブランチの現時点でのHEADをそれぞれ見ていきます。興味のある人だけお付き合いいただければ。
- python/cpython 2.7 https://github.com/python/cpython/tree/491740f116755e220135e596ec802ea3a0f65596
- python/cpython 3.6 https://github.com/python/cpython/tree/4ff38870b1de8a3add5357edf125c2866bc42b54
まずはsys.stdinの定義を見てみる
- Python 2.7: sysmodule.c#L1400
|
|
- Python 3.6: pylifecycle.c#L1259-L1270
|
|
Python 2.7で呼び出しているPyFile_FromFile
関数は、後で少しだけ内部実装を読んでいきますがfile
オブジェクトを作るための関数です。
一方、Python 3.6では、create_stdio
関数を呼び出しています。これはどこに定義されているかというと、すぐその上です。
- Python 3.6: create_stdio() – pylifecycle.c#L1066-L1182
さて、この関数の中身を見てみると、至る所で_PyObject_
系の関数が呼び出されていますね。 この関数の戻り値stream
は、pylifecycle.c#L1151-L1153で作られている、io.TextIOWrapperです。 つまり、Python 2.7と3.6ではsys.stdin
のクラスからして、そもそも違うことがわかります。
さて、この関数の中身はだいたい_PyObject_
を呼び出しているだけなので、Pythonコードでも、それなりに再現することができます。 sys.stdinを作るときのcreate_stdio
関数は次のようなコードで表すことができます。
Python 3.6のstdinをPythonコードで再現してみる
|
|
実行結果は次のようになります。
Python 2.7での実行結果
hoge
stdin: hoge
fuga
stdin: fuga
^D
Python 3.6での実行結果
hoge
stdin: hoge
fuga
stdin: fuga
^D
ご覧の通り、Python 2.7でも意図した通り1行ずつ処理されるようになりました。
ここまでをまとめると次の通りです。
- Python 2系での
sys.stdin
の実体はFileObject
- Python 3系での
sys.stdin
の実体はio.TextIOWrapper
- FileObjectに対するイテレーション(つまり
next(FileObject)
)はEnterキーを押しても次に進まない(EOFを受け取る、もしくはバッファがいっぱいになるまでブロックする) - io.TextIOWrapperに対するイテレーション(つまり
next(io.TextIOWrapper)
)はEnterキーを押すと次に進む
余談: 実はopenも仕様が異なる
普段何気なく使っているopen
関数(組み込み関数の1つです)、実は2系と3系で実体が違うということをご存知ですか?
私はつい最近まで知りませんでした。
具体的には、Python 2系ではfileオブジェクトを作って返しているのに対して、Python 3系ではioモジュールのオブジェクトを返すようになっています。
手軽にそれを実感するには、以下のコードを2系と3系両方で実行してみると良いでしょう。
|
|
- Python 2.7での実行結果:
__builtin__
- Python 3.6での実行結果:
io
next(FileObject)
とnext(io.TextIOWrapper)
の違いを見てみる
では、なぜこのような差が生まれるのか、詳しく実装の差を見ていきましょう。
Python 2.7: next(FileObject)
の概要
2.7の実装はだいぶ単純です。
まずPyFile_Type
の定義を見ればわかる通り、next(FileObject)
が呼び出されると、file_iternext
関数が呼び出されます(fileobject.c#L2344)。
そのなかで、readahead_get_line_skip
関数を呼び出し(fileobject.c#L2299)、さらにそのなかでreadahead
関数(fileobject.c#L2262)を呼び出し、さらにそこからPy_UniversalNewlineFread
関数を呼び出すことで、内部に持っているFILE *
から読み進めようとします。(fileobject.c#L2860)
さて、ここまで来ればお分かりかと思いますが、結局内部的にはbufferedなfread
関数を呼び出しています(fileobject.c#L2875)。 これなら戻ってこないのも納得です。
ちなみに、n
の値はREADAHEAD_BUFSIZE
として定義されている8192
です。
これがなぜ未だに直っていないかというと、これは個人的な印象ですが、sys.stdin
の仕様(というか実体)を変えずに、既存のコードへの影響が少ない対策を取る方法が難しいのではないのかなという気がしています。
というのも、FileObject
はstdin
以外でも多くの場所で使われており、今回の問題を解決しようとすると、iter
の挙動が変わってしまう(もしくはstdin
の時だけiter
の挙動を変えるという微妙なコードを入れる必要が出てしまう)ためです。
ところで、Python 2系のsys.stdin.readline
は改行で正しく動作しますよね。 こちらはどのような実装になっているかというと、内部的にはget_line
関数(fileobject.c#L1426)を呼び出していて、その中では実質getc
関数を連続的に読んで、改行までループを繰り返しています(ちょっとかわいいですね)。
Python 3.6: next(io.TextIOWrapper)
の概要
2.7とは異なり、3.6はちょっと追うのが大変です。まず、io
モジュール以下のクラス階層をざっくりと把握しておく必要があります。
- io.IOBase:
すべてのI/Oクラスの抽象基底クラス(io関係で実装が必要なメソッドが空定義されていたりする)io.TextIOBase:buffer(
BufferedIOBase
インスタンス)をもっている(重要)- io.TextIOWrapper:これ自身がiternext持っている(重要)
io.BufferedIOBase:raw(
FileIO
インスタンス)をもっている(重要)io.RawIOBase
- io.FileIO:readintoがここで隠蔽されている
ここからは入力処理を行っている部分までを説明するのは少々大変なので、この関数へのコールスタックだけ張っておきます。
- textio.c#L2968(textiowrapper_iternext)
- textio.c#L2977(textiowrapper_iternext)
- textio.c#L2037(_textiowrapper_readline)
- textio.c#L1738-L1740(textiowrapper_read_chunk)
- bufferedio.c#L957(_io__Buffered_read1_impl)
- bufferedio.c#L1479(_bufferedreader_raw_read)
- fileio.c#L633(_io_FileIO_readinto_impl)
- fileutils.c#L1353(_Py_read)
ようやく_Py_read
までたどり着けました。
この関数のなかで入力を受け取っているのは、実質fileutils.c#L1386にあるread
関数です。 ちなみに、1行の入力サイズがバッファーを超えてしまうような場合、_textiowrapper_readlineのなかにあるループまで戻って、再度1行の続きを読むことになります。
おわりに
ちょっと長くなりましたが、内部の実装的にこんな感じでPython 2系と3系のnext(sys.stdin)
の挙動は違っている感じです(どこか読み間違えているかもしれません)。 当たり前ですが、Python 2系と3系は内部のコードも大幅に違いますね。
最後に、Python 2系と3系の違いを感じられるiterator関係のコードを2つ貼って終わりにしたいと思います(どちらも最近私がやらかしそうになったやつです)。
その1
yarakashi1.py
|
|
Python 2.7での実行結果
called
called
called
0 hoge
1 hoge
2 hoge
Python 3.6での実行結果
called
0 hoge
called
1 hoge
called
2 hoge
その2
yarakashi2.py
|
|
Python 2.7での実行結果
called
called
called
called
called
called
called
...(以下無限ループ)
Python 3.6での実行結果
called