I know this was widely discussed, but I still can't find an answer to confirm this: is the with statement identical to calling the same code in a try - (except) -finally block, where whatever one defines in the __exit__
function of the context manager is placed in the finally block?
For example -- are these 2 code snippets doing exactly the same thing?
import sys from contextlib import contextmanager @contextmanager def open_input(fpath): fd = open(fpath) if fpath else sys.stdin try: yield fd finally: fd.close() with open_input("/path/to/file"): print "starting to read from file..."
the same as:
def open_input(fpath): try: fd = open(fpath) if fpath else sys.stdin print "starting to read from file..." finally: fd.close() open_input("/path/to/file")
Thanks!
One can use finally just after try without using except block, but no exception is handled in that case.
The try block lets you test a block of code for errors. The except block lets you handle the error. The else block lets you execute code when there is no error. The finally block lets you execute code, regardless of the result of the try- and except blocks.
When is the finally block executed? Explanation: The finally block is always executed.
When an exception is thrown in the try block, the execution immediately passes to the finally block. After all the statements in the finally block are executed, the exception is raised again and is handled in the except statements if present in the next higher layer of the try-except statement.
I'm going to put aside mentions of scope, because it's really not very relevant.
According to PEP 343,
with EXPR as VAR: BLOCK
translates to
mgr = (EXPR) exit = type(mgr).__exit__ # Not calling it yet value = type(mgr).__enter__(mgr) exc = True try: try: VAR = value # Only if "as VAR" is present BLOCK except: # The exceptional case is handled here exc = False if not exit(mgr, *sys.exc_info()): raise # The exception is swallowed if exit() returns true finally: # The normal and non-local-goto cases are handled here if exc: exit(mgr, None, None, None)
As you can see, type(mgr).__enter__
is called as you expect, but not inside the try
.
type(mgr).__exit__
is called on exit. The only difference is that when there is an exception, the if not exit(mgr, *sys.exc_info())
path is taken. This gives with
the ability to introspect and silence errors unlike what a finally
clause can do.
contextmanager
doesn't complicate this much. It's just:
def contextmanager(func): @wraps(func) def helper(*args, **kwds): return _GeneratorContextManager(func, *args, **kwds) return helper
Then look at the class in question:
class _GeneratorContextManager(ContextDecorator): def __init__(self, func, *args, **kwds): self.gen = func(*args, **kwds) def __enter__(self): try: return next(self.gen) except StopIteration: raise RuntimeError("generator didn't yield") from None def __exit__(self, type, value, traceback): if type is None: try: next(self.gen) except StopIteration: return else: raise RuntimeError("generator didn't stop") else: if value is None: value = type() try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: return exc is not value except: if sys.exc_info()[1] is not value: raise
Unimportant code has been elided.
The first thing to note is that if there are multiple yield
s, this code will error.
This does not affect the control flow noticeably.
Consider __enter__
.
try: return next(self.gen) except StopIteration: raise RuntimeError("generator didn't yield") from None
If the context manager was well written, this will never break from what is expected.
One difference is that if the generator throws StopIteration
, a different error (RuntimeError
) will be produced. This means the behaviour is not totally identical to a normal with
if you're running completely arbitrary code.
Consider a non-erroring __exit__
:
if type is None: try: next(self.gen) except StopIteration: return else: raise RuntimeError("generator didn't stop")
The only difference is as before; if your code throws StopIteration
, it will affect the generator and thus the contextmanager
decorator will misinterpret it.
This means that:
from contextlib import contextmanager @contextmanager def with_cleanup(func): try: yield finally: func() def good_cleanup(): print("cleaning") with with_cleanup(good_cleanup): print("doing") 1/0 #>>> doing #>>> cleaning #>>> Traceback (most recent call last): #>>> File "", line 15, in <module> #>>> ZeroDivisionError: division by zero def bad_cleanup(): print("cleaning") raise StopIteration with with_cleanup(bad_cleanup): print("doing") 1/0 #>>> doing #>>> cleaning
Which is unlikely to matter, but it could.
Finally:
else: if value is None: value = type() try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: return exc is not value except: if sys.exc_info()[1] is not value: raise
This raises the same question about StopIteration
, but it's interesting to note that last part.
if sys.exc_info()[1] is not value: raise
This means that if the exception is unhandled, the traceback will be unchanged. If it was handled but a new traceback exists, that will be raised instead.
This perfectly matches the spec.
with
is actually slightly more powerful than a try...finally
in that the with
can introspect and silence errors.
Be careful about StopIteration
, but otherwise you're fine using @contextmanager
to create context managers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With