いくつかのタスクでは、リソースを明示的に解放する複数のオブジェクトを要求するのが一般的です-たとえば、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
自体をi
とo
を処理するコンテキストマネージャーに変換します。
with Foo('in.txt', 'out.txt') as f:
# do stuff
self.i
とself.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
これはかなり満足ですが、私は次の事実に困惑しています。
いくつかの余分なコンテキスト:私は主に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の解決策は何ですか?
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
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
_
@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が正しく閉じられていることがわかります。
私はヘルパーを使用する方が良いと思います:
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
ファイルハンドラーを確実に処理したい場合、最も簡単な解決策は、ファイル名ではなく直接ファイルハンドラーをクラスに渡すことです。
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をシンプルに保つことです。