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
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() を呼ぶ。