文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

3年前我写了一篇文章 Python classic, static, class and abstract methods,现在似乎到了更新的时候,今天我想来剖析和讨论 Python 异常。

剖析异常基础类

Python 异常的基础类名为 BaseException。这个类在程序和库中很少用,更多时候它被当成是异常的实现细节。为了了解异常是怎么实现的,我们可以阅读 CPython 源码中的 Objects/exceptions.c 文件。在这个文件中你可以看到 BaseException 类中定义的所有基础方法和异常的属性。而我们常用的 Exception 类则继承于 BaseException,该类只包含如下代码:

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

另外一些直接继承 BaseException 的类是 GeneratorExit、SystemExit 和 KeyboardInterrupt。而其他内置的异常一般直接从 Exception 类中继承。你可以通过 pydoc2 exceptions 或者 pydoc3 builtins 命令来查看整个异常的结构。

这里是在 Python 2 和 3 中通过这个 脚本 生成的内置异常继承结构图。 

第 1 段(可获 2.03 积分)

Python 2 内置的异常继承图 (点击可放大)

Python 3内置的异常继承图 (点击可放大)

 

BaseException.__init__ 签名是BaseException.__init__(*args)。这个初始化方法保存了许多参数,都传入到的args属性上。 从exceptions.c 的源代码中可以看出这一点,在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;
}

 只有BaseException.__str__ 方法用到了args 属性。这个方法使用self.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)

 

第 2 段(可获 1.05 积分)

因此,异常信息应该被当作唯一一个参数传入给 BaseException.__init__ 方法。

正确的定义异常类

正如你可能已经知道了,在Python中,异常有可能在任何地方被抛出。最基本的异常类叫Exception ,它可用于程序的任何地方。在编码中,没有程序或库直接抛出Exception -这对我们来说还不够。

自从将所有的异常设计为都继承这个顶级的 Exception类,这个类可以很方便的用于捕获所有异常:

第 3 段(可获 1.21 积分)
try:
    do_something()
except Exception:
    # THis will catch any exception!
    print("Something terrible happened")

为了合理准确的定义你的异常类,这里有一些规范与编程技巧,你可以做为参照:

  • 必须继承 Exception类
    class MyOwnError(Exception):
        pass
  • 利用前面提到的BaseException.__str__: 它将传递给BaseException.__init__ 方法的第一个参数打印出来,所以通常在调用 BaseException.__init__ 方法时,只传递一个参数 
  • 当创建类库时,可以定义一个继承于Exception的基类.客户在使用类库时,会更方便的捕捉任何异常:
    class ShoeError(Exception):
        """Basic exception for errors raised by shoes"""
    
    class UntiedShoelace(ShoeError):
        """You could fall"""
    
    class WrongFoot(ShoeError):
        """When you try to wear your left show on your right foot"""
    在编写任何关于shose的代码时,这段代码将会很有用,除了ShoeError.例如,Django 对异常并没有拆分的很细,这导致我们很难捕获 "Django抛出的任何异常".
  • 提供关于异常的详细信息.这是很有价值的,它可以正确的记录日志,做进一步操作甚至恢复:
    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()
    例如,这里检测到违反SQL外键约束时,利用 Gnocchi 抛出特定的应用程序异常(NoSuchArchivePolicy): 
    try:
        with self.facade.writer() as session:
            session.add(m)
    except exception.DBReferenceError as e:
        if e.constraint == 'fk_metric_ap_name_ap_name':
            raise indexer.NoSuchArchivePolicy(archive_policy_name)
        raise
  • 在需要的时候继承内置的异常类型.这将使编程变得更方便,不需要为你的程序或类库编写特定的异常:
    class CarError(Exception):
        """Basic exception for errors raised by cars"""
    
    class InvalidColor(CarError, ValueError):
        """Raised when the color for a car is invalid"""
    这将允许更多程序在不知道你定义的异常类型情况下,使用通用方式来捕获异常.如果一个程序知道如何处理ValueError, 它将不需要任何特定的代码或修改。
第 4 段(可获 2.84 积分)

组织结构

你可以随时随地的定义异常,可以是任意的类,任意的包,函数,甚至是闭包。

很多的库都有他们自己定义的异常模块:SQLAlchemy的异常定义在sqlalchemy.exc中,requestsrequest.exceptions中,Werkzeugwekzeug.exceptions中,等等..

这些正常的包都能通过刚才的方法导出异常,同事也能方便的被用户所调用他们的异常模块,并且知道异常在哪里被定义,什么时候来用哪种异常处理代码可能发生的异常。

第 5 段(可获 1.15 积分)

不过这些都不是强制的,越小的Python模块可能越想把异常整合在他们唯一的模块中。通常如果你的代码小到只有一个文件,就不用非得把代码和异常分成不同的文件或者模块。

当然对于库的明智的选择是,把他们分成不同的文件或者模块。如果每个子系统都有一系列的异常通常就把他们放在这个子系统中。这也是为什么我不推荐一个系统中只放一个异常模块。这样myapp.exceptions就可能没必要了。

第 6 段(可获 1.34 积分)

例如,如果你的应用已经被一个定义在myapp.http的HTTP REST API所包含,并且他还是一个TCP服务器myapp.tcp的一部分,这样就他们就很可能同事定义不同的异常比如他们自己协议的错误或者消息请求生命周期的错误。如果把这些异常都写在myapp.exceptions里就会破坏代码的一致性,如果异常以诗歌本地文件,就把它们定义在这个文件的头部就好了。这将减缓维护代码的成本。

异常的包裹

第 7 段(可获 1.1 积分)

包装异常 就是将一个异常封装到另一个异常中: 

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

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

当写一个库的时候利用其它库这是有意义的.如果一个库使用了 requests,但他并没将requests异常封装到他自己定义的异常类中, 这会影响异常的传递.任何程序使用你的库可能会收到一个requests.exceptions.ConnectionError,这个问题的因为是:

  1. requests对应用程序来说是透明的,应用程序也不需要/不想知道requests.
  2. 应用程序不得不导入requests.exceptions , 因此它将依赖 requests—即使它不直接使用它.
  3. 一旦我的mylib库从requests 迁移到httplib2, 应用程序中捕获requests异常的代码将会显得多余.

Tooz 库就是封装的很好例子,由于它使用了基于驱动的方法并且依赖许多不同的Python模块与不同的后台服务进行交互 (ZooKeeper, PostgreSQL, 等…). 因此, 它将其它模块的各个场景的异常封装到自己的一组异常类中.Python 3引入了raise from来解决这个问题,Tooz利用它来抛出自己的错误。

第 8 段(可获 2.3 积分)

就像上面做的那样,将原始异常封装到用户自定义异常中也是可行的.它使得我们可以很方便的查检原始异常.

捕获异常并记录日志

当设计异常时,它应当针对人为因素与电脑因素,记住这很重要.这就是为什么它应该包含明显的消息,并尽可能多的包含信息.这将有助于调试并且编写有弹性的代码,可以依赖异常的属性来观察它的行为,就向上面看到的那样.

完全忽略异常(不做任何处理)应当被认为是不好的编程实践.你不应该写类似那样的代码:

第 9 段(可获 1.3 积分)
try:
    do_something()
except Exception:
    # Whatever
    pass

在异常发生的程序中如果没有任何类型的信息,对查找问题来说这就是恶梦.

如果你(应该)使用 logging 库,当有程序发生异常时你可以使用exc_info参数来记录完整的追踪, 在你调试服务或不可恢复的故障时会变得非常有用:

try:
    do_something()
except Exception:
    logging.getLogger().error("Something bad happened", exc_info=True)


延伸阅读

到目前为止如果你理解了上述所有内容,恭喜你,你已经做好了处理Python中异常的准备!如果你想有对异常有更多的了解,Python中未提到的部分,我鼓励你阅读条件系统并挖出异常的泛化--我希望我们未来有一天可以在Python中看到!

我希望这篇文章可以帮你构建更好的库和应用程序.尽情地在评论部分向我提任何问题!

第 10 段(可获 1.61 积分)

文章评论

广州访客
。。。。。。