目次
10 章

コンテキストマネージャ (with、contextlib)

try/finally を一行にする with、__enter__/__exit__ による自作、@contextmanager で生成器のように短く作る方法、そして ExitStack/suppress などの実戦の道具まで整理します。

第6章 エラーと例外処理try/finally より with の方がきれいだと少し触れました。本章ではそのテーマを掘り下げます。with のあらゆるパターン と、ユーザー定義コンテキストマネージャ、contextlib の道具を扱います。

本章の @contextmanager は第11章 イテラブル、ジェネレータ、yield from のジェネレータの上に作られた道具なので、両章をあわせて読むと理解しやすくなります。そして最後に扱う async with は、第14章 非同期入門 (asyncio) で本格的に登場します。

with — 一行に整理されるリソース #

最も基本的な例 — ファイル処理。

with がないとき
f = open("data.txt")
try:
    data = f.read()
finally:
    f.close()
with とともに
with open("data.txt") as f:
    data = f.read()
# ここで自動的に f.close()

withブロックを抜けるときに整理コードを自動で呼び出します。 正常終了、例外発生、return のどの経路で抜けても整理は行われます。

with が合うリソース #

リソース整理動作
ファイルclose()
ロック (Lock)release()
DB 接続 / トランザクションcommit() または rollback() + close()
一時ディレクトリディレクトリ削除
標準出力リダイレクト元の stdout を復元
環境変数の変更元の値を復元

共通点: 「しばらく何らかの状態に入ってから、抜けるときに元に戻したい」。これがコンテキストマネージャが解いてくれる問題です。

複数のリソースを一度に #

複数の with — カンマで
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

3.10 からは 括弧で囲んで複数行で書くこと もできます。

複数行の with (3.10+)
with (
    open("input.txt") as src,
    open("output.txt", "w") as dst,
    Lock() as lock,
):
    dst.write(src.read())

引数が多い関数と同じ形なので読みやすいです。

自作する — __enter__ / __exit__ #

with が動作するオブジェクトは2つのメソッドを持ちます。

  • __enter__(self)with に入るときに呼ばれる、as の後ろで受け取る値を返す
  • __exit__(self, exc_type, exc_value, traceback) — 抜けるときに呼ばれる

最も単純な例 — 一時的な chdir #

作業ディレクトリをしばらく変えたいとき。

リソースクラスを作る
import os

class chdir:
    def __init__(self, path: str):
        self.path = path

    def __enter__(self):
        self.old = os.getcwd()
        os.chdir(self.path)
        return self.path

    def __exit__(self, exc_type, exc_value, tb):
        os.chdir(self.old)

with chdir("/tmp"):
    print(os.getcwd())     # /tmp
print(os.getcwd())          # 元のディレクトリ

__exit__ の戻り値 — 例外を吸収するか #

__exit__truthy を返すと例外を飲み込みます。 これを知らずに True を返すと、すべての例外が消えてデバッグが難しくなります。

🚫 意図しない例外吸収
class Bad:
    def __enter__(self): return self
    def __exit__(self, *args):
        return True   # ✗ すべての例外を飲み込む

ほとんどの場合 何も返さないか、False / None にしてください。

特定の例外だけ意図的に吸収するなら exc_type を見て判断します。

特定の例外だけ吸収
class IgnoreFileNotFound:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc_value, tb):
        return exc_type is not None and issubclass(exc_type, FileNotFoundError)

with IgnoreFileNotFound():
    with open("missing.txt") as f:
        ...
# 例外なく通過

このような頻出パターンは標準ライブラリにすでにあります(下の suppress 参照)。

@contextmanager — 一つの関数で短く #

__enter__ / __exit__ の2メソッドを持つクラスを毎回作るのは面倒です。関数一つに yield を挟んで コンテキストマネージャを作る方法があります。

@contextmanager
from contextlib import contextmanager

@contextmanager
def chdir(path: str):
    old = os.getcwd()
    os.chdir(path)
    try:
        yield path        # この時点で with ブロックが実行される
    finally:
        os.chdir(old)

この1つの関数が上のクラス版と正確に同じ仕事をします。

読み方:

  • yield = __enter__ がする仕事
  • yield = as の後ろで受け取る値
  • yield = __exit__ がする仕事
  • try/finally で囲まないと例外時に整理が保証されない

@contextmanager がどのようにジェネレータの上に作られているかは、次の 第11章 イテラブル、ジェネレータ、yield from で改めて扱います。

例外も受け取りたいなら #

例外を捕まえる
@contextmanager
def transactional(conn):
    tx = conn.begin()
    try:
        yield tx
    except Exception:
        tx.rollback()
        raise
    else:
        tx.commit()

yield の地点で with ブロックが例外を投げると、それがジェネレータの yield 地点で再度投げられます。だから普通の try/except で捕まえられます。

contextlib の他の補助ツール #

suppress — 特定の例外を無視 #

suppress
from contextlib import suppress
import os

with suppress(FileNotFoundError):
    os.remove("maybe-missing.txt")
# ファイルがなくてもただ通過

上で作った IgnoreFileNotFound クラスそのものが標準にあります。

closing — close() メソッドだけ持つオブジェクトを包む #

closing
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen("https://example.com")) as resp:
    data = resp.read()
# resp.close() 自動

urlopen のレスポンスオブジェクトが __exit__ を持っていなかった昔のバージョンによく使いました。今はレスポンスオブジェクトも with を直接サポートする場合が多いです。

redirect_stdout — 標準出力を横取り #

redirect_stdout
import io
from contextlib import redirect_stdout

buf = io.StringIO()
with redirect_stdout(buf):
    print("hello")
print("captured:", buf.getvalue())
# captured: hello

テスト、キャプチャ、ログ変換のような場合に有用です。redirect_stderr も同じ動作です。

nullcontext — 条件付きで with を使う #

nullcontext
from contextlib import nullcontext

def process(path: str | None):
    ctx = open(path) if path else nullcontext()
    with ctx as f:
        if f is None:
            print("no file")
        else:
            print(f.read())

「ファイルがあれば開いて、なければただ通過」のような場合。with の後ろには常にコンテキストマネージャが来なければならないので、何もしない安全なコンテキストマネージャ が必要です。nullcontext がそれです。

ExitStack — 動的に複数のリソースを管理 #

リソースの個数が ランタイムに決まる とき。

ExitStack
from contextlib import ExitStack

def merge(paths: list[str]) -> str:
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        return "\n".join(f.read() for f in files)
# すべてのファイルが自動的に close

with open(p) as f を N 個書けないとき(paths リストの長さが動的なら)ExitStack がその問題を解いてくれます。登録されたすべてのリソースが逆順で整理 されます。

ExitStack__enter__ ではない一般の整理関数も登録できます。

コールバック登録
def setup_temp_resources():
    stack = ExitStack()
    tmp_dir = create_tmp_dir()
    stack.callback(remove_dir, tmp_dir)
    # ... さらに多くのリソース登録
    return stack

with setup_temp_resources() as stack:
    ...

async コンテキストマネージャ — 少しプレビュー #

非同期リソースには async with を使います。

async with
async with open_async_db() as conn:
    await conn.execute(...)

これはメソッドが __aenter__ / __aexit__ です。@asynccontextmanager もあり、使い方は同期版と同じです。詳細は第14章 非同期入門 (asyncio) で。

自作 — クラス vs @contextmanager #

状況クラス@contextmanager
短く単純
状態 / メソッドが多い
例外処理の分岐が複雑
短い副作用 (chdir、環境変数など)
ライブラリでユーザーに公開

迷ったら @contextmanager から 試して、複雑になったらクラスに移すのが無難です。

よく出会うパターン — 一か所に #

一時的な環境変数 #

環境変数の一時設定
import os
from contextlib import contextmanager

@contextmanager
def set_env(**kwargs: str):
    old = {}
    for k, v in kwargs.items():
        old[k] = os.environ.get(k)
        os.environ[k] = v
    try:
        yield
    finally:
        for k, v in old.items():
            if v is None:
                del os.environ[k]
            else:
                os.environ[k] = v

テストで環境変数依存のコードを検証するときによく使います。

時間計測 #

elapsed time
import time
from contextlib import contextmanager

@contextmanager
def timer(label: str):
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"{label}: {elapsed:.3f}s")

with timer("query"):
    do_expensive_thing()
# query: 0.123s

本パターンは正確な測定ツールではありません。正確なプロファイリングは第21章 パフォーマンス — cProfile、py-spy、メモリプロファイリング で扱います。

ロック #

lock
import threading

lock = threading.Lock()

with lock:
    # クリティカルセクション
    update_shared_state()

LockRLockSemaphore すべて with をサポートします。acquire() / release() を直接呼ぶコードはほとんど書きません。

練習問題 #

  1. @contextmanagerset_env(**kwargs: str) を書いてください。with ブロックに入るときに環境変数をセットし、抜けるときに元の値に復元します。os.environ の元の値がなかった場合 (None) も正確に復元しなければなりません。
  2. class TempFile: を書いてください。__enter__ で一時ファイルを作って経路を返し、__exit__ でそのファイルを削除します。with ブロックの中で例外が発生してもファイルが消えるかを確認します。
  3. ExitStackdef merge(paths: list[str]) -> str: を書いてください。引数として受け取ったすべての経路のファイルを開いて内容を合わせて返します。ファイルの途中で1つが FileNotFoundError を投げてもすでに開いたファイルが正常に close されることを確認します(開いたファイルハンドルはデバッガで追跡)。

一行まとめ: with は入る / 整理の境界を一行に圧縮する。自作はクラス (__enter__/__exit__) または @contextmanager (関数 + yield)。__exit__ の戻り値は False が普通 — True は例外吸収。contextlibsuppress / closing / redirect_stdout / nullcontext / ExitStack が実戦の補助ツール。動的なリソース個数は ExitStack。非同期は async with

次の章 #

次の 第11章 イテラブル、ジェネレータ、yield from では、内包表記の向こうの道具 — ユーザー定義イテラブル、ジェネレータ関数、yield from を扱います。本章の @contextmanager も実はジェネレータの上に作られた道具です。

X