Context Manager

はじめに

コンテクストマネージャは、with 文を裏で支えているオブジェクトです。 この文章では、with 文の基本的な使用法を説明した上で、コンテクストマネージャの定義の仕方や、with 文とコンテクストマネージャとの関係について述べていきます。

with

with 文は、例外や return 文などによりプログラムの実行が不可能となった際にも、特定の処理を確実に実行するための制御構造です。 たとえばファイルのオープンとクローズ、ロックの取得と解放、データベースとの接続の確立と切断など、あるリソースを管理する際に対をなして現れるような処理を簡潔に記述する際などによく用いられます。 こうした処理は、伝統的には try/finally によるパターンで記述されていました。 with 文は、この try/finally パターンを抽象化するための糖衣構文であるといえます。

たとえば次のコードは、ファイルをオープンして文字列を書き込み、ファイルをクローズするという処理を try/finally により実装する例です:

>>> f = open('foo.txt', 'w')
>>> try:
...     f.write('hello world')
... finally:
...     f.close()
...
11

上の処理を with 文を利用して書くと次のようになります:

>>> with open('foo.txt', 'w') as f:
...     f.write('hello world')
...
11
>>> f.closed
True

with 文ではファイルを明示的にクローズする処理は書かれていませんが、closed アトリビュートを確認することにより、ファイルがきちんとクローズされていることがわかります。 with ブロック内で例外が発生したとしても、

>>> with open('foo.txt', 'w') as f:
...     1 / 0
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
>>> f.closed
True

から、同様にファイルはクローズされることがわかります。 このように、with 文を利用すると、try/finally により記述しなければならなかった処理を簡潔に記述することができます。

with 文の挙動は、コンテクストマネージャというオブジェクトにより制御されています。 上の例において、ブロックを抜けた際にファイルがクローズされているのは、コンテクストマネージャによってそのように指定されているからに他なりません。 また、as f という記述により f にファイルオブジェクトが代入されるという挙動も、コンテクストマネージャによって指定されたものです。 以下では、コンテクストマネージャが with 文を制御する仕組みについて確認していきましょう。

コンテクストマネージャ

コンテクストマネージャ (context manager) は、__enter____exit__ という二つのメソッドを実装したクラスのインスタンスです。

__enter__ メソッドには、その名の通り、with ブロックに入る際の処理を記述します。 具体的には、たとえばファイルオブジェクトの生成や、データベースとの接続の初期化など、リソースを準備する処理などが実行されます。 __enter__ メソッドから値を返すと、その値は as x における x に代入されます。 したがって、__enter__ メソッドで初期化したオブジェクトを with ブロック内で利用するために受け渡したい場合などは、ここで適切な値を返す必要があります。

with ブロックから抜ける際に実行される処理を記述する場所が、__exit__ メソッドです。 たとえば、ファイルのクローズ処理やロックの解放など、一連の処理から抜ける際に実行する必要のある処理をここに記述します。 try/finally パターン における finally のブロックに相当する処理をここに記述します。

それでは、ここまでの説明を理解するために、ファイルのオープンとクローズを制御するコンテクストマネージャを実装してみましょう:


>>> class FileManager:
...     def __init__(self, name):
...         self.name = name
...     def __enter__(self):
...         self.file = open(self.name, 'w')
...         return self.file
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         if self.file:
...             self.file.close()
...
>>> with FileManager('foo.txt') as f:
...     f.write('hello world')
...
11

FileManager クラスは __enter____exit__ メソッドを実装しているので、そのインスタンスはコンテクストマネージャとなります。 FileManager('foo.txt') によりインスタンスが生成されますが、これが with 文へと渡されることで、インスタンスの __enter__ メソッドが呼ばれます。 __enter__ の内部では、ファイルオブジェクトが生成され、その値を返しています。 これにより、as に与えられた変数 f へとファイルオブジェクトが代入されます。 f への書き込みが終了し with ブロックを抜ける際には __exit__ が呼ばれ、ここでファイルをクローズしています。

ここでは利用しませんでしたが、__exit__ には、with ブロックで発生した例外のクラス、例外のインスタンス、そしてトレースバックオブジェクトが引数として与えられます。 with ブロックで例外が発生しなければ、これらはすべて None となります。

なお、with ブロックで例外が発生した場合に __exit__ の戻り値を True とすると、例外が __exit__ から再送出されることを抑制します。 戻り値が True でなければ例外は自動で再送出されるため、raise 文を使う必要はありません。

ところで、with 文を利用せずに、__enter____exit__ を直接呼ぶことも可能です。 with 文の挙動についてさらに理解を深めるために、これらのメソッドを手動で呼んでみましょう:

>>> manager = FileManager('foo.txt')
>>> f = manager.__enter__() # __enter__はファイルオブジェクトを返す
>>> f.closed
False
>>> f.write('hello world')
11
>>> manager.__exit__(None, None, None)
>>> f.closed # __exit__によりファイルオブジェクトはクローズされている
True

以上のように、コンテクストマネージャは __enter__ メソッドと __exit__ メソッドを定義することで、with 文の挙動を制御しているといえます。 これは、イテレータ__iter__ メソッドと __next__ メソッドにより for 文を制御していることと似ています。

contextlib.contextmanager を利用する

Python の標準ライブラリには、contextlib という、with 文のためのユーティリティを集めたライブラリが存在します。 これに含まれる contextlib.contextmanager というデコレータを利用することにより、ジェネレータを定義するだけでコンテクストマネージャを作成することができます。

contextlib.contextmanager デコレータを付加したジェネレータ関数において、yield 式の手前の処理がコンテクストマネージャにおける __enter__ に、yield 式より下が __exit__ に対応します。また、yield 式に与える値が、as においてバインドされます。すなわち、

class ContextManager:

    ....

    def __enter__(self):
        # 前処理
        return x

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 後処理

というコンテキストマネージャは、

@contextlib.contextmanager
def context_manager():
    # 前処理
    try:
        yield x
    finally:
        # 後処理

のようなジェネレータ関数へと書き換えることができます。 これが with 文において実行されると、yield の行で処理が停止し、 with ブロックの実行が開始されます。 そしてブロックを抜けたところで yield の行から処理が再開されることになります。 yield x と後処理の部分が try/finally により囲まれているのは、with ブロックにおいて例外が発生したときに、yield の行で再度送出される例外をハンドリングするためです。

たとえば、上で書いた FileManager と同等のジェネレータ関数は次のようになります:

@contextlib.contextmanager
def file_manager(name):
    f = open(name, 'w')
    try:
        yield f
    finally:
        f.close()

使用方法はこれまでと同様です:

>>> with file_manager('foo.txt') as f:
...     f.write('hello world')
...
11

このように、contextlib.contextmanager デコレータにより、クラスを定義することなく、より簡潔にコンテクストマネージャを記述することができます。

まとめ

参考