Context Manager
はじめに
コンテクストマネージャは、with
文を裏で支えているオブジェクトです。
この文章では、with
文の基本的な使用法を説明した上で、コンテクストマネージャの定義の仕方や、with
文とコンテクストマネージャとの関係について述べていきます。
with
文
with
文は、例外や return
文などによりプログラムの実行が不可能となった際にも、特定の処理を確実に実行するための制御構造です。
たとえばファイルのオープンとクローズ、ロックの取得と解放、データベースとの接続の確立と切断など、あるリソースを管理する際に対をなして現れるような処理を簡潔に記述する際などによく用いられます。
こうした処理は、伝統的には try/finally
によるパターンで記述されていました。
with
文は、この try/finally
パターンを抽象化するための糖衣構文であるといえます。
たとえば次のコードは、ファイルをオープンして文字列を書き込み、ファイルをクローズするという処理を try/finally
により実装する例です:
>>> f = open('foo.txt', 'w')
>>> try:
... f.write('hello world')
... finally:
... f.close()
...
11
上の処理を with
文を利用して書くと次のようになります:
>>> with open('foo.txt', 'w') as f:
... f.write('hello world')
...
11
>>> f.closed
True
with
文ではファイルを明示的にクローズする処理は書かれていませんが、closed
アトリビュートを確認することにより、ファイルがきちんとクローズされていることがわかります。
with
ブロック内で例外が発生したとしても、
>>> with open('foo.txt', 'w') as f:
... 1 / 0
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
>>> f.closed
True
から、同様にファイルはクローズされることがわかります。
このように、with
文を利用すると、try/finally
により記述しなければならなかった処理を簡潔に記述することができます。
with
文の挙動は、コンテクストマネージャというオブジェクトにより制御されています。
上の例において、ブロックを抜けた際にファイルがクローズされているのは、コンテクストマネージャによってそのように指定されているからに他なりません。
また、as f
という記述により f
にファイルオブジェクトが代入されるという挙動も、コンテクストマネージャによって指定されたものです。
以下では、コンテクストマネージャが with
文を制御する仕組みについて確認していきましょう。
コンテクストマネージャ
コンテクストマネージャ (context manager) は、__enter__
と __exit__
という二つのメソッドを実装したクラスのインスタンスです。
__enter__
メソッドには、その名の通り、with
ブロックに入る際の処理を記述します。
具体的には、たとえばファイルオブジェクトの生成や、データベースとの接続の初期化など、リソースを準備する処理などが実行されます。
__enter__
メソッドから値を返すと、その値は as x
における x
に代入されます。
したがって、__enter__
メソッドで初期化したオブジェクトを with
ブロック内で利用するために受け渡したい場合などは、ここで適切な値を返す必要があります。
with
ブロックから抜ける際に実行される処理を記述する場所が、__exit__
メソッドです。
たとえば、ファイルのクローズ処理やロックの解放など、一連の処理から抜ける際に実行する必要のある処理をここに記述します。
try/finally
パターン における finally
のブロックに相当する処理をここに記述します。
それでは、ここまでの説明を理解するために、ファイルのオープンとクローズを制御するコンテクストマネージャを実装してみましょう:
>>> class FileManager:
... def __init__(self, name):
... self.name = name
... def __enter__(self):
... self.file = open(self.name, 'w')
... return self.file
... def __exit__(self, exc_type, exc_val, exc_tb):
... if self.file:
... self.file.close()
...
>>> with FileManager('foo.txt') as f:
... f.write('hello world')
...
11
FileManager
クラスは __enter__
と __exit__
メソッドを実装しているので、そのインスタンスはコンテクストマネージャとなります。
FileManager('foo.txt')
によりインスタンスが生成されますが、これが with
文へと渡されることで、インスタンスの __enter__
メソッドが呼ばれます。
__enter__
の内部では、ファイルオブジェクトが生成され、その値を返しています。
これにより、as
に与えられた変数 f
へとファイルオブジェクトが代入されます。
f
への書き込みが終了し with
ブロックを抜ける際には __exit__
が呼ばれ、ここでファイルをクローズしています。
ここでは利用しませんでしたが、__exit__
には、with
ブロックで発生した例外のクラス、例外のインスタンス、そしてトレースバックオブジェクトが引数として与えられます。
with
ブロックで例外が発生しなければ、これらはすべて None
となります。
なお、with
ブロックで例外が発生した場合に __exit__
の戻り値を True
とすると、例外が __exit__
から再送出されることを抑制します。
戻り値が True
でなければ例外は自動で再送出されるため、raise
文を使う必要はありません。
ところで、with
文を利用せずに、__enter__
と __exit__
を直接呼ぶことも可能です。
with
文の挙動についてさらに理解を深めるために、これらのメソッドを手動で呼んでみましょう:
>>> manager = FileManager('foo.txt')
>>> f = manager.__enter__() # __enter__はファイルオブジェクトを返す
>>> f.closed
False
>>> f.write('hello world')
11
>>> manager.__exit__(None, None, None)
>>> f.closed # __exit__によりファイルオブジェクトはクローズされている
True
以上のように、コンテクストマネージャは __enter__
メソッドと __exit__
メソッドを定義することで、with
文の挙動を制御しているといえます。
これは、イテレータが __iter__
メソッドと __next__
メソッドにより for
文を制御していることと似ています。
contextlib.contextmanager
を利用する
Python の標準ライブラリには、contextlib
という、with
文のためのユーティリティを集めたライブラリが存在します。
これに含まれる contextlib.contextmanager
というデコレータを利用することにより、ジェネレータを定義するだけでコンテクストマネージャを作成することができます。
contextlib.contextmanager
デコレータを付加したジェネレータ関数において、yield
式の手前の処理がコンテクストマネージャにおける __enter__
に、yield
式より下が __exit__
に対応します。また、yield
式に与える値が、as
においてバインドされます。すなわち、
class ContextManager:
....
def __enter__(self):
# 前処理
return x
def __exit__(self, exc_type, exc_val, exc_tb):
# 後処理
というコンテキストマネージャは、
@contextlib.contextmanager
def context_manager():
# 前処理
try:
yield x
finally:
# 後処理
のようなジェネレータ関数へと書き換えることができます。
これが with
文において実行されると、yield
の行で処理が停止し、 with
ブロックの実行が開始されます。
そしてブロックを抜けたところで yield
の行から処理が再開されることになります。
yield x
と後処理の部分が try/finally
により囲まれているのは、with
ブロックにおいて例外が発生したときに、yield
の行で再度送出される例外をハンドリングするためです。
たとえば、上で書いた FileManager
と同等のジェネレータ関数は次のようになります:
@contextlib.contextmanager
def file_manager(name):
f = open(name, 'w')
try:
yield f
finally:
f.close()
使用方法はこれまでと同様です:
>>> with file_manager('foo.txt') as f:
... f.write('hello world')
...
11
このように、contextlib.contextmanager
デコレータにより、クラスを定義することなく、より簡潔にコンテクストマネージャを記述することができます。
まとめ
with
文はtry/finally
パターンを抽象化するための糖衣構文である
- コンテクストマネージャは
with
文を制御しているオブジェクトである- 前処理としての
__enter__
と、後処理としての__exit__
メソッドをもつ
contextlib.contextmanager
デコレータにより、ジェネレータを定義するだけでコンテクストマネージャを実装することができる