メモの日々


2023年01月03日(火) [長年日記]

[python] with文とコンテキストマネージャとcontextlib

Pythonのコンテキストマネージャはすぐにわからなくなってしまうのでメモ。コンテキストマネージャをうまく使えばある種のコードが簡潔に書けるようになる。

なお、関連してasync with非同期コンテキストマネージャもあるがここでは触れない。

with文

with文を使うことで、そこで指定したコンテキストマネージャの __enter__() と __exit__() を暗黙的に呼び出すことができる。

次のドキュメントできちんと説明されているので、わからなくなったらここを読むのがいい。

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__() を持つクラスがコンテキストマネージャだ。

__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
    • コンテキストマネージャや関数を登録することで複数の後処理を行えるようにする。