14. Context Manager
The concept of a context manager
is to manage resources. Typically, these resources are limited, expensive to acquire or the references to such resources need to be released (so others may also access the resources). Examples of resources that we might want to manage include connections to databases or references to files. The context manager’s main job is to manage the lifecycle of the resource, which is as follows.
initialization (setup): this phase is for the context manager to set up the resource
enter (reference and execution): this phase is to acquire the reference to the resource and give it back to the user for use
exit (tear down): this phase is for the context manager to clean up (release) the resource.
If a context manager is being used, Python provides the with
idiom to express the use of a resource.
with <context_manager> as <resource>:
# do something with the resource
14.1. Basic
A context manager is defined by creating a class and implementing at least two methods.
__enter__()
__exit__()
1class HelloManager(object):
2 def __init__(self):
3 print('hello, world!')
4
5 def __enter__(self):
6 print('how are you doing?')
7 return 7
8
9 def __exit__(self, exc_type, exc_value, exc_traceback):
10 print('bye, world!')
11
12with HelloManager() as hello_manager:
13 print(f'what did HelloManager return? {hello_manager}')
Note the method signature of __exit__(self, exc_type, exc_value, exc_traceback)
which has several arguments/parameters. These arguments might be useful when there are exceptions. The arguments are named in a short way but more useful names could be mapped as follows.
exc_type
toexception_type
exc_value
toexception_value
exc_traceback
totraceback
These arguments are not optional when defining __exit__()
; they must be part of the method signature or Python will assume that we are not implementing a cleanup for the exit phase.
14.1.1. Exercise
Create a context manager that generates the width and length randomly for a rectangle. Try using that context manager.
Solution.
1from random import randint
2
3class RandomRectManager(object):
4 def __init__(self, a, b):
5 self.a = a
6 self.b = b
7
8 def __enter__(self):
9 width = randint(self.a, self.b)
10 length = randint(self.a, self.b)
11 return {
12 'width': width,
13 'length': length
14 }
15
16 def __exit__(self, etype, eval, etrace):
17 pass
18
19with RandomRectManager(1, 10) as rect:
20 print(f'what did RandomRectManager return? {rect}')
14.1.2. Exercise
Create a context manager that generates the width and length randomly for 10 rectangle. Try using a generator function to help you.
Solution.
1from random import randint
2
3class RandomRectManager(object):
4 def __init__(self, a, b):
5 self.a = a
6 self.b = b
7
8 def __enter__(self):
9 return self.__get_rect()
10
11 def __exit__(self, etype, eval, etrace):
12 pass
13
14 def __get_rect(self):
15 n = 0
16 while n < 10:
17 n += 1
18
19 width = randint(self.a, self.b)
20 length = randint(self.a, self.b)
21
22 yield {
23 'width': width,
24 'length': length
25 }
26
27with RandomRectManager(1, 10) as rects:
28 for rect in rects:
29 print(rect)
14.2. Functions as context managers
We can also annotate
functions with the @contextmanager
decorator to make them context manager. Here’s an example below where we decorate get_rects()
to be a context manager. See how we use try-except
to manage the lifecycle of the context manager? Also, there should be only one yield
.
1from random import randint
2from contextlib import contextmanager
3
4@contextmanager
5def get_rects(a, b):
6 print('initialization goes here')
7 try:
8 yield ({'width': randint(a, b), 'height': randint(a, b)} for _ in range(10))
9 except:
10 print('exception handling goes here')
11 finally:
12 print('clean up goes here')
13
14with get_rects(1, 10) as rects:
15 for rect in rects:
16 print(rect)
You can use multiple context managers in tandem as follows.
1from random import randint
2from contextlib import contextmanager
3
4@contextmanager
5def get_rects(a=1, b=10, n=10):
6 try:
7 yield ({'width': randint(a, b), 'height': randint(a, b)} for _ in range(n))
8 except:
9 pass
10 finally:
11 pass
12
13@contextmanager
14def get_tris(a=1, b=10, n=10):
15 try:
16 yield ({'base': randint(a, b), 'height': randint(a, b)} for _ in range(n))
17 except:
18 pass
19 finally:
20 pass
21
22# avoid nested with if you can
23with get_rects() as rects:
24 with get_tris() as tris:
25 for rect, tri in zip(rects, tris):
26 print(rect, ' | ', tri)
27
28# you can reference multiple context managers
29with get_rects(1, 10) as rects, get_tris(1, 10) as tris:
30 for rect, tri in zip(rects, tris):
31 print(rect, ' | ', tri)
14.2.1. Exercise
Write a function that can be used as a context manager to generate any number of radiuses. Make sure you parameterize the range of randiuses and the number of radiuses to generate.
Solution.
1from random import randint
2from contextlib import contextmanager
3
4@contextmanager
5def get_circles(a=1, b=10, n=20):
6 try:
7 yield ({'radius': randint(a, b)} for _ in range(n))
8 except:
9 pass
10 finally:
11 pass
12
13with get_circles(5, 50, 10) as circles:
14 for c in circles:
15 print(c)