Error Handling

When you raise an exception or some function you called raises an exception, that normal code flow terminates and the exception starts propagating up the call stack until it encounters a proper exception handler. If no exception handler is available to handle it, the process (or more accurately the current thread) will be terminated with an unhandled exception message.

https://www.python.org/dev/peps/pep-3151/

The standard exception hierarchy is an important part of the Python language. It has two defining qualities: it is both generic and selective. Generic in that the same exception type can be raised - and handled - regardless of the context (for example, whether you are trying to add something to an integer, to call a string method, or to write an object on a socket, a TypeError will be raised for bad argument types). Selective in that it allows the user to easily handle (silence, examine, process, store or encapsulate…) specific kinds of error conditions while letting other errors bubble up to higher calling contexts. For example, you can choose to catch ZeroDivisionErrors without affecting the default handling of other ArithmeticErrors (such as OverflowErrors).

https://docs.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions

https://code.tutsplus.com/tutorials/professional-error-handling-with-python--cms-25950

https://eli.thegreenplace.net/2008/08/21/robust-exception-handling/

https://raygun.com/blog/errors-and-exceptions/

https://stackoverflow.com/questions/15542608/design-patterns-exception-error-handling

https://stackoverflow.com/questions/2052390/manually-raising-throwing-an-exception-in-python/24065533#24065533

  • philosophy

Give a chance to handle the error before program crash

简介

程序运行时,一定会有预料之外的情况,比如像系统请求内存失败、网络服务器无响应等。遇到这些意外情况时该如何处理呢?程序崩溃掉或给用户友好的提示,怎样的处理才够优雅呢?

许多编程语言提供了异常机制,其目的就在于,给了程序一次从致命错误中恢复的机会。这个恢复可能是简单的打印错误信息,或将错误记录在日志中,又或者以优雅的方式来终止程序。

Errors detected during execution are called exceptions and are not unconditionally fatal: you will soon learn how to handle them in Python programs

语法结构

A try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. Handlers only handle exceptions that occur in the corresponding try clause, not in other handlers of the same try statement. An except clause may name multiple exceptions as a parenthesized tuple, for example:

... except (RuntimeError, TypeError, NameError):
...     pass

A class in an except clause is compatible with an exception if it is the same class or a base class

The last except clause may omit the exception name(s), to serve as a wildcard. Use this with extreme caution, since it is easy to mask a real programming error in this way! It can also be used to print an error message and then re-raise the exception (allowing a caller to handle the exception as well):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

The tryexcept statement has an optional else clause, which, when present, must follow all except clauses. It is useful for code that must be executed if the try clause does not raise an exception.

The use of the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception that wasn’t raised by the code being protected by the tryexcept statement.

When an exception occurs, it may have an associated value, also known as the exception’s argument. The presence and type of the argument depend on the exception type.

The except clause may specify a variable after the exception name. The variable is bound to an exception instance with the arguments stored ininstance.args. For convenience, the exception instance defines __str__() so the arguments can be printed directly without having to reference .args

try

可能错问题的业务代码

except

The except clause(s) specify one or more exception handlers. When no exception occurs in the try clause, no exception handler is executed. When an exception occurs in the try suite, a search for an exception handler is started. This search inspects the except clauses in turn until one is found that matches the exception. An expression-less except clause, if present, must be last; it matches any exception. For an except clause with an expression, that expression is evaluated, and the clause matches the exception if the resulting object is “compatible” with the exception. An object is compatible with an exception if it is the class or a base class of the exception object or a tuple containing an item compatible with the exception.

If no except clause matches the exception, the search for an exception handler continues in the surrounding code and on the invocation stack

If the evaluation of an expression in the header of an except clause raises an exception, the original search for a handler is canceled and a search starts for the new exception in the surrounding code and on the call stack

When a matching except clause is found, the exception is assigned to the target specified after the as keyword in that except clause, if present, and the except clause’s suite is executed. All except clauses must have an executable block. When the end of this block is reached, execution continues normally after the entire try statement

Before an except clause’s suite is executed, details about the exception are stored in the sys module and can be accessed via sys.exc_info().sys.exc_info() returns a 3-tuple consisting of the exception class, the exception instance and a traceback object (see section The standard type hierarchy) identifying the point in the program where the exception occurred. sys.exc_info() values are restored to their previous values (before the call) when returning from a function that handled an exception.

else

The optional else clause is executed if and when control flows off the end of the try clause. [2] Exceptions in the else clause are not handled by the preceding except clauses.

finally

If finally is present, it specifies a ‘cleanup’ handler. The try clause is executed, including any except and else clauses. If an exception occurs in any of the clauses and is not handled, the exception is temporarily saved. The finally clause is executed. If there is a saved exception it is re-raised at the end of the finally clause. If the finally clause raises another exception, the saved exception is set as the context of the new exception.

The exception information is not available to the program during execution of the finally clause.

The return value of a function is determined by the last return statement executed. Since the finally clause always executes, a return statement executed in the finally clause will always be the last one executed:

>>> def foo():
...     try:
...         return 'try'
...     finally:
...         return 'finally'
...
>>> foo()
'finally'

抛出异常

允许开发者将业务逻辑错误交由上层处理。

The raise statement allows the programmer to force a specified exception to occur.

raise 语句的用法

The sole argument to raise indicates the exception to be raised. This must be either an exception instance or an exception class (a class that derives from Exception). If an exception class is passed, it will be implicitly instantiated by calling its constructor with no arguments:

raise ValueError  # shorthand for 'raise ValueError()'

重新抛出异常:只检测是否发生了异常,不对异常处理,重新抛出它让上层来处理

If you need to determine whether an exception was raised but don’t intend to handle it, a simpler form of the raise statement allows you to re-raise the exception:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise

raise 用法

无参数,重新抛出:

If no expressions are present, raise re-raises the last exception that was active in the current scope. If no exception is active in the current scope, a RuntimeError exception is raised indicating that this is an error.

raise expression,常见的抛出方式:

raise evaluates the first expression as the exception object. It must be either a subclass or an instance of BaseException. If it is a class, the exception instance will be obtained when needed by instantiating the class with no arguments.

raise expression from expression,指定异常链:

The from clause is used for exception chaining: if given, the second expression must be another exception class or instance, which will then be attached to the raised exception as the __cause__ attribute (which is writable). If the raised exception is not handled, both exceptions will be printed:

>>> try:
...     print(1 / 0)
... except Exception as exc:
...     raise RuntimeError("Something bad happened") from exc

或者通过将 exc 替换为 None 来强制关闭异常链

处理异常

异常抛出者负责为错误的发生提供详细的信息,即异常参数。

异常处理者需要尽量从该错误中恢复出来,或者将错误记录在日志中,再或者交由上层调用来处理。

except 和 finally 中处理异常时或清理工作时,如果引发新的异常的话,the previous exception is then attached as the new exception’s __context__ attribute:

>>> try:
...     print(1 / 0)
... except:
...     raise RuntimeError("Something bad happened")

清理工作

The try statement has another optional clause which is intended to define clean-up actions that must be executed under all circumstances.

不管怎样都会执行的 finally 语句块

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in an except or else clause), it is re-raised after the finally clause has been executed. The finally clause is also executed “on the way out” when any other clause of the try statement is left via a break, continue or return statement.

In real world applications, the finally clause is useful for releasing external resources (such as files or network connections), regardless of whether the use of the resource was successful.

对于预先定了清理行为的对象,优先使用 with 语句块

Some objects define standard clean-up actions to be undertaken when the object is no longer needed, regardless of whether or not the operation using the object succeeded or failed. The with statement allows objects like files to be used in a way that ensures they are always cleaned up promptly and correctly.

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

After the statement is executed, the file f is always closed, even if a problem was encountered while processing the lines. Objects which, like files, provide predefined clean-up actions will indicate this in their documentation.

自定义异常

异常类尽量简洁

Exception classes can be defined which do anything any other class can do, but are usually kept simple, often only offering a number of attributes that allow information about the error to be extracted by handlers for the exception.

异常类的层级继承结构

Programs may name their own exceptions by creating a new exception class (see Classes for more about Python classes). Exceptions should typically be derived from the Exception class, either directly or indirectly.

When creating a module that can raise several distinct errors, a common practice is to create a base class for exceptions defined by that module, and subclass that to create specific exception classes for different error conditions

异常类的命名约定

Most exceptions are defined with names that end in “Error,” similar to the naming of the standard exceptions.

Many standard modules define their own exceptions to report errors that may occur in functions they define

内置异常类型

https://docs.python.org/2/library/exceptions.html#bltin-exceptions

异常处理最佳实践

  • 不要使用异常来管理程序的业务逻辑。异常中断程序正常执行流,将业务逻辑混杂于异常处理中,降低了程序的可读性。
  • 异常的类名要有意义,可以反映出引起异常的原因。
  • 优先使用异常,而不是以返回值的形式来返回错误代码。
  • 优先捕获更加具体的异常类型。捕获抽象的异常类型可能会隐藏程序中潜在的问题。
  • 根据具体的业务场景来自定义异常的层级继承结构。
  • 根据程序是否可以从异常中恢复,将异常分为以下几种:Fatal: System crash states. Error: Lack of requirement. Warn: Not an error but error probability. Info: Info for user. Debug: Info for developer.
  • 不要吞噬异常,也即捕获了异常却不做任何的处理。这样会给以后的程序维护带来很多麻烦。
  • 不要对同一异常记录多次日志。
  • 一定不要忘记在 finally 语句块中释放之前打开的资源。
  • 一般来说,不要在循环体中处理异常,而应该将循环体整个包含到异常处理块中。
  • 要注意 try-except-finally 语句块处理异常的粒度。一个异常处理语句块中最好只处理一个业务逻辑操作,不要把所有业务执行操作都塞进一个异常处理中。
  • 可以考虑为异常定义代码。
  • 优先使用 with 语句块打开文件、套接字等资源,而不是自己用 try-except-finally 来管理这些资源的异常。这样可以免去资源的手动释放。
  • 如果一个业务操作很难拆分,那么应该将可能出现异常的部分放在 try 语句块中,剩余逻辑放在 else 语句块中。
  • 自定义的异常类,最好实现 __str__() 方法,来体现异常接收到的参数,而不是通过引用异常属性 args 来访问这些参数。
  • Derive exceptions from Exception rather than BaseException. Direct inheritance from BaseException is reserved for exceptions where catching them is almost always the wrong thing to do.

    Design exception hierarchies based on the distinctions that code catching the exceptions is likely to need, rather than the locations where the exceptions are raised. Aim to answer the question “What went wrong?” programmatically, rather than only stating that “A problem occurred” (see PEP 3151 for an example of this lesson being learned for the builtin exception hierarchy)

    Class naming conventions apply here, although you should add the suffix “Error” to your exception classes if the exception is an error. Non-error exceptions that are used for non-local flow control or other forms of signaling need no special suffix.

  • Use exception chaining appropriately. In Python 3, “raise X from Y” should be used to indicate explicit replacement without losing the original traceback.

    When deliberately replacing an inner exception (using “raise X” in Python 2 or “raise X from None” in Python 3.3+), ensure that relevant details are transferred to the new exception (such as preserving the attribute name when converting KeyError to AttributeError, or embedding the text of the original exception in the new exception message).

  • When raising an exception in Python 2, use raise ValueError('message') instead of the older form raise ValueError, 'message'.

    The latter form is not legal Python 3 syntax.

    The paren-using form also means that when the exception arguments are long or include string formatting, you don’t need to use line continuation characters thanks to the containing parentheses.

  • When catching exceptions, mention specific exceptions whenever possible instead of using a bare except: clause.

    For example, use:

    try:
        import platform_specific_module
    except ImportError:
        platform_specific_module = None
    
    

    A bare except: clause will catch SystemExit and KeyboardInterrupt exceptions, making it harder to interrupt a program with Control-C, and can disguise other problems. If you want to catch all exceptions that signal program errors, use except Exception: (bare except is equivalent to except BaseException:).

    A good rule of thumb is to limit use of bare ‘except’ clauses to two cases:

    1. If the exception handler will be printing out or logging the traceback; at least the user will be aware that an error has occurred.
    2. If the code needs to do some cleanup work, but then lets the exception propagate upwards with raise. try...finallycan be a better way to handle this case.
  • When binding caught exceptions to a name, prefer the explicit name binding syntax added in Python 2.6:

    try:
        process_data()
    except Exception as exc:
        raise DataProcessingFailedError(str(exc))
    
    

    This is the only syntax supported in Python 3, and avoids the ambiguity problems associated with the older comma-based syntax.

  • When catching operating system errors, prefer the explicit exception hierarchy introduced in Python 3.3 over introspection of errno values.

  • Additionally, for all try/except clauses, limit the try clause to the absolute minimum amount of code necessary. Again, this avoids masking bugs.

    Yes:

    try:
        value = collection[key]
    except KeyError:
        return key_not_found(key)
    else:
        return handle_value(value)
    
    

    No:

    try:
        # Too broad!
        return handle_value(collection[key])
    except KeyError:
        # Will also catch KeyError raised by handle_value()
        return key_not_found(key)
    
    
  • When a resource is local to a particular section of code, use a with statement to ensure it is cleaned up promptly and reliably after use. A try/finally statement is also acceptable.

参考资料

  1. http://codebuild.blogspot.com/2012/01/15-best-practices-about-exception.html
  2. https://docs.python.org/3.6/tutorial/errors.html
  3. http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html
Previous
Next