2018年8月29日

Python 2系と3系のsys.stdinの違い


早速ですが、次のコードを見てください

これをPython 2.7と3.6でそれぞれ実行してみると、それぞれ次のような結果になります。

hogefugaは入力で、^DはCtrl+Dを入力してEOFを送っていることを意味しています。

結果を見ると2系と3系の違いは明らかで、3系は1行ずつ処理されているにも関わらず、2系ではEOFが送られるまで(正確には一定の大きさのバッファがいっぱいになるまで)処理が始まりません。 インタラクティブなプログラムを作ろうと思って、2系で上記のコードを書いてしまうと、処理が進まずに、大変困ったことになります。

解決策

先に解決策を書いておくと、Python 2系においては以下のようにして標準入力を受け取りましょう。

  • iter関数を使って、毎回sys.stdin.readlineを呼び出す
  • sys.stdinioモジュールを使ったバージョンで書き換える(後述)
  • イテレーターを使うのをあきらめる(raw_inputなどを使う?)

なぜこの問題が発生するのか

ここから先はcpythonのコードを徐々に深くまで見ていきます。
Python 2.7にはcpythonの2.7のブランチの現時点でのHEADを、Python 3.6には3.6のブランチの現時点でのHEADをそれぞれ見ていきます。興味のある人だけお付き合いいただければ。

まずは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関数を呼び出しています。これはどこに定義されているかというと、すぐその上です。

さて、この関数の中身を見てみると、至る所で_PyObject_系の関数が呼び出されていますね。 この関数の戻り値streamは、pylifecycle.c#L1151-L1153で作られている、io.TextIOWrapperです。 つまり、Python 2.7と3.6ではsys.stdinのクラスからして、そもそも違うことがわかります。

さて、この関数の中身はだいたい_PyObject_を呼び出しているだけなので、Pythonコードでも、それなりに再現することができます。 sys.stdinを作るときのcreate_stdio関数は次のようなコードで表すことができます。

実行結果は次のようになります。


ご覧の通り、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の仕様(というか実体)を変えずに、既存のコードへの影響が少ない対策を取る方法が難しいのではないのかなという気がしています。
というのも、FileObjectstdin以外でも多くの場所で使われており、今回の問題を解決しようとすると、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がここで隠蔽されている

ここからは入力処理を行っている部分までを説明するのは少々大変なので、この関数へのコールスタックだけ張っておきます。

ようやく_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

  • その2


Leave a Reply

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*