Python コンテキストマネージャ

Python の最も “わかりにくい “機能の1つは、ほとんどすべての Python プログラマが、初心者でさえも使っているにもかかわらず、よく理解していない、コンテキストマネージャです。おそらく with 文の形で見たことがあると思いますが、通常 Python でファイルを開くことを学ぶときに最初に遭遇します。コンテキストマネージャは最初は少し奇妙に見えますが、その背後にある動機やテクニックを理解することで、私たちはプログラミングの新しい武器を手に入れることができるのです。それでは、さっそく見ていきましょう。

モチベーション リソース管理

私よりずっと賢明な人が言ったように、「必要は発明の母」です。コンテキスト・マネージャとは何か、そしてそれをどのように使うことができるかを本当に理解するためには、まずその背後にある動機、つまりこの「発明」を生み出した必要性を調査する必要があります。

コンテキスト・マネージャーの主な動機は、リソース管理です。プログラムがコンピュータ上のリソースにアクセスしたい場合、OSにそれを要求し、OSはそのリソースのハンドルを提供します。このようなリソースの一般的な例として、ファイルやネットワークポートがあります。ここで重要なのは、これらのリソースは利用できる数が限られているということです。例えば、ネットワークポートは一度に1つのプロセスしか利用できませんし、利用できるポートの数も限られています。ですから、リソースをオープンするときはいつでも、リソースを解放するためにクローズすることを忘れないようにしなければなりません。しかし、残念ながら、言うは易く行うは難しです。

適切なリソース管理を行う最も簡単な方法は、リソースを使い終わった後に close 関数を呼び出すことでしょう。例えば

opened_file = open('readme.txt')
text = opened_file.read()
...
opened_file.close()


ここでは、 readme.txt という名前のファイルをオープンし、ファイルを読み込んでその内容を文字列 text に保存しています。そして、使い終わったら opened_file オブジェクトの close() メソッドを呼び出してファイルをクローズしています。さて、一見するとこれは問題ないように見えますが、実は全く堅牢ではありません。ファイルを開いてから閉じるまでの間に何か予期せぬことが起こり、プログラムが close ステートメントを含む行の実行に失敗すると、リソースリークになります。これらの予期せぬ出来事は私たちが exceptions と呼んでいるもので、よくあるのは実行中のプログラムを誰かが強制的に閉じた場合です。

さて、これを処理する適切な方法は、try...elseブロックを使った例外処理を使うことでしょう。次の例を見てください。

try:
    opened_file = open('readme.txt')
    text = opened_file.read()
    ...
else:
    opened_file.close()


Pythonは、何が起ころうとも、常に else ブロックの中のコードが実行されるようにします。これは他の言語のプログラマがリソース管理を行う方法ですが、Pythonのプログラマは、同じ機能を定型文なしで実装できる特別な仕組みを手に入れることができます。ここで、コンテキストマネージャが登場します。

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

コンテキストマネージャーを理解するための最も重要な部分が終わったので、次はその実装に取りかかりましょう。このチュートリアルでは、カスタムの File クラスを実装することにします。Python はすでにこのクラスを提供しているので、完全に冗長ですが、それでも、標準ライブラリにすでにある File クラスに常に関連付けることができるので、良い学習練習になるでしょう。

標準的で「低レベル」なコンテキストマネージャの実装方法は、リソース管理を実装したいクラスで __enter____exit__ という 2 つの「マジック」メソッドを定義することです。もしあなたが「この魔法のメソッドって何? Pythonでオブジェクト指向プログラミングを始めた人なら、すでに魔法のメソッドに出会っていることでしょう。

うまく言えないのですが、これはクラスをより賢くしたり、クラスに「魔法」を追加するために定義できる特別なメソッドです。Pythonで利用できるすべてのマジックメソッドのリファレンスリストはこちらで見ることができます。

さて、話を元に戻して、この2つのマジックメソッドを実装する前に、その目的を理解しておく必要があります。enterはリソースを開いたときに呼び出されるメソッドで、もう少し専門的な言い方をすると、ランタイムコンテキストに "入る" と呼ばれるメソッドです。with 文は、このメソッドの戻り値を as 節で指定したターゲットに結びつけます。

例を見てみましょう。

class FileManager:
    def __init__(self, filename):
        self.filename = filename

    def __enter__(self):
        self.opened_file = open(self.filename)
        return self.opened_file


見ての通り、__enter__ メソッドはリソースであるファイルをオープンしてそれを返しています。この FileManagerwith 文で使用すると、このメソッドが呼び出され、その戻り値が as 節で指定したターゲット変数に束縛されます。以下のコードで、その様子をお見せします。

with FileManager('readme.txt') as file:
    text = file.read()


では、各部分を分解してみましょう。まず、コンストラクタにファイル名 “readme.txt” を渡すと、FileManager クラスのインスタンスが生成されます。そして、with 文で処理を開始します。FileManager オブジェクトの __enter__ メソッドを呼び出して、as 節で指定した file 変数に返り値を代入しています。そして、 with ブロックの中で、オープンしたリソースに対して行いたいことを何でも行うことができます。

パズルのもうひとつの重要な部分は __exit__ メソッドです。exit__メソッドにはクリーンアップのコードが含まれており、リソースを使い終わった後は何があっても実行しなければなりません。このメソッドに含まれる命令は、以前例外処理について説明したelseブロックに含まれる命令と似ています。繰り返しになりますが、exit` メソッドにはリソースハンドラを適切に閉じるための命令が含まれており、リソースは OS 内の他のプログラムによってさらに使用できるように解放されます。

では、このメソッドをどのように書くか見てみましょう。

class FileManager:
    def __exit__(self. *exc):
        self.opened_file.close()


さて、このクラスのインスタンスが with 文で使われるときはいつでも、この __exit__ メソッドはプログラムが with ブロックを抜ける前、あるいは何らかの例外でプログラムが停止する前に呼ばれることになります。では、全体像を把握するために FileManager クラス全体を見てみましょう。

class FileManager:
    def __init__(self, filename):
        self.filename = filename

    def __enter__(self):
        self.opened_file = open(self.filename)
        return self.opened_file

    def __exit__(self, *exc):
        self.opened_file.close()


シンプルでしょう?オープンやクリーンアップのアクションをそれぞれのマジックメソッドで定義するだけで、このクラスが使われる場所ではPythonがリソース管理の面倒をみてくれるのです。次のトピックは、この FileManager クラスのようなコンテキストマネージャークラスのさまざまな使用方法です。

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

ここで説明することはあまりないので、長い段落を書く代わりに、このセクションでいくつかのコードスニペットを提供することにします。

file = FileManager('readme.txt')
with file as managed_file:
    text = managed_file.read()
    print(text)


with FileManager('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)


def open_file(filename):
    file = FileManager(filename)
    return file


with open_file('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)


覚えておくべき重要なことがわかります。

  1. with文に渡されるオブジェクトはenterexit` のメソッドを持っていなければならない。
    1. __enter__ メソッドは with ブロックで使用されるリソースを返さなければならない。

重要: 議論を簡潔にするために、いくつかの微妙な点を省きました。これらのマジックメソッドの正確な仕様については、こちらの Python ドキュメントを参照してください。

コンテキストリブの使用

Pythonの禅-Pythonの指針を格言集にしたもの-は次のように述べています。

とあります。
シンプルなものは、複雑なものよりも優れている。
というものです。

というものです。

この点を強調するために、Pythonの開発者はコンテキストマネージャに関するユーティリティを含むcontextlibというライブラリを作成しました。ここではそのうちの1つだけを簡単に紹介します。詳細はPythonの公式ドキュメントを参照することをお勧めします。

from contextlib import contextmanager


@contextmanager
def open_file(filename):
    opened_file = open(filename)
    try:
        yield opened_file
    finally:
        opened_file.close()


上のコードのように、try文の中で保護されたリソースをyieldし、続くfinally文の中でそれをクローズする関数を定義すればよいのです。もう一つの理解方法は

  • return以外のすべての内容はenterメソッドのtry` ブロックの前に置かれます – 基本的にはリソースをオープンするための命令です。
  • リソースを返す代わりに、try ブロックの中で yield します。
  • __exit__ メソッドの内容は、対応する finally ブロックの中に入ります。

このような関数ができたら、 contextlib.contextmanager デコレータを使ってデコレートすればOKです。

with open_file('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)


見ての通り、デコレートされた open_file 関数はコンテキストマネージャーを返すので、それを直接使用することができます。これにより、面倒なことをせずに、 FileManager クラスを作成したのと同じ効果を得ることができます。

参考文献

もし、あなたが熱狂的なファンで、コンテキスト・マネージャーについてもっと読みたいと思ったら、以下のリンクをチェックすることをお勧めします。

  • https://docs.python.org/3/reference/compound
  • https://docs.python.org/3/reference/datamodel.html#context-managers
  • https://docs.python.org/3/library/contextlib.html
  • https://rszalski.github.io/magicmethods/
タイトルとURLをコピーしました