Python

【Python】自作クラスをwith文で使う方法【コンテキストマネージャ】

この記事では、Pythonのユーザー定義クラスをwith文で使えるようにする方法を解説します。

with文を使って定義したクラスを呼び出すことで、そのクラスの前処理と後処理を安全に実行することができます。

ユーザー定義クラスをwith文で使えるようにするには、そのクラスにコンテキストマネージャを実装します。

それでは、コンテキストマネージャについて見ていきましょう!

コンテキストマネージャとは?

コンテキストマネージャとは、with文の実行時に前後処理するオブジェクトのことを言います。例えば、ファイルオブジェクトはコンテキストマネージャです。

コンテキストマネージャは、以下のようにwith文で使用可能です。

with open('ファイル名', 'モード') as f:
    # ファイルの中身を読み込んだり書き込んだり

open()関数によって返されたファイルオブジェクトがfに格納され、with文ブロック内で使用可能です。また、close()メソッドで明示的にファイルを閉じなくてもブロックを抜ける際に後処理で勝手に閉じてくれます。

このように、決まった前後処理が必要なオブジェクトをコンテキストマネージャにすることで後処理を忘れることがなくなり、なおかつ記述が楽になります。

with文で使えるようにするには、2つのメソッドをクラスに実装する必要があります。

コンテキストマネージャを定義するには?

コンテキストマネージャを定義するには、クラスに以下の2つの特殊メソッドを定義します。

前処理

def __enter__(self):

このメソッドはwith文のコードブロックを処理する前に実行されます。

後処理

def __exit__(self, exc_type, exc_value, traceback):

このメソッドはwith文のコードブロックを抜ける際に実行されます。引数にはコードブロック内で例外が発生した際の詳細が格納され、発生しなかった場合はNoneが格納されます。

自作クラスにコンテキストマネージャを定義してwith文で使ってみます。

class ContextManager:

    def __enter__(self):
        print('前処理')

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理')


with ContextManager():
    print('with文のコードブロック内')

実行結果

前処理
with文のコードブロック内
後処理

自作したコンテキストマネージャをwith文を使って呼び出したことにより、自動的に「前処理」と「後処理」が出力されました。

前処理で生成したオブジェクトをブロック内で使用する

前処理で生成したオブジェクトをwith文ブロック内で使用するには__enter__()メソッドの戻り値として返すことでasで繋げた識別子に渡すことができます。

with ContextManager() as 識別子:
    # ブロック内で識別子を使用可能

例えば以下のコードでは、戻り値として文字列を渡してブロック内で出力している。

class ContextManager:

    def __enter__(self):
        print('前処理')
        return '__enter__の戻り値です'  # 戻り値を定義

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理')


with ContextManager() as text:
    print(text)

実行結果

前処理
__enter__の戻り値です
後処理

with文ブロック内で例外が発生した場合

with文 のコードブロック内で例外が発生しても__exit__()メソッドは実行されます。なので、後処理は必ず実行することが可能です。

ブロック内で例外を発生させて__exit__()メソッドが処理されるか確認してみましょう。ついでに、引数にどのような値が格納されているか出力してみます。

class ContextManager:

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理開始')
        print(f'exc_type: {exc_type}')
        print(f'exc_value: {exc_value}')
        print(f'traceback: {traceback}')
        print('後処理終了')


with ContextManager():
    # 例外を故意に起こす
    raise ValueError('エラー発生')

実行結果

後処理開始
exc_type: <class 'ValueError'>
exc_value: エラー発生
traceback: <traceback object at 0x10ae0c380>
後処理終了
Traceback (most recent call last):
  File "main.py", line 16, in <module>
    raise ValueError('エラー発生')
ValueError: エラー発生

with文ブロック内でエラーが発生しても後処理は正常に実行されました。安全に後処理を行いたい場合、コンテキストマネージャはとても強力です。

1つのwithで複数のコンテキストマネージャ

Python 3.10から()を使うことで1つのwith文で複数のコンテキストマネージャを処理できるようになりました。

class ContextManager1:

    def __enter__(self):
        print('前処理1')

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理1')

class ContextManager2:

    def __enter__(self):
        print('前処理2')

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理2')


with (ContextManager1(), ContextManager2()):
    pass

実行結果

前処理1
前処理2
後処理2
後処理1

それぞれasで戻り値を受け取ることができる。

class ContextManager1:

    def __enter__(self):
        print('前処理1')
        return 'ContextManager1'

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理1')

class ContextManager2:

    def __enter__(self):
        print('前処理2')
        return 'ContextManager2'

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理2')
    

with (ContextManager1() as c1, ContextManager2() as c2):
    print(c1)
    print(c2)

実行結果

前処理1
前処理2
ContextManager1
ContextManager2
後処理2
後処理1

以下のように改行して記述することもできるが、見やすいかどうかは微妙。

with (
    ContextManager1() as c1,
    ContextManager2() as c2
):
    print(c1)
    print(c2)

まとめ

この記事では、Pythonのユーザー定義クラスをwith文で使えるようにするためにコンテキストマネージャを定義する方法解説しました。

コンテキストマネージャを使うことで、強力に前処理と後処理を定義することができます。特に後処理は例外が発生した場合でも実行してくれるので安心ですね❗️

実装する機会は少ないかもしれませんが、選択肢の1つとして覚えておきましょう!

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ