[Python] Pythonic way to compose context managers for objects owned by a class

Discussão em 'Python' iniciado por Stack, Julho 21, 2025.

  1. Stack

    Stack Membro Participativo

    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::eek:fstream o("out.txt");
    // do stuff
    }


    and

    struct Foo {
    std::ifstream i;
    std::eek:fstream 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?

    Continue reading...

Compartilhe esta Página