It's typical to require for some task multiple objects which have resources to be explicitly released - say, two files; this is easily done when the task is local to a function using nested with blocks, or - even better - a single with block with multiple with_item clauses:
with open('in.txt', 'r') as i, open('out.txt', 'w') as o: # do stuff OTOH, I still struggle to understand how this is supposed to work when such objects aren't just local to a function scope, but owned by a class instance - in other words, how context managers compose.
Ideally I'd like to do something like:
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')) and have Foo itself turn into a context manager that handles i and o, such that when I do
with Foo('in.txt', 'out.txt') as f: # do stuff self.i and self.o are taken care of automatically as you would expect.
I tinkered about writing stuff such as:
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) but it's both verbose and unsafe against exceptions occurring in the constructor. After searching for a while, I found this 2015 blog post, which uses contextlib.ExitStack to obtain something very similar to what I'm after:
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 This is pretty satisfying, but I'm perplexed by the fact that:
- I find nothing about this usage in the documentation, so it doesn't seem to be the "official" way to tackle this problem;
- in general, I find it extremely difficult to find information about this issue, which makes me think I'm trying to apply an unpythonic solution to the problem.
Some extra context: I work mostly in C++, where there is no distinction between the block-scope case and the object-scope case for this issue, as this kind of cleanup is implemented inside the destructor (think __del__, but invoked deterministically), and the destructor (even if not explicitly defined) automatically invokes the destructors of the subobjects. So both:
{ std::ifstream i("in.txt"); std::ofstream o("out.txt"); // do stuff } and
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"); } do all the cleanup automatically as you generally want.
I'm looking for a similar behavior in Python, but again, I'm afraid I'm just trying to apply a pattern coming from C++, and that the underlying problem has a radically different solution that I can't think of.
So, to sum it up: what is the Pythonic solution to the problem of having an object who owns objects that require cleanup become a context-manager itself, calling correctly the __enter__/__exit__ of its children?
ExitStacksolution is perfectly Pythonic.