The with
statement simplifies managing resources by ensuring that any resources that are allocated for an object are automatically released when the block of code exits.The statement is generally used in scenarios where setup and teardown operations are needed.
Some objects such as files and databases needs to be closed once operations on them are over. This helps in ensuring that the resources associated with the object are released.
The following example shows the basic way that files can be opened and closed.
#open the file
file = open('demo.txt')
#do something with the file
for line in file.readlines():
print(line)
#close the file
file.close()
In the case with files, we can use the with
statement to automate the file closing operation. The with
statement alleviates the need to explicitly close the file. It ensures that the file is automatically closed before exiting the block even if there are exceptions raised within the
block.
The previous example can be written as shown below using the with statement.
with open('demo.txt') as file:
for line in file.readlines():
print(line)
#the file is closed
The syntax of the with statement is basically as shown below.
with <expression> as target:
#do something with the object
The expression
is evaluated and the resulting object is assigned to the target
name, so that from there onwards the object will be accessible through that name.
Context manager protocol
The context manager protocol is composed of two methods, __enter__()
and __exit__()
. An object which has these methods defined is known as a context manager. Such an object becomes eligible for use in the with
statement. A file object which is created when we use the open()
function to open a file is a typical example of a context manager.
The __enter__()
method is used to perform setup operations for the context (such as allocating resources, opening files or databases, etc). It sets up the context and prepares it for the body of the with
statement.
The __exit__()
method is called when the control leaves the with
statement, either by reaching the end of the
statement’s block or by having an exception thrown inside the statement’s body. The method cleans up the context and releases any acquired resources.
We can make custom objects support the context manager protocol by implementing the two methods.
#Context manager for timing code blocks.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
self.end = time.perf_counter()
self.elapsed = self.end - self.start
def delayed_add(a, b):
time.sleep(1)
result = f"{a} + {b} = {a + b}"
return result
with Timer() as t:
print(delayed_add(10, 20))
print("Elapsed time: {:.2f}s".format(t.elapsed))
The Timer
class above defines a context manager which is used to measure the amount of time it takes to run a process. The __enter__()
and __exit__()
methods are used to start and stop the timer. When the with
statement is used with Timer
objects, the __enter__()
method is called, which in this case sets the start time of the timer
and returns the Timer
instance. When the with
statement exits, the __exit__(
) method is called, which sets the end time, calculates the elapsed time, and stores it in the elapsed
attribute.
with statement execution process
The execution of the with block proceeds as follows:
- The expression in the
with
statement is evaluated and the context object is obtained. - A context manager object is created which will be used to handle the context in the body of the
with
statement. - The context manager's
__enter__()
method is invoked which in turn returns a reference to the context object created in step 1. - The context object is attached to the variable specified in the
as
clause of thewith
statement. - The statements in the body of the
with
block are executed. - The
__exit__()
method of the context manager object is called if the execution is over or an exception is encountered.
Multiple context managers in one with statement
It is possible to use multiple context managers in order to manage multiple resources at once. This is done by separating the different context managers with commas, allowing them to be used in the same with statement.
with <expression> as a, <expression> as b, <expression> as c:
#do something
Literally the above syntax is equal to nesting multiple with
statements as shown in the following syntax.
with <expression> as a:
with<expression> as b:
#do something
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
self.end = time.perf_counter()
self.elapsed = self.end - self.start
with open('file1.txt', 'r') as f1, open('file2.txt', 'w') as f2, Timer() as t:
for i in in f1.read():
f2.write(i)
print("Elapsed time: {:.2f}s".format(t.elapsed))
The above example demonstrates how we can use multiple context managers in the same with block. Inside the block, the contents of the first file(f1
) are copied to the second file(f2
), the timer object computes the time taken to perform this operations.
Multi-line with statement header
When we have multiple context manager objects like in the previous example, we can write them in a multi-line fashion By sarroundoing them with parenthesis. The syntax is as follows.
with(
<expression1> as target1,
<expression2> as target2,
<expression3> as target3,
):
#statements
This way, our earlier example can be written as follows:
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
self.end = time.perf_counter()
self.elapsed = self.end - self.start
with(
open('file1.txt', 'r') as f1,
open('file2.txt', 'w') as f2,
Timer() as t
):
for i in f1.read():
f2.write(i)
print("Elapsed time: {:.2f}s".format(t.elapsed))