contextlib – コンテキストマネージャユーティリティ

目的:コンテキストマネージャを作成して操作するためのユーティリティ
利用できるバージョン:2.5

contextlib モジュールはコンテキストマネージャと with 文を連携させるユーティリティを提供します。

Note

コンテキストマネージャは with 文と関係があります。 with 文は Python 2.6 から公式対応になるので Python 2.5 で contextlib を使用するには from __future__ からインポートする必要があります。

コンテキストマネージャ API

コンテキストマネージャ はコードブロック内のリソース管理に責任を持ちます。それはブロックに入るときにそのリソースが生成されて、ブロックから出るときにクリーンアップするような場合です。例えば、コンテキストマネージャの API をサポートするファイルは、そのファイルが全て読み込み、または書き込みされた後でクローズされることを保証するのが簡単になります。

with open('/tmp/pymotw.txt', 'wt') as f:
    f.write('contents go here')
# ファイルは自動的にクローズされる

コンテキストマネージャは with 文により利用可能となり、その API は2つのメソッドを実行します。 __enter__() メソッドは実行フローが with 内部のコードブロックに入るときに実行されます。そしてコンテキスト内で使用されるオブジェクトを返します。実行フローが with ブロックを出るときにコンテキストマネージャの __exit__() メソッドが使用されたリソースをクリーンアップするために呼ばれます。

class Context(object):

    def __init__(self):
        print '__init__()'

    def __enter__(self):
        print '__enter__()'
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print '__exit__()'
        
with Context():
    print 'Doing work in the context'

コンテキストマネージャと with 文を組み合わせることは try:finally ブロックをよりも小さなコーディングで済みます。それはエラーが発生したとしても、コンテキストマネージャの __exit__() メソッドは常に呼び出されるからです。

$ python contextlib_api.py
__init__()
__enter__()
Doing work in the context
__exit__()

__enter__()with 文の as で指定された名前に関連付けられるオブジェクトを返します。このサンプルでは、 Context はオープンされたコンテキストを使用するオブジェクトを返します。

class WithinContext(object):

    def __init__(self, context):
        print 'WithinContext.__init__(%s)' % context
        
    def do_something(self):
        print 'WithinContext.do_something()'

    def __del__(self):
        print 'WithinContext.__del__'
        

class Context(object):

    def __init__(self):
        print 'Context.__init__()'

    def __enter__(self):
        print 'Context.__enter__()'
        return WithinContext(self)

    def __exit__(self, exc_type, exc_val, exc_tb):
        print 'Context.__exit__()'
    
with Context() as c:
    c.do_something()

少し混乱するかもしれませんが、変数 c に関連付けられた値は __enter__() が返すオブジェクトで with 文で作成された Context インスタンスでは ありません

$ python contextlib_api_other_object.py
Context.__init__()
Context.__enter__()
WithinContext.__init__(<__main__.Context object at 0x1004606d0>)
WithinContext.do_something()
Context.__exit__()
WithinContext.__del__

__exit__() メソッドは with ブロック内で発生した例外の詳細を引数により受け取ります。

class Context(object):

    def __init__(self, handle_error):
        print '__init__(%s)' % handle_error
        self.handle_error = handle_error

    def __enter__(self):
        print '__enter__()'
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print '__exit__(%s, %s, %s)' % (exc_type, exc_val, exc_tb)
        return self.handle_error
        
with Context(True):
    raise RuntimeError('error message handled')

print

with Context(False):
    raise RuntimeError('error message propagated')

コンテキストマネージャが例外を扱う場合、その例外を伝搬する必要がないなら __exit__() は True を返します。False を返すことは __exit__() が返された後でその例外を再発生させることになります。

$ python contextlib_api_error.py
__init__(True)
__enter__()
__exit__(<type 'exceptions.RuntimeError'>, error message handled, <traceback object at 0x100467560>)

__init__(False)
__enter__()
__exit__(<type 'exceptions.RuntimeError'>, error message propagated, <traceback object at 0x1004675f0>)
Traceback (most recent call last):
  File "contextlib_api_error.py", line 30, in <module>
    raise RuntimeError('error message propagated')
RuntimeError: error message propagated

ジェネレータからコンテキストマネージャへ

伝統的な手法であるコンテキストマネージャを作成することは難しいことではありません。コンテキストマネージャは __enter__()__exit__() メソッドを持つクラスを書くことで作成できます。しかし、あなたが些細なコンテキストを管理したいだけならちょっと面倒になるときがあります。そういった状況では、ジェネレータの機能をコンテキストマネージャの中に転用して contextmanager() デコレータを使用することができます。

ジェネレータはコンテキストを初期化すべきで yield が1回呼び出されてからコンテキストをクリーンアップします。もし yield された値があれば with 文の as で指定された変数にセットされます。with ブロック内からの例外はその発生場所で扱えるようにジェネレータ内部で再発生させます。

import contextlib

@contextlib.contextmanager
def make_context():
    print '  entering'
    try:
        yield {}
    except RuntimeError, err:
        print '  ERROR:', err
    finally:
        print '  exiting'

print 'Normal:'
with make_context() as value:
    print '  inside with statement:', value

print
print 'Handled error:'
with make_context() as value:
    raise RuntimeError('showing example of handling an error')

print
print 'Unhandled error:'
with make_context() as value:
    raise ValueError('this exception is not handled')

ジェネレータはコンテキストを初期化し、厳密に一度だけ yield します。それからそのコンテキストをクリーンアップします。yield される値があるなら with 文の as の変数に束縛されます。 with ブロック内の例外は、ジェネレータ内部で扱うことができるので再発生させます。

$ python contextlib_contextmanager.py
Normal:
  entering
  inside with statement: {}
  exiting

Handled error:
  entering
  ERROR: showing example of handling an error
  exiting

Unhandled error:
  entering
  exiting
Traceback (most recent call last):
  File "contextlib_contextmanager.py", line 34, in <module>
    raise ValueError('this exception is not handled')
ValueError: this exception is not handled

ネストされたコンテキスト

時々、複数のコンテキストを同時に管理する必要があります(例えば、入出力のファイルハンドラ間でデータをコピーするときです)。そういったときに複数のコンテキスト間で with 文をネストすることができます。もし外側のコンテキストが独立したブロックである必要性がない場合、何の利点はなくてもインデントレベルを追加してしまいます。 nested() を使用すると1つの with 文でそういったコンテキストをネストします。

import contextlib

@contextlib.contextmanager
def make_context(name):
    print 'entering:', name
    yield name
    print 'exiting :', name

with contextlib.nested(make_context('A'), make_context('B'), make_context('C')) as (A, B, C):
    print 'inside with statement:', A, B, C

entering したコンテキストとは逆の順番で exiting していることに注意してください。

$ python contextlib_nested.py
entering: A
entering: B
entering: C
inside with statement: A B C
exiting : C
exiting : B
exiting : A

Python 2.7 以上では、 with 文が直接ネストすることをサポートしたことにより nested() は廃止予定です。

import contextlib

@contextlib.contextmanager
def make_context(name):
    print 'entering:', name
    yield name
    print 'exiting :', name

with make_context('A') as A, make_context('B') as B, make_context('C') as C:
    print 'inside with statement:', A, B, C

それぞれのコンテキストマネージャとオプションの as はカンマ (,) で分割されます。その効果は nested() を使用するのと似ていますが、 nested() が正しく実装できないエラー処理関連の特別な状況を回避します。

$ python contextlib_nested_with.py
entering: A
entering: B
entering: C
inside with statement: A B C
exiting : C
exiting : B
exiting : A

オープンハンドラをクローズする

file() クラスはコンテキストマネージャの API を直接サポートしますが、オープンハンドラを表す他のオブジェクトによってはサポートしないものもあります。 contextlib の標準ライブラリドキュメントで説明されている例では urllib.urlopen() から返されるオブジェクトがサポートしないものに該当します。 close() メソッドを使用するレガシーなクラスが他にもありますが、コンテキストマネージャ API をサポートしません。ハンドラがクローズされることを保証するには、そういったコンテキストマネージャを作成するための closing() を使用してください。

import contextlib

class Door(object):
    def __init__(self):
        print '  __init__()'
    def close(self):
        print '  close()'

print 'Normal Example:'
with contextlib.closing(Door()) as door:
    print '  inside with statement'

print
print 'Error handling example:'
try:
    with contextlib.closing(Door()) as door:
        print '  raising from inside with statement'
        raise RuntimeError('error message')
except Exception, err:
    print '  Had an error:', err

with ブロック内でエラーが発生するか否かに関わらずハンドラがクローズされます。

$ python contextlib_closing.py
Normal Example:
  __init__()
  inside with statement
  close()

Error handling example:
  __init__()
  raising from inside with statement
  close()
  Had an error: error message

See also

contextlib
本モジュールの標準ライブラリドキュメント
PEP 343
with
Context Manager Types
標準ライブラリドキュメントのコンテキストマネージャ API の説明
With Statement Context Managers
Python リファレンスガイドのコンテキストマネージャ API の説明
Bookmark and Share