はじめに
この記事は Python Advent Calendar その 2 の 7 日目の記事です。
僕はふだんは Ruby や JavaScript のコードを書いています。一方 Python は趣味で使っていて、Python 歴は 1 年ほどです。この言語を勉強していて with 文が気に入ったので、今回はこの with 文とコンテキストマネージャについて語ります。
with 文について
with 文が Python に導入された経緯については PEP 343 の Abstract で次のように書かれています。
This PEP adds a new statement "with" to the Python language to make it possible to factor out standard uses of try/finally statements.
try/finally
の標準的な使い方を抽出する目的で追加したとのことです。
例えば open() でファイルを開く場合、最終的にファイルが必ず閉じられることを保証するために try/finally
を使うことができます。
def divide_by_zero(n):
return n / 0
>>> f = None
>>> try:
... f = open('hidamari.txt', 'r')
... divide_by_zero(len(f.read()))
... finally:
... if f:
... f.close()
...
ZeroDivisionError: division by zero
>>> print(f.closed)
True
ここで with 文を使うと、同じことをよりシンプルなコードで実現できます。
>>> with open('hidamari.txt', 'r') as f:
... divide_by_zero(len(f.read()))
...
ZeroDivisionError: division by zero
>>> print(f.closed)
True
ちなみに、この with open()
を自作すると次のようなコードになります。
class Reading:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename, 'r')
return self.file
def __exit__(self, type, value, traceback):
self.file.close()
>>> with Reading('hidamari.txt') as f:
... divide_by_zero(len(f.read()))
...
ZeroDivisionError: division by zero
>>> print(f.closed)
True
with 文に渡されるオブジェクトをコンテキストマネージャと呼びます。上記の with 文の f
にはコンテキストマネージャの __enter__
の返り値がバインドされます。そして with ブロックを抜けたときに __exit__
が呼び出されるという仕組みです。__enter__
が前処理、__exit__
が後処理のイメージですね。
毎回 __enter__
と __exit__
を持つコンテキストマネージャを定義するのは面倒です。そこで @contextlib.contextmanager というデコレータを使うと、コンテキストマネージャを簡単に定義できます。
import contextlib
@contextlib.contextmanager
def reading(filename):
file = open(filename, 'r')
try:
yield file
finally:
file.close()
>>> with reading('hidamari.txt') as f:
... print(f.read())
...
ひだまり荘で、待ってます。
>>> print(f.closed)
True
なお with ブロック内で発生した例外は yield
を実行している箇所で再送出されます。with ブロック内でユーザがどんなコードを実行するのかはコンテキストマネージャからは予想できません。そのため yield
は try/finally
で囲っておく必要があります。
import contextlib
@contextlib.contextmanager
def reading_incompletely(filename):
file = open(filename, 'r')
yield file
file.close() # 呼ばれない可能性がある。
>>> with reading_incompletely('hidamari.txt') as f:
... divide_by_zero(len(f.read()))
...
ZeroDivisionError: division by zero
>>> print(f.closed) # file が close されていない。
False
もうひとつの例
コンテキストマネージャに前処理と後処理を任せることで、with 文を使うユーザはコードをよりシンプルに書くことができました。そして、コンテキストマネージャにはもうひとつの利点があると僕は考えます。それは、限られた範囲でのみ特別な処理を行う場合です。例えば、既存の組み込み関数にモンキーパッチを当てる場合などです。
次の例では sys.stdout.write()
を独自の関数に上書きすることで、print()
を呼び出すたびに出力を上書きできるようにしています。
import contextlib
@contextlib.contextmanager
def overwriting():
import sys
original_write = sys.stdout.write # 元の関数を保持しておく。
def overwrite(text):
original_write('\033[K') # カーソル位置から行末までを消す。
original_write('\r') # カーソルを行頭に移動する。
original_write(text.rstrip('\n')) # print() で出力する文字列の末尾の改行を取り除く。
sys.stdout.write = overwrite # 関数を入れ替える。
try:
yield
finally:
sys.stdout.write = original_write # 関数を元に戻す。忘れると大変。
print('※ sys.stdout.write を元に戻しました。')
if __name__ == '__main__':
import time
# この with ブロックだけ print() の挙動が特殊になる。
with overwriting():
print('カウントダウン開始!')
time.sleep(0.4)
for i in range(10, -1, -1):
print(i)
time.sleep(0.4)
print('')
このように限られた範囲でのみ特別な処理を行う場合に、@contextlib.contextmanager
を使って独自のコンテキストマネージャを作成し、with 文を使うことで
- with 文のシンタックスによって、限定されている範囲が分かりやすい。
- 重要な後処理を必ず実行することを保証できる。
というメリットがあると思います。