I think your understanding is mostly correct. A context manager is an object, which manages the context through its __enter__
and __exit__
methods. So what happens in __init__
stays true for the life of the object.
Let's look at a concrete example:
class CMan(object):
def __init__(self, *parameters):
"Creates a new context manager"
print "Creating object..."
def __enter__(self):
"Enters the manager (opening the file)"
print "Entering context..."
a_thing = self # Or any other relevant value to be used in this context
print "Returning %s" % a_thing
return a_thing
def __exit__(self, type, value, traceback):
"Exits the context"
if type is None:
print "Exiting with no exception -> Wrapping up"
return
print "Exiting with exception %s" % type
Which would be used as this:
>>> with CMan(1,2,3) as x:
... print 1 + 1
Creating object...
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up
Note that creating the object on the fly is not mandatory:
>>> mgr = CMan(1,2,3)
Creating object...
>>> with mgr as x:
... print 1 + 1
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up
Finally, the return value of __exit__
determines whether an exception should be raised. If the value evaluates to False
(e.g. False
, 0
, None
...), any exception will be raised. Otherwise, this means the context manager has handled the exception, and it does not need to be raised. For instance:
>>> class Arithmetic(object):
... def __enter__(self):
... return self
... def __exit__(self, type, value, traceback):
... if type == ZeroDivisionError:
... print "I dont care -> Ignoring"
... return True
... else:
... print "Unknown error: Panicking !"
... return False
>>> with Arithmetic() as a:
... print 1 / 0 # Divide by 0
I dont care -> Ignoring
>>> with Arithmetic() as a:
... print 1 + "2" # Type error
Unknown error: Panicking !
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Note that in the case of the divide by 0 error, as __exit__
returned True
, the error is not propagated. In other cases, it is raised after exiting the context manager. You can think of a call to a context manager:
>>> with X as x:
... f(x)
as being equivalent to:
>>> x = X.__enter__()
>>> try:
... exc = None
... f(x)
... except Exception as e:
... exc = e
... finally:
... handled = X.__exit__(exc)
... if exc and not handled:
... raise exc
Of course, if an exception is raised inside your method __enter__
or __exit__
, it should be handled appropriately, e.g. if generating a_thing
could fail. You can find a lot of resources on the web by looking for 'Python with statement', which is usually how you refer to this pattern (although Context manager is indeed more correct)