Context Managers¶
When using resources like files, memory, network connections, … it is desirable to release the resources after they aren’t in use anymore. Otherwise it can cause several issues like not being able to create another connection or various memory leaks.
Of course this could be avoided like in the following code:
connection = create_connection()
# use the connection, for example send bytes
connection.shutdown()
file = open("foo.txt")
# do something with the file, for example read and write bytes
file.close()
lock = acquire_lock()
# run code that requires exclusive access
lock.release()
db = database_open()
db.transaction()
# execute SQL statements for example db.execute("INSERT INTO foo VALUES ('bar')")
db.commit()
db.close()
This approach has some drawbacks
It requires to standardize the releasing method (for example to always to use
close
).The releasing method may be called conditionally at different places. That means it is not closed in all required cases.
The releasing method might never be called because it is just forgotten, the documentation wasn’t read, …
Errors may occur and exceptions will be raised.
Especially the last item would always require to write code such as:
file = open("foo.txt")
try:
# do something with the file
finally:
file.close()
where do something with the file can be very long, contain function calls, conditionals, additional error handling and therefore may not always be obvious. It gets verbose very quickly and is prone to forget some release action.
Context Manager Protocol¶
As you have seen in the previous examples handling resources consists of two phases: 1. acquiring (or setup) 2. releasing (or shutdown)
Context Managers have been introduced to handle the process of acquiring and releasing of resources automatically even under error conditions.
Let us take a look at the Context Manager base class:
class ContextManager:
def __enter__(self):
"""
Setup and acquire the resource and return it
"""
def __exit__(self, exc_type, exc_value, traceback):
"""
Shutdown and release the resource even if an error was raised
"""
The __enter__
method is intended to setup and acquire a resource. The resource
object or an object handling the resource may be returned from the method
optionally.
The __exit__
method is intended to shutdown and release the resource and gets
the current error information passed if an error was raised.
The With Statement¶
Context Managers are used in conjunction with the with
statement.
The with
statement is defined as:
with EXPRESSION as TARGET:
BLOCK
where the as TARGET part is optional.
This is semantically equivalent to:
manager = (EXPRESSION)
try:
TARGET = manager.__enter__(manager)
BLOCK
except:
if not manager.__exit__(*sys.exc_info()):
raise
else:
manager.__exit__(None, None, None)
TARGET will get the return value of the Context Manager’s __enter__
method.
The __exit__
method will be called either with the exception and traceback
information in case of an error, or when the code of SUITE has finished. In case
of an error the Context Manager can suppress the fall through of the error by
returning a truthy value from the __exit__
method.
With more than one item, the context managers are processed as if multiple with statements were nested:
with A() as a, B() as b:
BLOCK
is semantically equivalent to:
with A() as a:
with B() as b:
BLOCK
The Python with statement creates a runtime context that allows you to run a group of statements under the control of a context manager. PEP 343 added the with statement to make it possible to factor out standard use cases of the try … finally statement.
Compared to traditional try … finally constructs, the with statement can make your code clearer, safer, and reusable.
Implementing a Context Manager using contextlib¶
Using contextlib.contextmanager decorator allows for implementing a Context Manager
easily by
using a generator/iterator function.
from contextlib import contextmanager
@contextmanager
def managed_resource(*args, **kwds):
# Code to acquire resource, e.g.:
resource = acquire_resource(*args, **kwds)
try:
yield resource
finally:
# Code to release resource, e.g.:
release_resource(resource)
This is equivalent to
class managed_resource:
def __init__(*args, **kwargs):
self.args = args
self.kwargs = kwargs
self.resource = None
def __enter__(self):
self.resource = acquire_resource(*self.args, *self.kwargs)
return self.resource
def __exit__(self, exc_type, exc_value, exc_traceback):
release_resource(self.resource)
The implementation of the contextmanager
decorator could be as following:
class GeneratorContextManager:
def __init__(self, generator):
self.generator = generator
def __enter__(self):
return self.generator.send(None)
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type is None:
try:
self.generator.send(None)
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.generator.throw(exc_type, exc_value, exc_traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
raise
def contextmanager(func):
def wrapper(*args, **kwargs):
return GeneratorContextManager(func(*args, **kwargs))
return wrapper
Call Flow¶
As a class based Context Manager:
class SimpleContextManager:
def __enter__(self):
print("acquire")
return "resource"
def __exit__(self, exc_type, exc_value, exc_traceback):
print("release")
with SimpleContextManager() as manager:
print(manager)
Output:
>>> with SimpleContextManager() as manager:
... print(manager)
...
acquire
resource
release
Via contextlib.contextmanager
decorator:
from contextlib import contextmanager
@contextmanager
def simple_context_manager():
print("acquire")
try:
yield "resource"
finally:
print("release")
with simple_context_manager() as manager:
print(manager)
Output
>>> with simple_context_manager() as manager:
... print(manager)
...
acquire
resource
release
Example Context Managers¶
Additional examples for using Context Manager even beyond strict resource acquisition and release.
Example 1 - Redirect Stdout¶
A Context Manager to redirect stdout to some other IO object.
import sys
class RedirectStdout:
def __init__(self, new_target):
self._new_stdout = new_target
self._old_stdout = None
def __enter__(self):
self._old_stdout = sys.stdout
sys.stdout = self._new_stdout
return self._new_stdout
def __exit__(self, exc_type, exc_value, exc_traceback):
sys.stdout = self._old_stdout
with open('help.txt', 'w') as f, RedirectStdout(f):
help(print)
A similar Context Manager is available in the Python standard library contextlib.redirect_stdout
.
Example 2 - Suppress Exceptions¶
A Context Manager to suppress all raised exceptions.
from contextlib import contextmanager
@contextmanager
def catch_all():
try:
yield
except:
pass
with catch_all():
raise RuntimeException("foo")
A related Context Manager is available in the Python standard library contextlib.suppress
.
Example 3 - Add a directory to the Python module search path¶
import sys
@contextmanager
def add_module_path(path: str):
sys.path.append(path)
try:
yield
finally:
try:
sys.path.remove(path)
except ValueError:
# path is not in sys.path
pass
Example 4 - Print Prefix¶
class PrintPrefix:
def __init__(self, prefix: str):
self.prefix = prefix
self.active = False
def print(self, *args, **kwargs):
if self.active:
print(self.prefix, *args, **kwargs)
else:
print(*args, **kwargs)
def __enter__(self):
self.active = True
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.active = False
with PrintPrefix("😀") as out:
out.print("are we happy now?")
out.print("yes we are!")