By Llewellyn Falco & Matt Plavcan
I've been doing a lot python lately and context Manager has thrown me for a bit of a loop. Here's what happened
Context
First, A ContextManager is what python uses for the with
statement. It is the python equivalent of:
- java's
try with resources
- c#'s
using
It allows you to do the following:
@contextmanager
def printer():
print("enter")
yield
print("exit")
if __name__ == '__main__':
with printer():
print("middle")
Which prints
enter
middle
exit
Problem 1) Yield fails with exceptions
Let's say we have
@contextmanager
def printer():
print("enter")
yield
print("exit")
if __name__ == '__main__':
with printer():
raise Exception("middle")
I would expect it to close the context and then pass on the Exception. Like such:
enter
Exception: middle
exit
But it doesn't. Instead, I get:
enter
Exception: middle
There is no exit
printed.
Solution 1) Use ContextManager class
I need to expand the Printer to a full class. ContextManager is just an interface that assumes you have the __enter__
and __exit__
methods defined on a class.
For example:
class Printer():
def __enter__(self):
print("enter")
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")
if __name__ == '__main__':
with Printer():
print("Middle")
raise Exception("from middle")
Now I get the expected behavoir:
enter
Middle
exit
Problem 2) ContextManager is None?
But what if I want to reference the ContextManager?
Python allows you to assign the object to a variable with the as variable_name
syntax.
Let's try to print the ContextManager reference:
class Printer():
def __enter__(self):
print("enter")
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")
def __str__(self):
return "<Printer>"
if __name__ == '__main__':
with Printer() as p:
print(f"p = {p}")
Surprisingly this produces:
enter
p = None
exit
What is going on? The __enter__
and __exit__
work, but the value is somehow None
? How can both be true?
Solution 2) How assignment from ContextManager works.
You would think that:
with Printer() as p
is the same as
p = Printer()
with p:
but it's not. It's actually
_p = Printer()
p = p.__enter__()
Here's the solution
class Printer():
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")
def __str__(self):
return "<Printer>"
if __name__ == '__main__':
with Printer() as p:
print(f"p = {p}")
This produces the expected:
enter
p = <Printer>
exit
What's different? The __enter__
now returns self. Before it had no return, so there was an implicit return None
that all python methods have as a minimum.