Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Python *with* statement exactly equivalent to a try - (except) - finally block?

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!

like image 674
Clara Avatar asked Sep 29 '14 09:09

Clara


People also ask

Can we use finally with except in Python?

One can use finally just after try without using except block, but no exception is handled in that case.

What is the difference between try-Except and try finally in Python?

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.

What is the finally block executed in Python?

When is the finally block executed? Explanation: The finally block is always executed.

How do I use try and finally in Python?

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.


1 Answers

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 yields, 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.


TL;DR

  • 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.

like image 80
Veedrac Avatar answered Sep 22 '22 08:09

Veedrac