2023年01月03日(火) [長年日記]
■ [python] with文とコンテキストマネージャとcontextlib
Pythonのコンテキストマネージャはすぐにわからなくなってしまうのでメモ。コンテキストマネージャをうまく使えばある種のコードが簡潔に書けるようになる。
なお、関連してasync withや非同期コンテキストマネージャもあるがここでは触れない。
with文
with文を使うことで、そこで指定したコンテキストマネージャの __enter__() と __exit__() を暗黙的に呼び出すことができる。
次のドキュメントできちんと説明されているので、わからなくなったらここを読むのがいい。
- 8.5. with 文 (docs.python.org)
class C: def __enter__(self): print("enter") def __exit__(self, exc_type, exc_value, exc_tb): print("exit") with C(): print("hello") raise RuntimeError()
enter hello exit Traceback (most recent call last): File "/home/kenichi/work/python/context_manager.py", line 10, in <module> raise RuntimeError() RuntimeError
with文のブロック内で例外が投げられたときに C.__exit__() が呼ばれることが重要だ。同じことはtry/finallyで実現できるが、with文を使うとより簡潔に書けるようになる。
コンテキストマネージャ
上述のCクラスのように __enter__() と __exit__() を持つクラスがコンテキストマネージャだ。
- コンテキストマネージャ型 (docs.python.org)
__enter__()は戻り値を返すことができ、それはwith文のas節で受け取ることができる。
__exit__()の方は例外を意識する必要がある。
- 戻り値で真を返すとwith文のブロック内で投げられた例外がwith文の外へ伝播しなくなる。
- with文のブロックで例外が投げられると、その情報を引数のexc_type, exc_value, exc_tbで受け取る。
contextlib
contextlibモジュールにてwith文用のユーティリティが提供されている。
@contextmanager
@contextmanagerはコンテキストマネージャを関数で定義できるようになるデコレータだ。先の例と同じものを次のように書くことができる。
import contextlib from collections.abc import Iterator @contextlib.contextmanager def f() -> Iterator[None]: print("enter") try: yield finally: print("exit") with f(): print("hello") raise RuntimeError()
f()には型ヒントを付けたが、@contextmanagerで修飾する関数はIteratorを返す必要がある。yieldで渡した値がwith文のas節に渡される(上の例には無いけど)。
また、@contextmanagerで作ったコンテキストマネージャは自動的にデコレータにもなり、次のように使えるようになる。
@f() def g(): print("world") g()
enter world exit
組み込みのコンテキストマネージャ
contextlibでは色々なコンテキストマネージャを用意してくれている。
- closing
- 指定したオブジェクトのclose()を自動的に呼び出してくれる。
- nullcontext
- 何もしないコンテキストマネージャ?
- suppress
- 指定した例外を捨てる。
- redirect_stdout
- 標準出力を一時的に指定先へリダイレクトする。
- redirect_stderr
- 標準エラー出力を一時的に指定先へリダイレクトする。
- chdir
- カレントディレクトリを一時的に変更する。
- ExitStack
- コンテキストマネージャや関数を登録することで複数の後処理を行えるようにする。