Python的异常使用经验总结


在Python中,BaseException是所有异常的基类。但是通常我们不应该直接使用它来捕获异常(虽然它都可以捕获),取而代之的是我们需要针对于具体的业务场景,自定义适合的异常类型。

BaseException的实现在CPython源码中的Objects/exceptions.c这个文件中。如下文:

/*
 *    Exception extends BaseException
 */
SimpleExtendsException(PyExc_BaseException, Exception,
                       "Common base class for all non-exit exceptions.");

再往下看你会发现,BaseException这个类定义了所有的异常需要用到的属性和方法。我们经常用的Exception这个异常其实只是继承了BaseException,其它的什么都没有做。

其它直接继承自BaseException的异常类型有:

  • GeneratorExit
  • SystemExit
  • KeyboardInterrupt

其它的内置异常类型都继承自Exception。完整的Python2和Python3的内置异常类型关系如下图:

Python2的内置异常关系树

Python3的内置异常关系树

BaseException的初始化方法(init(*args))保存了所有通过args传递进来的参数。这个在Python2和Python3的实现是一样的:

static int
BaseException_init(PyBaseExceptionObject *self, PyObject *args, PyObject *kwds)
{
    if (!_PyArg_NoKeywords(Py_TYPE(self)->tp_name, kwds))
        return -1;

    Py_INCREF(args);
    Py_XSETREF(self->args, args);

    return 0;
}

args参数唯一被用到的地方就是它的str方法,它会将args转换成一个字符串并返回给调用方:

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);
    }
}

对应到Python代码的话,就是这样:

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

定义你自己的异常

在Python中,异常可以在程序的任何位置被抛出来,异常的种类也可以是五花八门。一般来说,我们可以使用Exception来捕获大部分的异常,但是更多的时候我们还是应该尽可能的明确要捕获的异常类型,毕竟如果只是抛出来一个Exception的话,对于debug其实并没有太大的帮助。

定义一个定制的异常类型非常容易,只需要(大部分场景)继承自Exception即可:

class MyException(Exception):
    pass

一个导航APP

假设我们要做一个导航APP,那可以定义基础异常:

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()
except CarCrashError as msg:
    # do sth

这样的异常捕获,对于后续的debug来说,显然要比直接捕获Exception要友好、有用的多。

再次抛出异常

我自己的编程习惯有一个原则: 异常要在自己控制的范围内。因此如果异常不属于当前函数,就把异常再次抛出,交由上层逻辑进行处理。例如:

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

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

def request_url():
    try:
        foo()
    except MylibError as msg:
        # do sth

这个例子可以看到,当异常在本函数内无法处理时,就把它通过raise关键词再次跑出去。交给上层逻辑进行处理。 这样做的好处是每一层的任务更加清晰。不会出现异常捕获混乱不堪的局面。

异常捕获和日志

通常来说,异常都是需要处理的。因此比较常见的做法是将他们记录到日志里,方便后续的相关同学进行debug。 只是捕获异常,不做任何记录时,会让修bug的同学无从下手。