LoginSignup
17
11

More than 3 years have passed since last update.

Python 3 で標準出力のブロックバッファリングを止める方法

Last updated at Posted at 2019-06-25

Python の標準出力には3つのバッファリングモードがあります。

  • バッファなし。バイナリモードとなり、データが即座に渡されます1
  • 行バッファリング。改行されるたびに、暗黙的にフラッシュします2
  • ブロックバッファリング。バッファが足りなくなるたびに書き出されます3

デフォルトでは、インタラクティブだと行バッファリングとなり、それ以外だとブロックバッファリングとなります4。この記事では、ブロックバッファリングとなっている標準出力を、行バッファリングあるいはバッファリングなしに設定する方法を4つ紹介します。

python -u

python コマンドに -u 引数を与えると、標準入出力及び標準エラー出力がすべてバッファリングなしになります1。ブロックバッファリングを止める最も簡単な方法です。一時的にバッファリングを止めた状態でスクリプトを実行したい場合に適しています。しかし、実行する度に設定する必要があるため、頻繁に使う場合はあまり良い選択肢ではありません。

PYTHONUNBUFFERED 環境変数

PYTHONUNBUFFERED 環境変数に空でない文字列を設定すると、-u オプションを指定したときと同様の効果を得られます5。特定のマシンで起動される Python をすべてバッファリングなしに設定したい場合や、-u 引数を一々設定するのが面倒な場合に便利です。

sys.stdout を置き換える

sys.stdout にバッファリングモードを変更したファイルオブジェクトを格納することで、ブロックバッファリングを止める方法です。

行バッファリングに設定する場合

import io
import sys


def line_buffered_text_io(stream):
    if (not isinstance(stream, io.TextIOWrapper)
            or not hasattr(stream, 'buffer')):
        return stream
    return io.TextIOWrapper(stream.buffer, encoding=stream.encoding,
                            errors=stream.errors, line_buffering=True)


sys.stdout = line_buffered_text_io(sys.stdout)
sys.stderr = line_buffered_text_io(sys.stderr)

sys.stdoutline_buffering を有効にした io.TextIOWrapper で置き換えています。sys.stderr も同様です。

バッファリングなしに設定する場合

import io
import sys


def unbuffered_text_io(stream):
    if (not isinstance(stream, io.TextIOWrapper)
            or not hasattr(stream, 'buffer')):
        return stream
    raw = stream.buffer
    if isinstance(raw, io.BufferedIOBase) and hasattr(raw, 'raw'):
        raw = raw.raw
    return io.TextIOWrapper(raw, encoding=stream.encoding,
                            errors=stream.errors, write_through=True)


sys.stdout = unbuffered_text_io(sys.stdout)
sys.stderr = unbuffered_text_io(sys.stderr)

sys.stdoutwrite_through を有効にした io.TextIOWrapper で置き換えています。ただし、それだけだと下層の io.BufferedWriter によってバッファリングされてしまうので、更に下層にある io.FileIO を取り出して対処しています。

なお、write_through コンストラクタ引数は Python 3.3 以降でないと利用できないため6、それ未満のバージョンで使用する場合は write_through=True を削除してください。

問題点

ライブラリによって置き換え前の sys.stdout が保持されてしまう

ライブラリがインポート時に sys.stdout を保持してしまった場合、後で sys.stdout を置き換えても意味がありません。そのため、ライブラリをインポートする前に置き換えを完了する必要があります。

'isort:skip_file'
import io
import sys


def unbuffered_text_io(stream):
    if (not isinstance(stream, io.TextIOWrapper)
            or not hasattr(stream, 'buffer')):
        return stream
    raw = stream.buffer
    if isinstance(raw, io.BufferedIOBase) and hasattr(raw, 'raw'):
        raw = raw.raw
    return io.TextIOWrapper(raw, encoding=stream.encoding,
                            errors=stream.errors, write_through=True)


sys.stdout = unbuffered_text_io(sys.stdout)
sys.stderr = unbuffered_text_io(sys.stderr)

# sys.stdout を保持するライブラリのインポート
import bottle  # noqa

ただインポートを遅らせただけだと Linter に怒られたり、インポートの自動ソートが行われるため、noqa や isort:skip_file を追記する必要があります。

余談ですが、私は Bottle が sys.stdout.write を保持していることに気づかず1時間以上溶かしました(盲点でした...)。皆さんは同じ轍を踏まないように気をつけてください。

sys.__stdout__

ちなみに、置き換え前のファイルオブジェクトは sys.__stdout__ でいつでも参照できます。そのため、もしライブラリが sys.__stdout__ を参照している場合は、sys.stdout を置き換えても意味がありません(そんなライブラリがあるかどうかは分かりません)。

sys.stdout.reconfigure

Python 3.7 以降では io.TextIOWrapperreconfigure というメソッドが追加されています7。これを使えば、既存のオブジェクトを置き換えずにバッファリングモードを変更することができます。

import sys

sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)

これで行バッファリングになります。インポート順序等を考えずに済むのでラクチンです。

ちなみに、reconfigure には write_through 引数がありますが、有効にしても下層の io.BufferedWriter によってバッファリングされてしまうので意味がありません。

4つの中から状況にあった方法を選んで、バッファリングモードを変更すると良いでしょう。


  1. python(1)-u オプションを参照。 

  2. io.TextIOWrapperline_buffering コンストラクタ引数を参照。 

  3. io.BufferedWriter 

  4. sys.stdout 

  5. PYTHONUNBUFFERED 

  6. io.TextIOWrapper 

  7. io.TextIOWrapper.reconfigure 

17
11
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
17
11