Simplified solution using contextmanager
Sraw's answer can be simplified as follows:
from contextlib import contextmanager class Foo: def __init__(self, i, o): self.i = i self.o = o @contextmanager def multiopen(i, o): with stack.enter_context(open(i)) as i, stack.enter_context(open(o)) as o: yield Foo(i, o)
You don't need an explicit ExitStack in this case.
Of course, you can use it as follows:
with multiopen() as foo: print(foo)
If the number of objects is not fixed, then it's still recommended to use ExitStack as shown in Sraw's answer.
A context manager that is designed to make it easy to programmatically combine other context managers and cleanup functions, especially those that are optional or otherwise driven by input data.
If you need the resulting object to be both a contextmanager and a class
Use case described in https://stackoverflow.com/a/48957756/5267751 .
For example you may want something like the following to work:
@multiopen() def f(): ... with multiopen() as foo: ...
For this, the object being returned by multiopen() must implement all of __call__, __enter__ and __exit__.
The simplest solution is the following.
@contextmanager def _multiopen(i, o): with stack.enter_context(open(i)) as i, stack.enter_context(open(o)) as o: yield Foo(i, o) class multiopen: def __init__(self, i, o): self._multiopen_instance = _multiopen(i, o) def __enter__(self): self._multiopen_instance.__enter__() return self def __exit__(self, exc_type, exc_value, traceback): return self._multiopen_instance.__exit__(exc_type, exc_value, traceback) def __call__(self): ...
That is, still make a contextmanager, but wrap over it with a class.
Of course, this may looks ugly (you need the @contextmanager decorator, then immediately throw it away and make a class manually). Actually, it's not that bad --- even the recipe in the documentation https://docs.python.org/3/library/contextlib.html#cleaning-up-in-an-enter-implementation reduces a contextmanager.
If you refuse to use @contextmanager
The complexity is not with ExitStack, the complexity is with the with statement itself.
From the documentation:
The following code:
with EXPRESSION as TARGET: SUITE
is semantically equivalent to:
manager = (EXPRESSION) enter = type(manager).__enter__ exit = type(manager).__exit__ value = enter(manager) hit_except = False try: TARGET = value SUITE except: hit_except = True if not exit(manager, *sys.exc_info()): raise finally: if not hit_except: exit(manager, None, None, None)
So, if we want to make a class, of course we cannot use with statement.
I can't figure out how to implement this correctly yet.
- If the first object's
__enter__ raises an exception, then we must not call the first object's __exit__. - However, if the second object's
__enter__ raises an exception, we must call the first object's __exit__. - Furthermore, if the second object's
__exit__ raises an exception, we must proceed to continue to call the first object's __exit__. - The first object's
__exit__ may, in turn, suppress that exception.
(In this context, the open() should be seen as the __enter__. It may be more idiomatic to write a contextmanager that represents a file.)