Let’s say you have a custom context manager. Without loss of generality, consider:
class MyGreeter(object):
def __init__(self, name):
self.name = name
def __enter__(self):
print(self.name, 'saying hello.')
return len(self.name)
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.name, 'saying goodbye.')
return False
with MyGreeter('Mori') as thing:
print(thing)
# Output:
# Mori saying hello.
# 4
# Mori saying goodbye.
This is fine, but you might want to extend the functionality and depend on
another context manager. Let’s say, in the above example, we wanted to print
the message to a temporary file instead of to the console. Typical usage of
NamedTemporaryFile
would look like this:
import tempfile
with tempfile.NamedTemporaryFile() as f:
f.write('...')
But now we’re in trouble. We’d like f to be accessible in the __enter__
and
__exit__
methods. But we can’t have a reusable class defined inside of a
with
construct.
So the question is, how do we compose these two context managers while still being mindful of failure semantics?
Solution 1: Invoke context manager methods directly
Something like this would work:
import tempfile
class MyGreeter(object):
def __init__(self, name):
self.name = name
self.tempfile_manager = tempfile.NamedTemporaryFile(
mode='w', encoding='utf-8')
def __enter__(self):
self.tempfile = self.tempfile_manager.__enter__()
self.tempfile.write('{} saying hello'.format(self.name))
return self.tempfile # or whatever
def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.tempfile.write('{} saying goodbye'.format(self.name))
finally:
return self.tempfile_manager.__exit__(exc_type, exc_val, exc_tb)
with MyGreeter('Bob') as thing:
pass
Recall that, according to the specification
for ContextManagers, the three parameters to __exit__
identify a currently
handled exception. If no exception is currently being handled, all three values
are None. Furthermore, __exit__
returns True if a currently handled exception
should be re-thrown, or False if it should be swallowed.
This enables ContextManagers to ease cognitive load of their users – if certain exceptions are recoverable, the author can do the recovery logic inside the CM itself.
Our simple Greeter has no special semantics for exception handling, so we defer
to our inner CM tempfile_manager. If an exception is currently being handled,
maybe NamedTemporaryFile
will want to do something special with it.
Beware though: When composing CMs like this, you could in theory swallow
exceptions that someone up the stack expected to see for their API to recover
properly. So if you catch any of these exceptions, you should probably
re-raise
.
Solution 2: Use the contextlib
decorator
If you have no exception handling semantics, you can use something much more concise.
import contextlib
import tempfile
@contextlib.contextmanager
def my_greeter(name):
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') as temp:
temp.write('{} saying hello\n'.format(name))
try:
yield name
finally:
temp.write('{} saying goodbye\n'.format(name))
The cool thing is, since generators pass exceptions through to the consumer, NamedTemporaryFile gets to see the exceptions it was counting on seeing.