专栏首页CDA数据分析师Python 异常处理完整指南

Python 异常处理完整指南

年前我写了一篇文章 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 中通过这个 脚本 生成的内置异常继承结构图。

Python 2 内置的异常继承图 (大图链接:https://coyee.com/uploads/img/20160813/111701_AtY7.png)

Python 3内置的异常继承图 (大图链接:https://coyee.com/uploads/img/20160813/111702_OPKJ.png)

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)

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

正确的定义异常类

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

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

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, 它将不需要任何特定的代码或修改。

组织结构

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

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

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

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

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

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

异常的包裹

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

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,这个问题的因为是:

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

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

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

捕获异常并记录日志

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

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

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中看到!


原文链接:https://coyee.com/article/10907-the-definitive-guide-to-python-exceptions

本文分享自微信公众号 - CDA数据分析师(cdacdacda)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2016-12-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 2019年过去一小半了,这些深度学习研究值得一看!

    转眼间2019年已经过去了快一半,这对于日新月异的深度学习技术已经算是很长一段发展时间。Open Data Science在Medium上整理了2019年到现在...

    CDA数据分析师
  • 数据分析最容易犯的7个错误,请绕行!

    要实现大数据分析项目的最佳实践并非易事。正因如此,Gartner研究总监Svetlana Sicular题为“大数据7大失败案例”的报告吸引了思科工程师Kare...

    CDA数据分析师
  • R语言预处理之异常值问题

    >>>> 一、问题 什么是异常值?如何检测异常值?请伙伴们思考或者留言讨论。 >>>> 二、解决方法 1. 单变量异常值检测 2. 使用局部异常因子进行异常值...

    CDA数据分析师
  • 剑指offer【03~09】

    注意:这道题如果数据范围变为 [1, n],那么可以将其转化为使用快慢指针判断有环问题,和 Leetcode 【Two Pointers】287. Find t...

    echobingo
  • 深度学习之BP神经网络识别手写数字(五)

    根据MNIST数据集的特性: 每张图片为28*28,其中大约有60000个手写字体训练样本。因为是对数字的识别,所以输出的范围为0~9。这就类似于一个10分类...

    李小白是一只喵
  • 尝试:Script Lab,快速 O365 开发工具//SL01)

    Script Lab 我希望有一个系列(连载),可是我挺担心没偿没有能力去驾驭它。虽然早年前己经接触过,但一直未有下决心开始 Office 365 的开发之旅,...

    心莱科技雪雁
  • 尝试:Script Lab,快速 O365 开发工具//SL01)

    Script Lab 我希望有一个系列(连载),可是我挺担心没偿没有能力去驾驭它。虽然早年前己经接触过,但一直未有下决心开始 Office 365 的开发之旅,...

    寒树Office与RPA
  • 云时代的传统产业转型之路

    最近三年,传统企业,都纷纷患上了「互联网综合症」,他们认为互联网是一股神秘的力量,随时可能消灭他们,甚至都不知道如何去抵挡。

    刘永峰
  • Emlog 熊掌号页面改造

    Youngxj
  • 接口测试 | 25 requests + pytest测试实例

    概述 本文主要分享如何将pytest和requests结合一起使用,让大家有个初步的了解。 主要内容有: pytest简介 pytest + requests示...

    苦叶子

扫码关注云+社区

领取腾讯云代金券