Reputation: 402
I have a python class A with with
statement and class B with with
statement. Now it works like:
with A() as a:
with B() as b:
do_things()
How can I build a class C that wrap class A and class B so that I can call it like:
with C() as c:
do_things()
has the identical funtions
Upvotes: 1
Views: 859
Reputation: 281683
If your reason for doing this is to reduce indentation or merge the with
statements, you don't need to. You can just do
with A() as a, B() as b:
...
entering multiple context managers on a single line.
If your C
has other reasons to exist, you need to be careful about handling the case where one context manager fails after another context manager has been created and/or entered. contextlib.ExitStack
can help you implement this robustly, or contextlib2.ExitStack
if you're still on Python 2:
import contextlib
class C(object):
def __enter__(self):
with contextlib.ExitStack() as stack:
stack.enter_context(A())
stack.enter_context(B())
self._stack = stack.pop_all()
return self
def __exit__(self, exc_type, exc_value, exc_tb):
return self._stack.__exit__(exc_type, exc_value, exc_tb)
Upvotes: 1
Reputation: 477318
Short answer: it is possible. But context-managers allow to implement some logic, which makes it "tricky" to implemented it exactly the right way. Below you see a "Proof of Concept", but I do not guarantee that it has exactly the same behavior. Therefore I really advise to work with nested with
s.
What is not covered here: the __init__
, or __enter__
can raise exceptions as well, and these are then handled by the "outer" context managers. This makes it of course rather complicated. You basically would need to "build" a stack in the __enter__
, and then "pop" the stack in case one of the __enter__
s fails. This scenario is not covered here.
We can make a "composite" context manager:
class C:
def __init__(self, *ctxs):
self.ctxs = ctxs
def __enter__(self):
return tuple(ctx.__enter__() for ctx in self.ctxs)
def __exit__(self, self, exception_type, exception_value, traceback):
for ctx in reversed(self.ctxs):
try:
if ctx.__exit__(exception_type, exception_value, traceback):
(exception_type, exception_value, traceback) = (None,) * 3
except Exception as e:
exception_value = e
traceback = e.__traceback__
exception_type = type(e)
return exception_value is None
The __exit__
part is tricky. First of all, we need to exit in reverse order. But the exception handling is even more complicated: if an __exit__
silenced an exception, by returning a "truthful" value, then we should pass (None, None, None)
as (exception_type, exeption_value, traceback)
, but a problem that can occur is that an __exit__
on the other hand triggers an exception itself, and thus then introduces a new exception.
We can then use the context processor like:
with C(A(), B()) as (a,b):
# ...
pass
The above thus allows to implement a context manager for an arbitrary number of "sub-contextmanagers". We can subclass this to generate a specific one, like:
class ContextAB(C):
def __init__(self):
super(ContextAB, self).__init__(A(), B())
and then use this as:
with ContextAB() as (a, b):
# ...
pass
But long story short: use nested with
statements. It also makes it more explicit what is going on here. Right now the C
encapsulate all sorts of logic, which are better made explicit. If entering B
fails, then this should result in an exception that is handled by the __exit__
of A
, etc. This makes it very cumbersome to get the "details" completely equivalent to the semantics of the with
statement.
Upvotes: 3
Reputation: 402922
I'd like to suggest an alternative. You can initialise both a
and b
on the same line:
with A() as a, B() as b:
do_things()
This is more concise and reduces the amount of indentation in deeply nested code.
However, if you absolutely must use a class, then override the __enter__
and __exit__
methods:
class C:
def __enter__(self):
self._a = A()
self._b = B()
return (self._a.__enter__(), self._b.__enter__())
def __exit__(self ,type, value, traceback):
# Cleanup code here.
self._b.__exit__(type, value, traceback)
self._a.__exit__(type, value, traceback)
And then use C
inside a context manager like this:
with C() as (a, b):
do_things()
If you don't want to reference a
and b
, or if you don't plan on doing anything with them, then
with C():
do_things()
Will also work. This should be enough to get started, but please note there are drawbacks as kindly mentioned by users in the comments. The main one being that if self._b.__enter__
throws errors, self._a.__enter__
will need to be cleaned up (this can be done using try-except-finally). Furthermore, some context managers may need to be treated differently based on what resource is being managed.
Upvotes: 3