Python のファイル I/O バッファリングの 3 層構造

Python でファイルに write() を呼んでも、データがすぐにディスクに書き込まれるわけではない。実際には 3 つの層でバッファリングが行われている。

3 層のバッファ構造

ファイルに書き込んだデータは、以下の順序で移動する。

Python のバッファ(io モジュール)

C ライブラリのバッファ(stdio)

カーネルのページキャッシュ

ディスク(最終目的地)

それぞれの層がバッファリングを行うため、write() の呼び出しがディスク書き込みを意味するわけではない。

第 1 層:Python のバッファ

Python の open() が返すファイルオブジェクトは、内部にバッファを持っている。

# バッファリングモードを指定できる
f = open("data.txt", "w", buffering=1)      # 行バッファリング
f = open("data.txt", "w", buffering=8192)   # 8KB バッファ
f = open("data.txt", "w", buffering=0)      # バッファなし(バイナリのみ)
テキストモードのデフォルト

行バッファリング(対話的)または固定サイズバッファ(非対話的)。

バイナリモードのデフォルト

io.DEFAULT_BUFFER_SIZE(通常 8192 バイト)のバッファ。

buffering=0

バッファなし。バイナリモードでのみ使用可能。

import io
print(io.DEFAULT_BUFFER_SIZE)  # 8192

第 2 層:C ライブラリのバッファ

CPython は内部で C の標準ライブラリ(stdio)を使用しており、FILE* のバッファが存在する。ただし、Python 3 の io モジュールは多くの場合このレイヤーをバイパスする。

Python 2

C の stdio を直接使用。setvbuf() でバッファリングを制御

Python 3

io モジュールが独自にバッファリング。C レベルのバッファはほぼ使われない

第 3 層:カーネルのページキャッシュ

Python の flush() を呼んでも、データはカーネルのページキャッシュに留まる。ディスクへの実際の書き込みはカーネルが非同期で行う。

f = open("data.txt", "w")
f.write("hello")

# flush() は Python バッファ → カーネルバッファへの転送
f.flush()
# この時点ではまだディスクに書き込まれていない可能性がある

# fsync() でカーネルバッファ → ディスクへの書き込みを強制
import os
os.fsync(f.fileno())
# これでディスクに到達したことが保証される

f.close()

各関数の役割

関数動作データの到達先
write()Python バッファに書くPython バッファ
flush()カーネルに渡すカーネルバッファ
fsync()ディスクに書くディスク

実験:バッファリングの確認

バッファリングの動作を確認してみよう。

import os
import time

# バッファありで書き込み
f = open("test.txt", "w")
f.write("hello")
print(f"書き込み直後: {os.path.getsize('test.txt')} bytes")  # 0 bytes

f.flush()
print(f"flush後: {os.path.getsize('test.txt')} bytes")  # 5 bytes

f.close()

write() 直後はファイルサイズがゼロ。flush() して初めてカーネルに渡され、ファイルシステムに反映される。

バッファなしモード

リアルタイム性が必要な場合はバッファなしモードを使う。

# バイナリモードでのみ buffering=0 が使える
f = open("realtime.bin", "wb", buffering=0)
f.write(b"data")  # 即座にカーネルバッファへ
f.close()

# テキストモードでバッファなしはエラー
try:
    f = open("test.txt", "w", buffering=0)
except ValueError as e:
    print(e)  # can't have unbuffered text I/O

テキストモードでリアルタイム書き込みが必要なら、毎回 flush() を呼ぶか、行バッファリングを使う。

# 行バッファリング(改行で自動 flush)
f = open("log.txt", "w", buffering=1)
f.write("line 1\n")  # 改行で自動的に flush される

読み取り時のバッファリング

読み取りにもバッファリングが効く。小さな read() を繰り返すと、実際にはまとめて読み込まれる。

# 1 バイトずつ読んでも、内部では 8KB 単位で読み込まれる
with open("large.txt", "rb") as f:
    while True:
        byte = f.read(1)  # 見かけ上 1 バイト読み取り
        if not byte:
            break
        # 実際は最初の read(1) で 8KB 読み込まれ、
        # 以降はバッファから返される

mmap によるバッファリング回避

メモリマップドファイルを使うと、Python と C のバッファリング層をスキップしてカーネルのページキャッシュに直接アクセスできる。

import mmap
import os

with open("data.bin", "r+b") as f:
    # ファイルをメモリにマップ
    mm = mmap.mmap(f.fileno(), 0)
    
    # 配列のようにアクセス
    mm[0:5] = b"hello"
    
    # 変更をディスクに同期
    mm.flush()
    
    mm.close()

まとめ

バッファリングの目的

システムコールの回数を減らしてパフォーマンスを向上させる。1 バイトずつ write() しても、まとめてディスクに書かれる。

データの永続化を保証するには

flush() だけでは不十分。fsync() を呼んで初めてディスク書き込みが保証される。

リアルタイム性が必要なら

buffering=0(バイナリ)、buffering=1(テキスト、行単位)、または毎回 flush() を呼ぶ。