2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BytesIO じゃダメな理由 — Python インメモリ FS ライブラリを作った話

2
Last updated at Posted at 2026-03-02

はじめに

「テストのためにファイルシステムをモックしたい」「CI でディスクに書かずに一時ファイルを扱いたい」——そういった場面で真っ先に思い浮かぶのが io.BytesIO だと思います。でも、少し複雑なことをしようとすると、BytesIO はすぐに限界を見せます。

この記事では、BytesIO の限界を整理したうえで、その課題をそのまま解決するために作った純粋 Python のインメモリファイルシステムライブラリ D-MemFS を紹介します。

BytesIO の限界

単一バッファにすぎない

io.BytesIO は単一の可変バイト列に対する read/write インターフェースを提供するものです。ファイルを扱う感覚で使えますが、あくまでも「1 つのバッファ」にすぎません。

from io import BytesIO

buf = BytesIO()
buf.write(b"hello, world")
buf.seek(0)
print(buf.read())  # b'hello, world'

これで困るのは、複数のファイルを扱いたいときです。

dict[str, BytesIO] で代替しようとすると…

複数ファイルを扱いたい場合、よく見かける応急処置が辞書です。

from io import BytesIO

# 簡易インメモリ"ファイルシステム"
vfs: dict[str, BytesIO] = {}

def vfs_write(path: str, data: bytes) -> None:
    buf = BytesIO(data)
    vfs[path] = buf

def vfs_read(path: str) -> bytes:
    buf = vfs[path]
    buf.seek(0)
    return buf.read()

vfs_write("config/settings.json", b'{"debug": true}')
vfs_write("data/input.csv", b"id,name\n1,Alice\n")
print(vfs_read("config/settings.json"))

一見動くように見えますが、すぐに問題が出てきます。

欲しい機能 dict[str, BytesIO] での状況
ディレクトリ作成・一覧 キープレフィックスで擬似的に実装するしかない
サブツリーの削除 {k: v for k, v in vfs.items() if not k.startswith(prefix)} を自前で
メモリ上限の設定 ない。無制限に積み上がる
スレッドセーフな読み書き ない。自前でロックを書く
ファイル stat (サイズ・更新日時) 自前で管理するしかない
append モード buf.seek(0, 2) を自前で

ディレクトリ構造を真面目に実装しようとすると、もはや「ファイルシステムを自作している」状態になります。しかも、誰がやっても微妙にバグが入ります。

もう一つの落とし穴:メモリが無制限に膨らむ

大量のデータをインメモリで処理するとき、BytesIO はメモリ上限を設定できないため、プロセスが OOM で落ちるまで気づけません。テスト環境で再現しない、本番だけで落ちる——そういったトラブルの温床になります。

D-MemFS を作った

上記の問題をまるごと解決するために、D-MemFS を作りました。

  • 標準ライブラリのみ(ゼロ外部依存)
  • 階層ディレクトリ構造
  • ハードクォータ(書き込み前に拒否)
  • RW ロックによるスレッドセーフ
  • 非同期ラッパー (AsyncMemoryFileSystem)
pip install D-MemFS

基本的な使い方

mkdir / open / write / read

from dmemfs import MemoryFileSystem

mfs = MemoryFileSystem()

# ディレクトリ作成
mfs.mkdir("/work/data")

# ファイルへの書き込み
with mfs.open("/work/data/hello.txt", "wb") as f:
    f.write(b"Hello, D-MemFS!\n")

# ファイルの読み込み
with mfs.open("/work/data/hello.txt", "rb") as f:
    print(f.read())  # b'Hello, D-MemFS!\n'

# stat 情報
st = mfs.stat("/work/data/hello.txt")
print(st["size"])         # 16
print(st["is_dir"])       # False
print(st["modified_at"])  # Unix タイムスタンプ (float)

ディレクトリ操作

# ディレクトリ一覧
for entry in mfs.listdir("/work/data"):
    print(entry)  # 'hello.txt'

# サブツリーごと削除
mfs.rmtree("/work")

# 存在確認
print(mfs.exists("/work"))  # False

テキストファイルを扱う

バイナリモードのみですが、テキスト I/O には MFSTextHandle を使います。

from dmemfs import MemoryFileSystem, MFSTextHandle

mfs = MemoryFileSystem()
mfs.mkdir("/logs")

# テキスト書き込み
with mfs.open("/logs/app.log", "wb") as f:
    th = MFSTextHandle(f, encoding="utf-8")
    th.write("起動しました\n")
    th.write("処理完了\n")

# テキスト読み込み
with mfs.open("/logs/app.log", "rb") as f:
    th = MFSTextHandle(f, encoding="utf-8")
    for line in th:
        print(line, end="")

クォータで暴走を防ぐ

max_quota パラメータを渡すだけで、書き込み前にメモリ使用量を制限できます。

from dmemfs import MemoryFileSystem, MFSQuotaExceededError

# 1 MiB のクォータを設定
mfs = MemoryFileSystem(max_quota=1 * 1024 * 1024)

mfs.mkdir("/data")

try:
    with mfs.open("/data/big.bin", "wb") as f:
        # 1 MiB を超えようとするとここで例外
        f.write(b"x" * (2 * 1024 * 1024))
except MFSQuotaExceededError as e:
    print(f"クォータ超過: {e}")
    # → クォータ超過: quota exceeded (limit=1048576, used=0, requested=2097152)

MFSQuotaExceededError は書き込みが実行される前に発生します。中途半端な状態でファイルが汚染されることはありません。

ファイル数の上限も設定できます。

from dmemfs import MemoryFileSystem, MFSNodeLimitExceededError

mfs = MemoryFileSystem(max_nodes=4)
mfs.mkdir("/data")

mfs.open("/data/a.txt", "xb").close()
mfs.open("/data/b.txt", "xb").close()

try:
    mfs.open("/data/c.txt", "xb").close()
except MFSNodeLimitExceededError as e:
    print(f"ノード数超過: {e}")

import_tree / export_tree で一括操作

ファイルをまとめてインポート・エクスポートする機能も備えています。

import zipfile
import io
from dmemfs import MemoryFileSystem

mfs = MemoryFileSystem()

# dict[str, bytes] 形式でまとめてインポート
mfs.import_tree({
    "/snapshot/config.json": b'{"debug": true}',
    "/snapshot/data.csv": b"id,name\n1,Alice\n",
})

# 処理...
with mfs.open("/snapshot/config.json", "rb") as f:
    config = f.read()

# export_tree でまとめて取得し、ZIP に書き出す
exported = mfs.export_tree("/snapshot")
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
    for path, data in exported.items():
        zf.writestr(path.lstrip("/"), data)
with open("snapshot.zip", "wb") as f:
    f.write(buf.getvalue())

「処理中はすべてメモリ上で完結し、最後だけ書き出す」というパターンが自然に書けます。

比較まとめ

機能 BytesIO dict[str, BytesIO] D-MemFS
単一ファイル I/O
階層ディレクトリ △ 擬似的に
ディレクトリ一覧 △ 自前実装
サブツリー削除 △ 自前実装
append モード △ 手動 seek △ 手動 seek
ハードクォータ
stat (サイズ・日時)
スレッドセーフ
外部依存 なし なし なし(標準ライブラリのみ)

インストール

pip install D-MemFS

Python 3.11 以上が必要です。外部依存はゼロです。

おわりに

BytesIO は「1 つのバッファ」として優秀ですが、ファイルシステムの代わりにはなれません。dict[str, BytesIO] で自前実装すると、毎回同じバグを踏み続けます。

D-MemFS は、インメモリでファイルシステムが必要なあらゆる場面——テスト、CI、一時的なデータ処理パイプライン——で使えるように設計しています。まず pip install D-MemFS して、TemporaryDirectory を置き換えてみてください。


🛡️ D-MemFS 活用ガイド(連載)

📖 設計の裏側をもっと深く
なぜこのライブラリを作ったのか、その原体験や「バイブコーディング消耗からの脱却」としてのSDD(仕様駆動開発)の実践記録については、Zenn で連載しています。あわせてどうぞ。

GitHub で Star ⭐ をいただけると、開発の大きな励みになります!
(↓以下のリンク先がプロジェクトの本体です)

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?