Edited at
pythonDay 7

with 文と @contextlib.contextmanager が便利

More than 1 year has passed since last update.


はじめに

この記事は Python Advent Calendar その 2 の 7 日目の記事です。

僕はふだんは Ruby や JavaScript のコードを書いています。一方 Python は趣味で使っていて、Python 歴は 1 年ほどです。この言語を勉強していて with 文が気に入ったので、今回はこの with 文とコンテキストマネージャについて語ります。


with 文について

with 文が Python に導入された経緯については PEP 343Abstract で次のように書かれています。


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 ブロック内でユーザがどんなコードを実行するのかはコンテキストマネージャからは予想できません。そのため yieldtry/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() を呼び出すたびに出力を上書きできるようにしています。


countdown.py

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('')


anime.gif

このように限られた範囲でのみ特別な処理を行う場合に、@contextlib.contextmanager を使って独自のコンテキストマネージャを作成し、with 文を使うことで


  • with 文のシンタックスによって、限定されている範囲が分かりやすい。

  • 重要な後処理を必ず実行することを保証できる。

というメリットがあると思います。


参考