コンテキストマネージャ (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 — 一行に整理されるリソース
#
最も基本的な例 — ファイル処理。
f = open("data.txt")
try:
data = f.read()
finally:
f.close()with open("data.txt") as f:
data = f.read()
# ここで自動的に f.close()with は ブロックを抜けるときに整理コードを自動で呼び出します。 正常終了、例外発生、return のどの経路で抜けても整理は行われます。
with が合うリソース
#
| リソース | 整理動作 |
|---|---|
| ファイル | close() |
ロック (Lock) | release() |
| DB 接続 / トランザクション | commit() または rollback() + close() |
| 一時ディレクトリ | ディレクトリ削除 |
| 標準出力リダイレクト | 元の stdout を復元 |
| 環境変数の変更 | 元の値を復元 |
共通点: 「しばらく何らかの状態に入ってから、抜けるときに元に戻したい」。これがコンテキストマネージャが解いてくれる問題です。
複数のリソースを一度に #
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read())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 を挟んで コンテキストマネージャを作る方法があります。
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 — 特定の例外を無視
#
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("maybe-missing.txt")
# ファイルがなくてもただ通過上で作った IgnoreFileNotFound クラスそのものが標準にあります。
closing — close() メソッドだけ持つオブジェクトを包む
#
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 — 標準出力を横取り
#
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 を使う
#
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 — 動的に複数のリソースを管理
#
リソースの個数が ランタイムに決まる とき。
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)
# すべてのファイルが自動的に closewith 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 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テストで環境変数依存のコードを検証するときによく使います。
時間計測 #
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、メモリプロファイリング で扱います。
ロック #
import threading
lock = threading.Lock()
with lock:
# クリティカルセクション
update_shared_state()Lock、RLock、Semaphore すべて with をサポートします。acquire() / release() を直接呼ぶコードはほとんど書きません。
練習問題 #
@contextmanagerでset_env(**kwargs: str)を書いてください。with ブロックに入るときに環境変数をセットし、抜けるときに元の値に復元します。os.environの元の値がなかった場合 (None) も正確に復元しなければなりません。class TempFile:を書いてください。__enter__で一時ファイルを作って経路を返し、__exit__でそのファイルを削除します。with ブロックの中で例外が発生してもファイルが消えるかを確認します。ExitStackでdef merge(paths: list[str]) -> str:を書いてください。引数として受け取ったすべての経路のファイルを開いて内容を合わせて返します。ファイルの途中で1つがFileNotFoundErrorを投げてもすでに開いたファイルが正常に close されることを確認します(開いたファイルハンドルはデバッガで追跡)。
一行まとめ:
withは入る / 整理の境界を一行に圧縮する。自作はクラス (__enter__/__exit__) または@contextmanager(関数 +yield)。__exit__の戻り値はFalseが普通 —Trueは例外吸収。contextlibのsuppress / closing / redirect_stdout / nullcontext / ExitStackが実戦の補助ツール。動的なリソース個数はExitStack。非同期はasync with。
次の章 #
次の 第11章 イテラブル、ジェネレータ、yield from では、内包表記の向こうの道具 — ユーザー定義イテラブル、ジェネレータ関数、yield from を扱います。本章の @contextmanager も実はジェネレータの上に作られた道具です。