標準入出力のバッファリングとパイプでの挙動

Python スクリプトをターミナルで実行するときと、パイプで繋げたときで出力の挙動が変わることがある。これは標準入出力のバッファリングが原因だ。

対話的 vs 非対話的

標準出力(stdout)のバッファリングモードは、出力先が端末かどうかで変わる。

端末に出力(対話的)

行バッファリング。改行文字が出力されるたびに flush される

パイプやファイルに出力(非対話的)

フルバッファリング。バッファ(通常 8KB)が満杯になるか、プログラム終了時に flush される

# test.py
import time

for i in range(10):
    print(f"line {i}")
    time.sleep(1)
# 端末に直接出力: 1 秒ごとに表示される
python test.py

# パイプ経由: 最後にまとめて表示される
python test.py | cat

なぜこうなるのか

これは C 言語の stdio ライブラリの動作に由来する。端末は人間が見ているので即座にフィードバックが必要だが、パイプやファイルはバッファリングで効率を上げるほうが理にかなっている。

Python も内部でこの仕組みを踏襲している。

import sys

# 標準出力が端末かどうかを確認
print(sys.stdout.isatty())  # 端末なら True、パイプなら False

flush で強制的に出力する

パイプ経由でもリアルタイムに出力したい場合は、明示的に flush する。

import sys
import time

for i in range(10):
    print(f"line {i}", flush=True)  # Python 3.3+
    time.sleep(1)

# または
for i in range(10):
    print(f"line {i}")
    sys.stdout.flush()
    time.sleep(1)

-u オプション(unbuffered)

Python を -u オプションで起動すると、標準入出力がバッファなしになる。

# バッファなしモードで実行
python -u test.py | cat

# 環境変数でも設定可能
PYTHONUNBUFFERED=1 python test.py | cat
-u の効果

stdout と stderr がバッファなしになる。パイプ経由でもリアルタイム出力される。

注意点

バッファなしは低速になる可能性がある。大量の出力がある場合は避けたほうがよい。

stderr のバッファリング

標準エラー出力(stderr)はデフォルトでバッファなしだ。エラーメッセージは即座に表示されるべきだからだ。

import sys

# stdout は行バッファリング(端末)またはフルバッファリング(パイプ)
# stderr は常にバッファなし

print("stdout message")      # バッファリングされる可能性
print("stderr message", file=sys.stderr)  # 即座に出力

パイプのバッファリング

パイプ自体にもバッファがある。Linux ではパイプバッファは 64KB(カーネル 2.6.11 以降)だ。

# パイプのバッファサイズを確認
cat /proc/sys/fs/pipe-max-size
# 1048576 (1MB、最大値)

Python の stdout バッファ(〜8KB)

パイプのカーネルバッファ(64KB)

次のプロセスの stdin バッファ

複数のバッファが介在するため、パイプラインでのリアルタイム処理は予想以上に遅延することがある。

stdbuf コマンド

システムレベルでバッファリングを制御するには stdbuf コマンドを使う。

# 行バッファリングに変更
stdbuf -oL python test.py | cat

# バッファなしに変更
stdbuf -o0 python test.py | cat

# -o: stdout, -e: stderr, -i: stdin
# L: 行バッファ, 0: バッファなし, サイズ指定も可

ただし、stdbuf は LD_PRELOAD を使うため、静的リンクされたバイナリには効かない。

実践例:リアルタイムログ処理

# logger.py - ログを生成するスクリプト
import time
import sys

for i in range(100):
    print(f"[{i}] Processing...", flush=True)
    time.sleep(0.1)
# リアルタイムで grep
python logger.py | grep --line-buffered "Processing"

# リアルタイムでタイムスタンプ付与
python logger.py | while IFS= read -r line; do
    echo "$(date +%H:%M:%S) $line"
done

grep --line-buffered はパイプ経由でも行単位で出力するオプションだ。

まとめ

状況stdout バッファ対策
端末出力行バッファ不要(自動 flush)
パイプ / ファイルフルバッファflush=True、-u、stdbuf
stderrバッファなし不要(常に即座に出力)
パイプで遅延が起きたら

print に flush=True を付けるか、python -u で実行する。

パフォーマンスを気にするなら

バッファリングを無効にせず、必要な箇所でのみ flush() を呼ぶ。