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__
メソッドはリソースであるファイルをオープンしてそれを返しています。この FileManager
を with
文で使用すると、このメソッドが呼び出され、その戻り値が 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)
覚えておくべき重要なことがわかります。
- with
文に渡されるオブジェクトは
enterと
exit` のメソッドを持っていなければならない。 -
-
__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/