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

スポンサーリンク

Pythonのwith`文はとても便利です。

このステートメントの何がそんなに便利で、誰もが使うほどなのでしょうか?

一番便利なのは(実際これだけです!)、リソースを開いて解放してくれることです。

基本的には、プログラムの特定の部分で使用する必要があるようなリソースのオープンとクローズを処理し、その後自動的にクローズします。

この文を、今からいくつかの例を使って、もう少し詳しく見てみましょう。

スポンサーリンク

なぜコンテキストマネージャーが必要なのか?

ファイル操作を行う場合のシナリオを考えてみましょう。

C言語など他の言語では、このように手動でファイルを開いたり閉じたりしなければならない。

# Open the file
file_obj = open('input.txt', 'r')
 
# File operations come here
...
 
# Manually close the file
file_obj.close()

しかし、with文はこれを自動的に抽象化してくれるので、毎回手動でファイルを閉じる必要がなくなります!

with` ステートメントはコンテキスト(ブロック)を持っていて、その下で動作します。

これを文のスコープと呼びます。

プログラムがこのコンテキストを抜けると、withは自動的にファイルを閉じます!

このため、with はしばしばコンテキストマネージャと呼ばれます。

つまり、with ステートメントと一緒に、同じファイル操作手順をこのように使うことができるのです。

with open('input.txt', 'r') as file_obj:
    ...

これは非常に直感的であることに注意してください。

Pythonの with 文は、たとえプログラムがコンテキストやブロックの中で異常終了したとしても、必ず最後にファイルを閉じます。

この安全な機能により、すべてのPythonプログラマに受け入れられる(そして推奨される)選択となります!

Python の with 文を使う

さて、 with を使う機能を実装しているクラスはたくさんありますが、私たちはそれがどのように動作するのかに興味があり、自分たちでも書けるようになりたいと思っています。

  • まず、 with 文はオブジェクトの参照をコンテキストオブジェクトに格納します。

コンテキストオブジェクトとは、モジュールやスコープなどの状態に関する特別な情報を含むオブジェクトのことです。

これは、このオブジェクトの状態を保存したり復元したりできるので便利です。

つまり、このオブジェクトへの参照を保持することには、何らかの意味があるのです

さて、次に進みましょう。

コンテキストオブジェクトが生成されると、そのオブジェクトに対して __enter__ ダンダメソッドを呼び出します。

  • __enter__ 文は、ファイルやソケットなど、オブジェクトのリソースを開く作業を実際に行うものです。通常は、必要に応じてコンテキストオブジェクトの状態を保存するために実装することができます。

さて、 as キーワードを覚えていますか?これは実際にコンテキストオブジェクトを返します。


open() で返されたオブジェクトが必要なので、 as キーワードを使用してコンテキストオブジェクトを取得します。

特に、元のコンテキストオブジェクトへの参照を別の場所に持っている場合は、 as を使用することは任意です。

この後、入れ子になったステートメントのブロックに入ります。

ネストされたブロックが終了すると、あるいはその中で Exception が発生した場合には、プログラムは常にコンテキストオブジェクトに対して __exit__ メソッドを実行します!

これは、先ほど話した安全第一の機能です。

つまり、何が起こっても必ず __exit__ を使ってリソースを解放し、コンテキストを終了させるのです。

最後に、可能であれば __exit__ を実装してコンテキストオブジェクトの状態を復元し、元の状態に戻すことができます。

さて、かなり長い説明になってしまいました。

もっとわかりやすくするために、あるクラスに対して独自のコンテキスト・マネージャを作成する例を見てみましょう。

クラス独自のコンテキストマネージャーを作成する

以下のクラスで、ファイル操作のための独自のコンテキスト・マネージャを作成することを考えてみましょう。

class MyFileHandler():
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        # Originally, context object is None
        self.context_object = None
 
 
    # The context manager executes this first
    # Save the object state
    def __enter__(self):
        print("Entered the context!")
        self.context_object = open(self.filename, self.mode)
        return self.context_object
 
 
    # The context manager finally executes this before exiting
    # Information about any Exceptions encountered will go to
    # the arguments (type, value, traceback)
    def __exit__(self, type, value, traceback):
        print("Exiting the context....")
        print(f"Type: {type}, Value: {value}, Traceback: {traceback}")
        # Close the file
        self.context_manager.close()
        # Finally, restore the context object to it's old state (None)
        self.context_object = None
 
# We're simply reading the file using our context manager
with MyFileHandler('input.txt', 'r') as file_handle:
    for line in file_handle:
        print(line)

クラスのメソッドをよく観察してみましょう。

ハンドラのための __init__ メソッドがあり、Context オブジェクトと関連する変数の初期状態を設定します。

次に、__enter__ ダンダメソッドでオブジェクトの状態を保存し、ファイルを開いています。

これで、ブロックの内部に入ったことになります。

ブロックが実行された後、コンテキストマネージャは最後に __exit__ を実行し、そこでコンテキストオブジェクトの元の状態が復元され、ファイルが閉じられます。

さて、それでは出力をチェックしてみましょう。

結果は以下の通りです。

Entered the context!
Hello from Python
 
This is the second line
 
This is the last line!
Exiting the context....
Type: None, Value: None, Traceback: None

さて、どうやらエラーはなさそうですね。

これでカスタムクラスに独自のコンテキスト・マネージャーを実装することができました。

さて、コンテキスト・マネージャーを作成する方法として、ジェネレータを使用する方法もあります。

しかし、これは少し面倒で、例外を自分で処理しなければならないので、何をやっているのかよく分かっている人以外には一般的にお勧めできません。

しかし、念のため、この方法を使う方法をここで見ておくことにしましょう。

クラスベースのアプローチに慣れたら、これを読むことをお勧めします。

まとめ

今回は、PythonでContext Managersを使うために、with文の使い方について学びました。

タイトルとURLをコピーしました