Python のコンテキストマネージャと with ブロック

コンテキストマネージャ (context manager) を with ブロックを組み合わせて使うことによって、ファイルやロックなどのリソースの解放を行なうコードを簡便に実装できるようになります。

ここではコンテキストマネージャの具体的な仕組みと、実装方法について説明します。

コンテキストマネージャの仕組み

コンテキストマネージャを定義するには、__enter__ メソッドと __exit__ メソッドを実装します。

コンテキストマネージャが生成されるときに、__enter__ が呼ばれ、with ブロックによって定義されたコンテキストから抜けるときに __exit__ メソッドが呼ばれます。

裏で例外処理 (try..catch..finally) を利用しており、コンテキストから抜けるときに、__exit__ メソッドが確実に呼ばれます。

コンテキストマネージャの利用用途

コンテキストマネージャを使うと、ファイルハンドルの解放とか、ロックのリリースなど、たとえ例外が発生して処理が飛んでも確実に実行しておきたい処理をわかりやすい形で書いておくことが可能になります。

例えばファイルオブジェクトでコンテキストマネージャを実装し、ファイルオブジェクトを with ブロックで作成することで、コンテキストを抜けるときに、 確実に close を呼ぶ (__exit__ で呼べばよい) ことが可能です。close の書き忘れを防ぐためにも、コンテキストマネージャを実装すべきです。

コンテキストマネージャの実装方法

それでは具体例を示しながらコンテキストマネージャの実装方法を説明します。

コンテキストマネージャを実装するには前述の通り、__enter__ メソッドと __exit__ メソッドを定義するだけです。 __exit__ メソッドには、そのコンテキストで例外が発生した場合に、その例外 (Exception) の情報が渡されます。例外が発生していないときは、 None となります。

class MyContextManager:
    def __enter__(self):
        print('ENTER!')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('EXIT!')

    def foo(self):
        print('FOO!')


# テストプログラム
with MyContextManager() as cm:
    print('** hello **')
    cm.foo()

このコードの実行結果は次の通り。

ENTER!
** hello **
FOO!
EXIT!

with ブロックでコンテキストマネージャとして実装した MyContextManager を作成しています。 __enter__ メソッドで返した値が with の as で指定した変数にセットされます。実行結果より、hello という文字をプリントした後に foo メソッドも呼べていることもわかりますね。

with ブロックから抜けるときに、確かに __exit__ メソッドが呼び出されています。

コンテキストマネージャーの例外発生時の振る舞い

それでは with ブロックで例外が発生した場合の振る舞いを確認してみましょう。

__exit__ メソッドを書き換え、第一引数 exec_type を調べて None なら正常終了時 (例外非発生時)、例外がセットされていれば例外発生時と判別します。

class MyContextManager:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            print('OK!')
        else:
            print('--------')
            print(exc_type)
            print('--------')
            return True


# テストプログラム
with MyContextManager() as cm:
    print('** hello **')
    raise TypeError
    print('** bye **')

この実行結果は次のようになります。

** hello **
--------
< class 'TypeError'>
--------

with ブロックの最後の "** bye **" の手前で TypeError を発生させていますので、bye の文字のプリントはありません。

しかし、コンテキストマネージャの __exit__ メソッドが呼ばれており、そこで例外の型を出力しています。

例外発生時に __exit__ で True を返していますが、これによって例外処理ハンドラのチェーンを切っています。ここで False を返すと、 そのひとつ上の例外処理ハンドラが呼び出されます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Python 入門