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

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

- **バッファなし。**バイナリモードとなり、データが即座に渡されます[^unbuffered]。
- **行バッファリング。**改行されるたびに、暗黙的にフラッシュします[^line-buffering]。
- **ブロックバッファリング。**バッファが足りなくなるたびに書き出されます[^block-buffering]。

[^unbuffered]: [python(1)](https://linux.die.net/man/1/python) の `-u` オプションを参照。
[^line-buffering]: [io.TextIOWrapper](https://docs.python.org/ja/3/library/io.html?highlight=textio#io.TextIOWrapper) の `line_buffering` コンストラクタ引数を参照。
[^block-buffering]: [io.BufferedWriter](https://docs.python.org/ja/3/library/io.html?highlight=textio#io.BufferedWriter)

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

[^buffer-default]: [sys.stdout](https://docs.python.org/ja/3/library/sys.html?highlight=sys#sys.stdout)

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

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

[^PYTHONUNBUFFERED]: [PYTHONUNBUFFERED](https://docs.python.org/ja/3/using/cmdline.html?highlight=pythonunbuffered#envvar-PYTHONUNBUFFERED)

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

## 行バッファリングに設定する場合
```python
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.stdout` を `line_buffering` を有効にした `io.TextIOWrapper` で置き換えています。`sys.stderr` も同様です。

## バッファリングなしに設定する場合
```python
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` を `write_through` を有効にした `io.TextIOWrapper` で置き換えています。ただし、それだけだと下層の `io.BufferedWriter` によってバッファリングされてしまうので、更に下層にある `io.FileIO` を取り出して対処しています。

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

[^write-through-version]: [io.TextIOWrapper](https://docs.python.org/ja/3/library/io.html?highlight=textio#io.TextIOWrapper)

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

```python
'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` を保持している](https://github.com/bottlepy/bottle/blob/c8179b2/bottle.py#L119)ことに気づかず1時間以上溶かしました（盲点でした...）。皆さんは同じ轍を踏まないように気をつけてください。

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

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

```python
import sys

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

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

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

[^reconfigure]: [io.TextIOWrapper.reconfigure](https://docs.python.org/ja/3/library/io.html?highlight=textio#io.TextIOWrapper.reconfigure)

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