In Python, a context manager is an object that can be used in a with statement. Here's a context manager that reports the total wall-clock time spent inside a with block:
import time
class Timer(object):
def __init__(self, msg):
self._msg = msg
def __enter__(self):
# time.monotonic() requires Python >= 3.3
self._start = time.monotonic()
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type:
print('Failed: {}: {}'.format(self._msg, exc_value))
else:
print('{}: {} s'.format(self._msg, time.monotonic() - self._start))We can use it like this:
with Timer("doing stuff"):
for i in range(1000000):
passwhich produces this output:
doing stuff: 0.04281306266784668 s
Context managers can also handle exceptions from inside the block they guard. Timer does this. If we do:
with Timer("doing stuff"):
raise ValueError('ack')we get:
Failed: doing stuff: ack
Traceback (most recent call last):
File "test.py", line 20, in <module>
raise ValueError('ack')
ValueError: ack
Because __exit__() doesn't return True, the exception continues to propagate, so we see both the Failed message and the traceback. If __exit__() returned True, the exception would be swallowed by the context manager and we would see only the Failed line.
Let's say we wanted the context manager to return some information, perhaps the granularity of the timer. We can modify __enter__() like this:
def __enter__(self):
# time functions require Python >= 3.3
self._start = time.monotonic()
return time.clock_getres(time.CLOCK_MONOTONIC)Now we can do:
with Timer("doing stuff") as resolution:
print('Resolution: {}'.format(resolution))
for i in range(1000000):
passwhich produces:
Resolution: 1e-09
doing stuff: 0.043778783998277504 s
Writing a context manager involves some boilerplate: the __enter__() and __exit__() methods, and possibly an __init__() method to handle any arguments. There's an easier way: the contextlib.contextmanagerdecorator. We can rewrite Timer like this:
import contextlib
import time
@contextlib.contextmanager
def Timer(msg):
start = time.monotonic()
try:
yield time.clock_getres(time.CLOCK_MONOTONIC)
except BaseException as e:
print('Failed: {}: {}'.format(msg, e))
raise
else:
print('{}: {} s'.format(msg, time.monotonic() - start))Now Timer is just a generator that yields once (yielding the value to be bound by the with statement). The yield expression raises any exception thrown from the block, which the context manager can then handle or not.
One use for context managers is ensuring that an object (file, network connection, database handle) is closed when leaving a block. contextlib.closing() does this: its __exit__() method calls another object's close(). Use it like this:
import contextlib
with contextlib.closing(open("/etc/passwd")) as fh:
lines = fh.readlines()But that seems a bit verbose. A better approach: many objects implement __enter__() and __exit__(), so you can use them to manage themselves. The implementation of file (the type returned by open()) has something like this:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.close()So you can just do:
with open("/etc/passwd") as fh:
lines = fh.readlines()Reimplement the contextmanager decorator. You'll need to use some of the more obscure features of generators.