Skip to content

异常处理经验 #1

@tourun

Description

@tourun

  异常处理时任何一种高级语言都会涉及到的议题。在编码过程中,正确高效的异常处理,来避免一些错误导致的应用程序中止,使得程序更健壮,同时能够更快的定位且修复问题。在Python中通过 try/except/[else]/[finally] 语句来捕获异常,其中 elsefinally 关键字可以省略。要抛出异常,需要使用 raise 关键字。关于python 中异常处理的更多内容可以查看官方文档,这里不再赘述。

python 异常体系

  python异常的根类是 BaseException,在标准库中还有四种builtin异常类直接继承自 BaseException,分别是 GeneratorExitSystemExitKeyboardInterrupt 以及 Exception 类。其他builtin的异常类,都继承自 Exception查看python标准库中异常类的层次结构。即使python官方提供了有很多builtin的异常类,但是在编写应用程序时,仍然需要我们自定义与业务相关的异常类。在自定义异常时,

   python官方推荐其继承自Exception或者Exception的子类而非BaseException。这是因为Exception表示应用程序中最普遍且并非系统退出导致的异常出错,其他三个BaseException的子类都有特殊的含义,GeneratorExit在生成器执行close()方法抛出的,而它从技术上讲并非是一种错误,KeyboardInterrupt通常在用户执行中断操作如Control-C时抛出的,它继承自BaseException可以避免被捕获Exception的代码意外捕获,从而阻止python解释器的退出,SystemExit在执行sys.exit()方法时抛出,与KeyboardInterrupt一样,它也不会被捕获Exception的代码意外捕获。应用程序在捕获异常时,更关注的是Exception类而非BaseException类,因此自定义的异常应该继承自Exception

更好的使用异常

   在现实中,应尽量避免直接使用*raise Exception()*来抛出异常,因为它不够具体,没有实际的意义。正确的做法时根据业务逻辑,来自定义不同的异常。我们知道,定义异常需要继承自Exception,定义最简单的异常类:

class MyException(Exception):
    """customize exception"""
    pass

   捕获所有的异常:

try:
    do_something()
except Exception:
    # catch any exception !
    handle_exception()

   在自定义异常类时,通常需要一些额外信息,来表明当前异常发生的上下文,那么需要来了解下异常类的初始化方法。奇怪的是,尽管通过pycharm查看python typeshed,在builtins.pyi文件中可以找到BaseExceptionException的声明如下(每个Python模块都由扩展名为 .pyi 的 "stub file" 表示,它是一个普通的python文件,只包含模块的公共接口的描述,不包含任何实现,类似于java的接口类):

class BaseException:
    def __init__(self, *args: object, **kwargs: object) -> None: ...
        
class Exception(BaseException): ...

   实际上异常基类并不支持关键字参数,而只支持位置参数,网上查了下已经有issue了:

In [1]: e = Exception("trivial", error_code=-1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-67c4664d4f81> in <module>
----> 1 e = Exception(error_code=-1)

TypeError: Exception does not take keyword arguments

   因此要在自定义的异常类中,调用其基类的初始化方法,不能传递关键字参数。调试程序时,常常使用 print(exception) 来输出异常的内容,print方法会调用异常类的 __str__方法。查看cpython源码,在exception.c文件中找到其定义:

static PyObject *
BaseException_str(PyBaseExceptionObject *self)
{
    switch (PyTuple_GET_SIZE(self->args)) {
    case 0:
        return PyUnicode_FromString("");
    case 1:
        return PyObject_Str(PyTuple_GET_ITEM(self->args, 0));
    default:
        return PyObject_Str(self->args);
    }
}

   switch语句决定输出的内容,并且使用到了位置参数args,转化为python语法如下:

def __str__(self):
    str_len = len(self.args)
    if str_len == 0:
        return ""
    elif str_len == 1:
        return str(self.args[0])
    else:
        return str(self.args)

   因此如果要使用默认的 _str_ 输出异常信息时,自定义异常类最好将需要输出的内容以单个参数形式来初始化Exception基类。当然,子类也可以复写基类的 str方法。

   当需要异常提供的更多细节时,可以在初始化异常实例时传递更多的参数,如:

class CarError(Exception):
    """Basic exception for errors raised by cars"""
    def __init__(self, car, msg=None):
        if msg is None:
            # Set some default useful error message
            msg = "An error occured with car %s" % car
        super(CarError, self).__init__(msg)
        self.car = car

class CarCrashError(CarError):
    """When you drive too fast"""
    def __init__(self, car, other_car, speed):
        super(CarCrashError, self).__init__(
            car, msg="Car crashed into %s at speed %d" % (other_car, speed))
        self.speed = speed
        self.other_car = other_car

try:
    drive_car(car)
except CarCrashError as e:
    # If we crash at high speed, we call emergency
    if e.speed >= 30:
        call_911()

   实现一个library时,定义一个继承自Exception的异常基类,会让使用者更容易的捕获从这个library中抛出的任何异常,比如sqlalchemy中的异常定义:

class SQLAlchemyError(Exception):
    """Generic error class."""

class ArgumentError(SQLAlchemyError):
    """Raised when an invalid or conflicting function argument is supplied.
    This error generally corresponds to construction time state errors."""

class ObjectNotExecutableError(ArgumentError):
    """Raised when an object is passed to .execute() that can't be
    executed as SQL."""

class NoForeignKeysError(ArgumentError):
    """Raised when no foreign keys can be located between two selectables
    during a join."""
    
# other exceptions

   这样,在使用SQLAlchemy可以简单的通过 except SQLAlchemyError 来捕获任何SQLAlchemy相关的异常。查看sqlalchemy/exc.py,会发现很多异常的定义都很简单,不同的类名代表不同异常抛出的含义,这是因为对不同类型的异常,并非平等对待,事实上更希望以不同的方式来对它们做出反应或者处理。这样就需要不同的 except 语句,来捕获不同的异常,同时要注意except语句的顺序,保证子类异常的捕获总在父类之前。当然,并不需要捕获所有异常,好的做法是只捕获您愿意处理的异常

组织异常结构

   在何时何处定义异常并无限制,与其他类型一样,可以定义在任何模块,函数,类甚至闭包中。大部分library将它们的异常类都定义在同一个模块中,比如SQLAlchemy的异常定义在exc.py中,Requests定义在exceptions.py中。在使用这些library时,很容易就能import它们的异常模块,并且在编写处理异常的代码时,知道它们是在何处定义的。这种方式并非强制性的,当library的规模很小时,没有必要将异常与其他模块分割为不同的文件。

   一些应用程序往往由不同的子系统组合而成,而每个子系统又有各自的异常模块。这种情况下,将这些子系统的异常统一到同一个模块,比如都放在myapp.exceptions,并不是一种好的做法。例如,当应用程序有两个子系统组成,分别是定义在myapp.http模块的HTTP REST API服务,和定义在myapp.tcp模块的TCP服务。这两个服务都可以定义与自己的协议相关的异常。如果这些异常都定义在一个myapp.exceptions模块,那么只会为了一些无用的一致性,而使得代码分散。如果在子系统中维护各自的异常模块,只需要将它们定义在文件顶部某处,这样会简化代码的维护。

包装异常

   包装异常是将一个异常封装到另一个异常之中的做法。为什么不直接抛出异常,而要在其上封装一层呢?试想当我们在开发自己的lib时,在其中使用了 requests,且未将requests的异常类封装到自定义的lib异常类中,这样就会出现layer violation。任何应用程序在使用到我们定义的lib时,可能会收到 requests.exceptions.ConnectionError类似的异常,这正是问题所在:

  • 应用程序并不清楚我们的lib使用了 requests,而它也并不需要知道
  • 为了处理这个异常,应用程序需要 import requests.exceptions,因此需要依赖于requests库,即使并未直接使用它
  • 或许之后的某天,我们会使用其他的lib比如httplib来替换掉requests,在出现异常时会抛出httplib.HTTPConnection而非requests相关的异常,此时应用程序的异常捕获处理已经不再正确

   因此在任何场合,务必将异常从其他模块封装到自己的异常处理中,就像下面这样:

class MylibError(Exception):
    """Generic exception for mylib"""
    def __init__(self, msg, original_exception):
        super(MylibError, self).__init__(msg + (": %s" % original_exception))
        self.original_exception = original_exception

try:
    requests.get("http://example.com")
except requests.exceptions.ConnectionError as e:
     raise MylibError("Unable to connect", e)

参考文章:

https://julien.danjou.info/python-exceptions-guide/)

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonpython language feature

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions