web-dev-qa-db-ja.com

クラスが所有するオブジェクトのコンテキストマネージャを作成するPythonの方法

いくつかのタスクでは、リソースを明示的に解放する複数のオブジェクトを要求するのが一般的です-たとえば、2つのファイル。これは、タスクがネストされたwithブロックを使用して関数に対してローカルである場合、またはさらに良いことに、複数のwith_item句を含む単一のwithブロックである場合に簡単に実行できます。

with open('in.txt', 'r') as i, open('out.txt', 'w') as o:
    # do stuff

OTOH、私はまだ、そのようなオブジェクトが関数スコープに対してローカルであるだけでなく、クラスインスタンスによって所有されている場合、つまりコンテキストマネージャーがどのように構成されている場合に、これがどのように機能するかを理解するのに苦労しています。

理想的には私は次のようなことをしたいと思います:

class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = WITH(open(in_file_name, 'r'))
        self.o = WITH(open(out_file_name, 'w'))

Foo自体をioを処理するコンテキストマネージャーに変換します。

with Foo('in.txt', 'out.txt') as f:
    # do stuff

self.iself.oは、予想どおりに自動的に処理されます。

私は次のようなものを書くことをいじりました:

class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = open(in_file_name, 'r').__enter__()
        self.o = open(out_file_name, 'w').__enter__()

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        self.i.__exit__(*exc)
        self.o.__exit__(*exc)

しかし、それは冗長であり、コンストラクタで発生する例外に対して安全ではありません。しばらく検索したところ、 2015年のブログ投稿 が見つかりました。これは、contextlib.ExitStackを使用して、私が求めているものと非常によく似たものを取得します。

class Foo(contextlib.ExitStack):
    def __init__(self, in_file_name, out_file_name):
        super().__init__()
        self.in_file_name = in_file_name
        self.out_file_name = out_file_name

    def __enter__(self):
        super().__enter__()
        self.i = self.enter_context(open(self.in_file_name, 'r')
        self.o = self.enter_context(open(self.out_file_name, 'w')
        return self

これはかなり満足ですが、私は次の事実に困惑しています。

  • 私はドキュメントでこの使用法について何も見つけていないので、この問題に取り組むための「公式の」方法ではないようです。
  • 一般に、この問題に関する情報を見つけるのは非常に難しいので、問題に非Pythonの解決策を適用しようとしていると思います。

いくつかの余分なコンテキスト:私は主にC++で動作しますが、ブロックスコープ間に違いはありませんこの種類のクリーンアップはデストラクタ内で実装され(__del__と考えられますが、決定的に呼び出されます)、デストラクタは(明示的に定義されていなくても)自動的にデストラクタを呼び出します。サブオブジェクト。したがって、両方:

{
    std::ifstream i("in.txt");
    std::ofstream o("out.txt");
    // do stuff
}

そして

struct Foo {
    std::ifstream i;
    std::ofstream o;

    Foo(const char *in_file_name, const char *out_file_name) 
        : i(in_file_name), o(out_file_name) {}
}

{
    Foo f("in.txt", "out.txt");
}

通常、必要に応じてすべてのクリーンアップを自動的に実行します。

私はPythonで同様の動作を探していますが、C++からのパターンを適用しようとしているのではないかと心配しています。


つまり、要約すると、クリーンアップが必要なオブジェクトを所有するオブジェクトがコンテキストマネージャー自体になり、その子の__enter__/__exit__を正しく呼び出すという問題に対するPythonicの解決策は何ですか?

25
Matteo Italia

Contextlib.ExitStackはPythonicおよびcanonicalであり、この問題に対する適切な解決策だと思います。この回答の残りの部分は、私がこの結論に至るために使用したリンクと私の思考プロセスを示しています。

元のPython拡張リクエスト

https://bugs.python.org/issue13585

元のアイデア+実装は、Python標準ライブラリの機能強化と推論とサンプルコードの両方で提案されました。レイモンドヘッティンガーやエリックスノーなどのコア開発者によって詳細に議論されました。この問題に関する議論は、元のアイデアが標準ライブラリに適用可能でPythonicに成長したことを明確に示しています。スレッドを要約すると、次のようになります。

nikratioは最初に提案しました:

http://article.gmane.org/gmane.comp.python.ideas/12447 で説明されているCleanupManagerクラスをcontextlibモジュールに追加することを提案します。独自のコンテキストマネージャーが付属していない(PythonまたはPython以外の)リソースを管理するために、汎用のコンテキストマネージャーを追加するというアイデアです。

これは、レッティンガーの懸念に会いました:

これまでのところ、これに対する需要はゼロであり、実際に使用されているようなコードは見たことがありません。 AFAICT、それは単純明快なトライ/ファイナルよりも明らかに良くはありません。

これに対する応答として、これが必要かどうかについて長い議論があり、ncoghlanから次のような投稿がありました。

TestCase.setUp()とTestCase.tearDown()はto__enter __()とexit()の前駆体の中にありました。 addCleanUp()はここでまったく同じ役割を果たします-そして、私は見てきたplentyマイケルに向けられた肯定的なフィードバックのユニットテストAPIへの追加について... ...カスタムコンテキストマネージャーは通常悪い考えですこれらの状況では、それらは読みやすくするため悪い(コンテキストマネージャが何をするかを理解するために人々に依存する)。一方、標準ライブラリベースのソリューションは、両方の長所を提供します。-コードが正しく記述され、正確性を監査しやすくなります(最初にステートメントが追加されたすべての理由により)-イディオムは最終的にはすべての人に馴染みがあるPython users ... ...必要に応じてpython-devでこれを取り上げることができますが、私はあなたにその欲求を説得したいと思いますisそこ...

そして、しばらくしてから再びncoghlanから:

ここでの私の以前の説明はあまり適切ではありません。contextlib2をまとめ始めた直後、このCleanupManagerのアイデアはすぐにContextStack [1]に変わりました。これは、必ずしも対応しない方法でコンテキストマネージャーを操作するためのはるかに強力なツールです。ソースコードで字句スコープを使用します。

例/レシピ/ ExitStackのブログ投稿標準ライブラリのソースコード自体にはいくつかの例とレシピがあり、追加したマージリビジョンで確認できますこの機能: https://hg.python.org/cpython/rev/8ef66c73b1e1

また、元の問題の作成者(Nikolaus Rath/nikratio)からのブログ投稿もあり、ContextStackが優れたパターンである理由を説得力のある方法で説明し、使用例もいくつか示しています。 https://www.rath.org/ on-the-beauty-of-pythons-exitstack.html

9
Matthew Horst

2番目の例は、Python(つまり、ほとんどのPythonic)でそれを行う最も簡単な方法です。ただし、例にはまだバグがあります。2番目のopen()

_self.i = self.enter_context(open(self.in_file_name, 'r')
self.o = self.enter_context(open(self.out_file_name, 'w') # <<< HERE
_

その場合、Foo.__exit__()が正常に返らない限り、Foo.__enter__()が呼び出されないため、予期したときに_self.i_は解放されません。これを修正するには、例外が発生したときにFoo.__exit__()を呼び出すtry-exceptで各コンテキスト呼び出しをラップします。

_import contextlib
import sys

class Foo(contextlib.ExitStack):

    def __init__(self, in_file_name, out_file_name):
        super().__init__()
        self.in_file_name = in_file_name
        self.out_file_name = out_file_name

    def __enter__(self):
        super().__enter__()

        try:
            # Initialize sub-context objects that could raise exceptions here.
            self.i = self.enter_context(open(self.in_file_name, 'r'))
            self.o = self.enter_context(open(self.out_file_name, 'w'))

        except:
            if not self.__exit__(*sys.exc_info()):
                raise

        return self
_
6
user369450

@cpburnzで述べたように、最後の例が最適ですが、2回目のオープンが失敗した場合のバグが含まれています。このバグの回避については、標準ライブラリのドキュメントで説明されています。 ExitStack documentation のコードスニペットと 29.6.2.4 ____enter___実装でのクリーンアップResourceManagerの例を簡単に適応できますMultiResourceManagerクラスを考え出す:

_from contextlib import contextmanager, ExitStack
class MultiResourceManager(ExitStack):
    def __init__(self, resources, acquire_resource, release_resource,
            check_resource_ok=None):
        super().__init__()
        self.acquire_resource = acquire_resource
        self.release_resource = release_resource
        if check_resource_ok is None:
            def check_resource_ok(resource):
                return True
        self.check_resource_ok = check_resource_ok
        self.resources = resources
        self.wrappers = []

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.Push(self)
            yield
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            stack.pop_all()

    def enter_context(self, resource):
        wrapped = super().enter_context(self.acquire_resource(resource))
        if not self.check_resource_ok(wrapped):
            msg = "Failed validation for {!r}"
            raise RuntimeError(msg.format(resource))
        return wrapped

    def __enter__(self):
        with self._cleanup_on_error():
            self.wrappers = [self.enter_context(r) for r in self.resources]
        return self.wrappers

    # NB: ExitStack.__exit__ is already correct
_

これでFoo()クラスは簡単です:

_import io
class Foo(MultiResourceManager):
    def __init__(self, *paths):
        super().__init__(paths, io.FileIO, io.FileIO.close)
_

Try-exceptブロックが必要ないため、これは素晴らしいです。おそらく、そもそもContextManagerを使用してそれらを取り除くだけです!

次に、あなたが望むようにそれを使うことができます(注意_MultiResourceManager.__enter___は渡されたquire_resource()によって与えられたオブジェクトのリストを返します):

_if __name__ == '__main__':
    open('/tmp/a', 'w').close()
    open('/tmp/b', 'w').close()

    with Foo('/tmp/a', '/tmp/b') as (f1, f2):
        print('opened {0} and {1}'.format(f1.name, f2.name))
_

次のスニペットのように_io.FileIO_を_debug_file_に置き換えて、実際の動作を確認できます。

_    class debug_file(io.FileIO):
        def __enter__(self):
            print('{0}: enter'.format(self.name))
            return super().__enter__()
        def __exit__(self, *exc_info):
            print('{0}: exit'.format(self.name))
            return super().__exit__(*exc_info)
_

次に、次のようになります。

_/tmp/a: enter
/tmp/b: enter
opened /tmp/a and /tmp/b
/tmp/b: exit
/tmp/a: exit
_

ループの直前にimport os; os.unlink('/tmp/b')を追加すると、次のようになります。

_/tmp/a: enter
/tmp/a: exit
Traceback (most recent call last):
  File "t.py", line 58, in <module>
    with Foo('/tmp/a', '/tmp/b') as (f1, f2):
  File "t.py", line 46, in __enter__
    self.wrappers = [self.enter_context(r) for r in self.resources]
  File "t.py", line 46, in <listcomp>
    self.wrappers = [self.enter_context(r) for r in self.resources]
  File "t.py", line 38, in enter_context
    wrapped = super().enter_context(self.acquire_resource(resource))
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/b'
_

/ tmp/aが正しく閉じられていることがわかります。

6
fritzelr

私はヘルパーを使用する方が良いと思います:

from contextlib import ExitStack, contextmanager

class Foo:
    def __init__(self, i, o):
        self.i = i
        self.o = o

@contextmanager
def multiopen(i, o):
    with ExitStack() as stack:
        i = stack.enter_context(open(i))
        o = stack.enter_context(open(o))
        yield Foo(i, o)

使用法はネイティブopenに近いです:

with multiopen(i_name, o_name) as foo:
    pass
4
Sraw

ファイルハンドラーを確実に処理したい場合、最も簡単な解決策は、ファイル名ではなく直接ファイルハンドラーをクラスに渡すことです。

with open(f1, 'r') as f1, open(f2, 'w') as f2:
   with MyClass(f1, f2) as my_obj:
       ...

カスタム__exit__機能が必要ない場合は、ネストしてスキップすることもできます。

ファイル名を__init__に渡したい場合は、次のようにして問題を解決できます。

class MyClass:
     input, output = None, None

     def __init__(self, input, output):
         try:
             self.input = open(input, 'r')
             self.output = open(output, 'w')
         except BaseException as exc:
             self.__exit___(type(exc), exc, exc.__traceback__)
             raise

     def __enter__(self):
         return self

     def __exit__(self, *args):
            self.input and self.input.close()
            self.output and self.output.close()
        # My custom __exit__ code

だから、それは本当にあなたのタスクに依存します、pythonで作業するためのオプションがたくさんあります。結局のところ、Pythonicの方法はAPIをシンプルに保つことです。

3
Victor Gavro