LoginSignup
114
89

More than 5 years have passed since last update.

with 文と @contextlib.contextmanager が便利

Last updated at Posted at 2017-12-06

はじめに

この記事は 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 文のシンタックスによって、限定されている範囲が分かりやすい。
  • 重要な後処理を必ず実行することを保証できる。

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

参考

114
89
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
114
89