使用Python’with’语句时捕获异常

问题:使用Python’with’语句时捕获异常

令我感到羞耻的是,我不知道如何处理python’with’语句的异常。如果我有代码:

with open("a.txt") as f:
    print f.readlines()

我真的很想处理“找不到文件异常”以便执行某些操作。但是我不能写

with open("a.txt") as f:
    print f.readlines()
except:
    print 'oops'

而且不能写

with open("a.txt") as f:
    print f.readlines()
else:
    print 'oops'

在try / except语句中包含“ with”,否则将不起作用:不会引发异常。为了以Python方式处理“ with”语句中的失败,我该怎么办?

To my shame, I can’t figure out how to handle exception for python ‘with’ statement. If I have a code:

with open("a.txt") as f:
    print f.readlines()

I really want to handle ‘file not found exception’ in order to do somehing. But I can’t write

with open("a.txt") as f:
    print f.readlines()
except:
    print 'oops'

and can’t write

with open("a.txt") as f:
    print f.readlines()
else:
    print 'oops'

enclosing ‘with’ in a try/except statement doesn’t work else: exception is not raised. What can I do in order to process failure inside ‘with’ statement in a Pythonic way?


回答 0

from __future__ import with_statement

try:
    with open( "a.txt" ) as f :
        print f.readlines()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
    print 'oops'

如果您希望通过公开调用与工作代码进行不同的错误处理,则可以执行以下操作:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()
from __future__ import with_statement

try:
    with open( "a.txt" ) as f :
        print f.readlines()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
    print 'oops'

If you want different handling for errors from the open call vs the working code you could do:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()

回答 1

利用该with语句的最佳“ Pythonic”方法在PEP 343中列为示例6 ,给出了该语句的背景。

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

用法如下:

with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")

The best “Pythonic” way to do this, exploiting the with statement, is listed as Example #6 in PEP 343, which gives the background of the statement.

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

Used as follows:

with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")

回答 2

使用Python’with’语句时捕获异常

从Python 2.6开始,with语句无需__future__导入即可使用。您可以使用以下命令在Python 2.5之前获得它(但现在是时候进行升级了!):

from __future__ import with_statement

这是更正您所拥有的东西。您快到了,但是with没有except子句:

with open("a.txt") as f: 
    print(f.readlines())
except:                    # <- with doesn't have an except clause.
    print('oops')

上下文管理器的__exit__方法(如果返回)False将在完成时引发错误。如果返回True,它将抑制它。在open内置的__exit__不返回True,那么你只需要嵌套一个try,except块:

try:
    with open("a.txt") as f:
        print(f.readlines())
except Exception as error: 
    print('oops')

和标准样板:不要使用裸露的东西except:,以免发生BaseException任何其他异常和警告。至少要Exception和错误一样具体,也许要抓住IOError。只捕获您准备处理的错误。

因此,在这种情况下,您需要执行以下操作:

>>> try:
...     with open("a.txt") as f:
...         print(f.readlines())
... except IOError as error: 
...     print('oops')
... 
oops

Catching an exception while using a Python ‘with’ statement

The with statement has been available without the __future__ import since Python 2.6. You can get it as early as Python 2.5 (but at this point it’s time to upgrade!) with:

from __future__ import with_statement

Here’s the closest thing to correct that you have. You’re almost there, but with doesn’t have an except clause:

with open("a.txt") as f: 
    print(f.readlines())
except:                    # <- with doesn't have an except clause.
    print('oops')

A context manager’s __exit__ method, if it returns False will reraise the error when it finishes. If it returns True, it will suppress it. The open builtin’s __exit__ doesn’t return True, so you just need to nest it in a try, except block:

try:
    with open("a.txt") as f:
        print(f.readlines())
except Exception as error: 
    print('oops')

And standard boilerplate: don’t use a bare except: which catches BaseException and every other possible exception and warning. Be at least as specific as Exception, and for this error, perhaps catch IOError. Only catch errors you’re prepared to handle.

So in this case, you’d do:

>>> try:
...     with open("a.txt") as f:
...         print(f.readlines())
... except IOError as error: 
...     print('oops')
... 
oops

回答 3

区分复合with语句引发的异常的可能来源

区分with语句中发生的异常非常棘手,因为它们可能起源于不同的地方。可以从以下任一位置(或其中调用的功能)引发异常:

  • ContextManager.__init__
  • ContextManager.__enter__
  • 的身体 with
  • ContextManager.__exit__

有关更多详细信息,请参见有关Context Manager Types的文档。

如果我们想区分这些不同的情况,仅将包裹with到a中try .. except是不够的。考虑以下示例(ValueError用作示例,但当然可以用任何其他异常类型代替):

try:
    with ContextManager():
        BLOCK
except ValueError as err:
    print(err)

在此,except遗嘱将捕获源自四个不同地方的所有exceptions,因此不允许对其进行区分。如果将上下文管理器对象的实例移到之外with,则可以区分__init__BLOCK / __enter__ / __exit__

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        with mgr:
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        # At this point we still cannot distinguish between exceptions raised from
        # __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
        pass

有效地,这仅对__init__零件有所帮助,但是我们可以添加一个额外的哨兵变量来检查with开始执行的主体(即__enter__与其他主体区别):

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        entered_body = False
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        else:
            # At this point we know the exception came either from BLOCK or from __exit__
            pass

棘手的部分是区分源自BLOCK__exit__的异常,这是因为逃脱了withwill 主体的异常将传递给__exit__它,从而可以决定如何处理它(请参阅docs)。但是__exit__,如果出现异常,则将原来的exceptions替换为新的exceptions。为了处理这些情况,我们可以except在的主体中添加一个通用子句,with以存储可能会被忽略而不会被忽略的任何潜在异常,并将其与except后来在最外层捕获的异常进行比较-如果它们相同,则表示起源是BLOCK否则,它是__exit__(以防万一__exit__,通过在最外层返回true值来抑制异常except 根本不会执行)。

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    entered_body = exc_escaped_from_body = False
    try:
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
            except Exception as err:  # this exception would normally escape without notice
                # we store this exception to check in the outer `except` clause
                # whether it is the same (otherwise it comes from __exit__)
                exc_escaped_from_body = err
                raise  # re-raise since we didn't intend to handle it, just needed to store it
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        elif err is exc_escaped_from_body:
            print('BLOCK raised:', err)
        else:
            print('__exit__ raised:', err)

使用PEP 343中提到的等效形式的替代方法

PEP 343-“ with”语句指定该语句的等效“ non-with”版本with。在这里,我们可以轻松地将各个部分包装在一起try ... except,从而区分出不同的潜在错误源:

import sys

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        value = type(mgr).__enter__(mgr)
    except ValueError as err:
        print('__enter__ raised:', err)
    else:
        exit = type(mgr).__exit__
        exc = True
        try:
            try:
                BLOCK
            except TypeError:
                pass
            except:
                exc = False
                try:
                    exit_val = exit(mgr, *sys.exc_info())
                except ValueError as err:
                    print('__exit__ raised:', err)
                else:
                    if not exit_val:
                        raise
        except ValueError as err:
            print('BLOCK raised:', err)
        finally:
            if exc:
                try:
                    exit(mgr, None, None, None)
                except ValueError as err:
                    print('__exit__ raised:', err)

通常,更简单的方法就可以了

这种特殊异常处理的需求应该非常少,通常将整个包装with在一个try ... except块中就足够了。特别是如果各种错误源由不同的(自定义)异常类型指示(需要相应地设计上下文管理器),我们可以很容易地区分它们。例如:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...

Differentiating between the possible origins of exceptions raised from a compound with statement

Differentiating between exceptions that occur in a with statement is tricky because they can originate in different places. Exceptions can be raised from either of the following places (or functions called therein):

  • ContextManager.__init__
  • ContextManager.__enter__
  • the body of the with
  • ContextManager.__exit__

For more details see the documentation about Context Manager Types.

If we want to distinguish between these different cases, just wrapping the with into a try .. except is not sufficient. Consider the following example (using ValueError as an example but of course it could be substituted with any other exception type):

try:
    with ContextManager():
        BLOCK
except ValueError as err:
    print(err)

Here the except will catch exceptions originating in all of the four different places and thus does not allow to distinguish between them. If we move the instantiation of the context manager object outside the with, we can distinguish between __init__ and BLOCK / __enter__ / __exit__:

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        with mgr:
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        # At this point we still cannot distinguish between exceptions raised from
        # __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
        pass

Effectively this just helped with the __init__ part but we can add an extra sentinel variable to check whether the body of the with started to execute (i.e. differentiating between __enter__ and the others):

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        entered_body = False
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        else:
            # At this point we know the exception came either from BLOCK or from __exit__
            pass

The tricky part is to differentiate between exceptions originating from BLOCK and __exit__ because an exception that escapes the body of the with will be passed to __exit__ which can decide how to handle it (see the docs). If however __exit__ raises itself, the original exception will be replaced by the new one. To deal with these cases we can add a general except clause in the body of the with to store any potential exception that would have otherwise escaped unnoticed and compare it with the one caught in the outermost except later on – if they are the same this means the origin was BLOCK or otherwise it was __exit__ (in case __exit__ suppresses the exception by returning a true value the outermost except will simply not be executed).

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    entered_body = exc_escaped_from_body = False
    try:
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
            except Exception as err:  # this exception would normally escape without notice
                # we store this exception to check in the outer `except` clause
                # whether it is the same (otherwise it comes from __exit__)
                exc_escaped_from_body = err
                raise  # re-raise since we didn't intend to handle it, just needed to store it
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        elif err is exc_escaped_from_body:
            print('BLOCK raised:', err)
        else:
            print('__exit__ raised:', err)

Alternative approach using the equivalent form mentioned in PEP 343

PEP 343 — The “with” Statement specifies an equivalent “non-with” version of the with statement. Here we can readily wrap the various parts with try ... except and thus differentiate between the different potential error sources:

import sys

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        value = type(mgr).__enter__(mgr)
    except ValueError as err:
        print('__enter__ raised:', err)
    else:
        exit = type(mgr).__exit__
        exc = True
        try:
            try:
                BLOCK
            except TypeError:
                pass
            except:
                exc = False
                try:
                    exit_val = exit(mgr, *sys.exc_info())
                except ValueError as err:
                    print('__exit__ raised:', err)
                else:
                    if not exit_val:
                        raise
        except ValueError as err:
            print('BLOCK raised:', err)
        finally:
            if exc:
                try:
                    exit(mgr, None, None, None)
                except ValueError as err:
                    print('__exit__ raised:', err)

Usually a simpler approach will do just fine

The need for such special exception handling should be quite rare and normally wrapping the whole with in a try ... except block will be sufficient. Especially if the various error sources are indicated by different (custom) exception types (the context managers need to be designed accordingly) we can readily distinguish between them. For example:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...