pythonコンテキストマネージャ(context manager)

8383 ワード

まず、コンテキストマネージャとは?コンテキストマネージャは、コンテキスト管理プロトコルを実装するオブジェクトです.主に各種のグローバル状態の保存と復元、ファイルの閉じるなどに用いられ、コンテキストマネージャ自体が装飾器である.次のセクションでは、コンテキストマネージャについて説明します.
コンテキスト管理プロトコル
コンテキスト管理プロトコルには、次の2つの方法があります.
  • contextmanager.__enter__()メソッドからランタイムコンテキストに入り、現在のオブジェクトまたはランタイムコンテキストに関連する他のオブジェクトを返します.with文にasキーワードが存在する場合、戻り値はas後の変数にバインドされます.
  • contextmanager.__exit__(exc_type, exc_val, exc_tb)実行時コンテキストを終了し、処理する必要がある異常があるかどうかを示すブール値を返します.with文体の実行中に異常が発生した場合、終了時にパラメータに異常タイプ、異常値、異常追跡情報が含まれます.そうしないと、3つのパラメータはNoneになります.

  • 一般的なファイルの開く操作を見てみましょう
    >>> with open("/etc/hosts", "r") as file:
    ...     dir(file)
    ... 
    ['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']
    

    Open関数は、コンテキスト管理プロトコルを実装したファイルタイプ変数を返します.
    with文
    コンテキストマネージャといえばwith文を言わざるを得ません.なぜですか.with文はコンテキストマネージャをサポートするために存在するため、コンテキスト管理プロトコルを使用してコードブロック(with文体)の実行を包み、try...except...finallyは使いやすいパッケージを提供しています.with文の構文は次のとおりです.
    with EXPR as VAR:
        BLOCK
    

    withとasはキーワードであり、EXPRはコンテキスト式であり、任意の式(式であり、式リストではない)であり、VARは付与されたターゲット変数である.「as VAR」はオプションです.上記の文の最下位の実装は、次のように記述できます.
    mgr = (EXPR)
    exit = type(mgr).__exit__  #      
    value = type(mgr).__enter__(mgr)
    exc = True
    try:
        try:
            VAR = value  #    "as VAR"
            BLOCK
        except:
            #        
            exc = False
            if not exit(mgr, *sys.exc_info()):
                raise
            #   __exit__    false,      ;       ,      
    finally:
        if exc:
            exit(mgr, None, None, None)
    

    これでwith文の実行過程が明らかになります.
  • コンテキスト式を実行し、コンテキストマネージャ
  • を取得する.
  • コンテキストマネージャの__exit__()メソッドをロードして、後期呼び出し
  • に備える.
  • コンテキストマネージャを呼び出す__enter__()メソッド
  • with文に指定されたターゲット変数がある場合、__enter__()メソッドから取得された関連オブジェクトは、ターゲット変数
  • に割り当てられる.
  • 実行with文体
  • コンテキストマネージャの__exit__()メソッドを呼び出します.with文体による異常終了であれば、異常タイプ、異常値、異常追跡情報は__exit__()に渡されます.そうでなければ、3つのパラメータはNoneです.

  • 複数の式をまとめることもできます.
    with A() as a, B() as b:
        BLOCK
    

    それは
    with A() as a:
        with B() as b:
            BLOCK
    

    注意:マルチコンテキスト式はpython 2.7からサポートされています.
    カスタムコンテキストマネージャ
    class ContextManager(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 ContextManager():
        print "OK, we can do something here~~"
        
    #  
    __init__()
    __enter__()
    OK, we can do something here~~
    __exit__()
    

    この例ではwith文はasキーワードを用いず,現在のオブジェクトを返す.次に、現在のオブジェクトではないオブジェクトを返す例を見てみましょう.
    class InnerContext(object):
        def __init__(self, obj):
            print 'InnerContext.__init__(%s)' % obj
    
        def do_something(self):
            print 'InnerContext.do_something()'
    
        def __del__(self):
            print 'InnerContext.__del__()'
    
    class ContextManager(object):
        def __init__(self):
            print 'ContextManager.__init__()'
    
        def __enter__(self):
            print 'ContextManager.__enter__()'
            return InnerContext(self)
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print "ContextManager.__exit__()"
    
    with ContextManager() as obj:
        obj.do_something()
        print "OK, we can do something here~~"
        
    #  
    ContextManager.__init__()
    ContextManager.__enter__()
    InnerContext.__init__(<__main__.contextmanager object="" at="">)
    InnerContext.do_something()
    OK, we can do something here~~
    ContextManager.__exit__()
    InnerContext.__del__()
    

    次に異常処理の例を見てみましょう
    class ContextManager(object):
        def __init__(self, flag):
            print 'ContextManager.__init__(%s)' % flag
            self.flag = flag
    
        def __enter__(self):
            print 'ContextManager.__enter__()'
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print 'ContextManager.__exit__(%s, %s, %s)' % (exc_type, exc_val, exc_tb)
            return self.flag
    
    with ContextManager(True):
        raise RuntimeError('error message handled')
    
    print
    with ContextManager(False):
        raise RuntimeError('error message propagated')
    
    #  
    ContextManager.__init__(True)
    ContextManager.__enter__()
    ContextManager.__exit__(, error message handled, )
    
    ContextManager.__init__(False)
    ContextManager.__enter__()
    ContextManager.__exit__(, error message propagated, )
    Traceback (most recent call last):
      File "ContextManager.py", line 19, in 
        raise RuntimeError('error message propagated')
    RuntimeError: error message propagated
    

    contextlibモジュール
    コンテキストの管理についてpythonは、同じメカニズムを実装するために組み込まれたモジュールcontextlibも提供し、ジェネレータと装飾器によって実装されるコンテキストマネージャは、with文と手動でコンテキスト管理プロトコルを実装するよりも優雅に見えます.contextlibは、異なるシーンでそれぞれ使用される3つの相関関数を提供します.
    contextlib.contextmanager(func)
    コンテキスト管理プロトコルを手動で実装するのは難しくありませんが、コンテキスト管理プロトコルを実装する必要がなく、contextmanagerを使用してジェネレータをコンテキストマネージャに変換することができます.
    import contextlib
    
    @contextlib.contextmanager
    def createContextManager(name):
        print '__enter__ %s' %  name
        yield name
        print '__exit__ %s' % name
    
    with createContextManager('foo') as value:
        print value
    
    #  
    __enter__ foo
    foo
    __exit__ foo
    

    contextlib.nested(mgr1[, mgr2[, ...]])
    複数のコンテキストを同時に管理する必要がある場合があります.この場合、nestedを使用して、複数のコンテキストマネージャをネストされた上下マネージャに結合できます.
    with contextlib.nested(createContextManager('a'),createContextManager('b'),createContextManager('c')) as (a, b, c):
        print a, b, c
        
    #  
    __enter__ a
    __enter__ b
    __enter__ c
    a b c
    __exit__ c
    __exit__ b
    __exit__ a
    

    に等しい
    with createContextManager('a') as a:
        with createContextManager('b') as b:
            with createContextManager('c') as c:
                print a, b, c
    
    #  
    __enter__ a
    __enter__ b
    __enter__ c
    a b c
    __exit__ c
    __exit__ b
    __exit__ a
    

    python 2.7以降、withは複数のコンテキストの直接ネスト操作をサポートしているので、nestedはもう使用することをお勧めしません.
    with createContextManager('a') as a,createContextManager('b') as b,createContextManager('c') as c:
        print a, b, c
        
    #  
    __enter__ a
    __enter__ b
    __enter__ c
    a b c
    __exit__ c
    __exit__ b
    __exit__ a
    

    contextlib.closing(thing)
    本明細書の最初の例から分かるように、ファイルクラスはコンテキスト管理プロトコルをサポートし、with文を直接使用することができ、一部のオブジェクトはプロトコルをサポートしていないが、使用時に正常に終了することを確保し、closingを使用してコンテキストマネージャを作成することができる.それは
    from contextlib import contextmanager
    
    @contextmanager
    def closing(thing):
        try:
            yield thing
        finally:
            thing.close()
    

    ただし、closingの使用は次のようになります.
    from contextlib import closing
    import urllib
    
    with closing(urllib.urlopen('http://www.python.org')) as page:
        for line in page:
            print line
    

    ページを閉じる必要はありません.異常を投げてもページは正常に閉じます.
    注意:with文体(with statement body)with文では、withに包まれた文体には非常に正式な名前はありませんが、一般的にはそう呼ばれています.
    reference
  • https://www.python.org/dev/peps/pep-0343/
  • https://docs.python.org/2.7/library/stdtypes.html#typecontextmanager
  • https://docs.python.org/2.7/reference/compound_stmts.html#with
  • https://docs.python.org/2.7/library/contextlib.html?highlight=contextlib#contextlib.contextmanager
  • https://pymotw.com/2/contextlib/