【问题标题】:Why does handling multiple exceptions require a tuple, but not a list?为什么处理多个异常需要一个元组,而不是一个列表?
【发布时间】:2016-06-21 11:28:16
【问题描述】:

考虑以下示例:

def main_list(error_type):

    try:
        if error_type == 'runtime':
            raise RuntimeError("list error")
        if error_type == 'valueerror':
            raise ValueError("list error")

    except [RuntimeError, ValueError] as e:
        print str(e)

def main_tuple(error_type):

    try:
        if error_type == 'runtime':
            raise RuntimeError("tuple error")
        if error_type == 'valueerror':
            raise ValueError("tuple error")

    except (RuntimeError, ValueError) as e:
        print str(e)


main_tuple('runtime')
main_tuple('valueerror')

main_list('runtime')
main_list('valueerror')

元组是处理多种异常类型的正确方法。对多种异常类型使用列表会导致两者都无法处理。

我想知道为什么 Python 语法 需要 一个元组来处理多种异常类型。 docs 说它使用元组,所以也许它只是“从未使用列表而不是元组来实现”。

在我看来,在这种情况下也可以使用列表,至少在概念上是合理的。

在这种情况下 Python 使用元组而不是列表有什么原因吗?

【问题讨论】:

    标签: python python-2.7 exception error-handling


    【解决方案1】:

    为什么处理多个异常需要一个元组而不是一个列表?

    用 C 语言编写的错误处理,在其他类型检查和异常处理之前对元组的特殊情况使用类型检查,以便可以捕获多种类型的异常。

    至少有一位 Python 核心开发人员主张对控制流使用异常处理。将列表添加为要检查的附加类型将违反此策略。

    似乎核心开发团队尚未专门解决将其扩展为允许集合或列表的问题,但如果可以找到它,我会很乐意参考它。关于Python mailing list 的讨论进行了很多推测(此处的另一个答案详细引用了一个响应)。

    在进行了下面的分析之后,在邮件列表讨论的背景下,我认为推理是显而易见的。我不建议添加其他容器。

    列出失败与元组的演示

    exceptions = TypeError, RuntimeError
    list_of_exceptions = list(exceptions)
    

    捕获异常元组确实有效:

    try:
        raise TypeError('foo')
    except exceptions as error:
        print(error)
    

    输出:

    foo
    

    但是捕获异常列表不起作用:

    try:
        raise TypeError('foo')
    except list_of_exceptions as error:
        print(error)
    

    打印:

    
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    TypeError: foo
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "<stdin>", line 3, in <module>
    TypeError: catching classes that do not inherit from BaseException is not allowed
    
    

    这表明我们正在对元组的特殊情况进行类型检查。添加另一种类型进行检查肯定会使代码变慢,并且核心开发人员一直在说use exception handling for control flow in Python 一段时间以来是件好事。

    源码分析

    对来源的分析与上述结论一致。

    语法

    这不是 Python 的 grammar 或解析的问题。它将接受任何表达式。因此,任何导致异常或异常元组的表达式都应该是合法的。

    反汇编

    如果我们在 Python 3 中反汇编一个执行此操作的函数,我们会看到它看起来将异常与比较操作相匹配。

    def catch(exceptions):
        try:
            raise Exception
        except exceptions:
            pass
    
    import dis
    dis.dis(catch)
    

    哪些输出:

          2           0 SETUP_EXCEPT            10 (to 13)
    
          3           3 LOAD_GLOBAL              0 (Exception)
                      6 RAISE_VARARGS            1
                      9 POP_BLOCK
                     10 JUMP_FORWARD            18 (to 31)
    
          4     >>   13 DUP_TOP
                     14 LOAD_FAST                0 (exceptions)
                     17 COMPARE_OP              10 (exception match)
        ...
    

    这引导我们进入 Python 解释器。

    内部控制流程——CPython的实现细节

    CPython 控制流先checks for if the value is a tuple. 如果是, 它使用特定于元组的代码遍历元组——寻找异常的值:

    case PyCmp_EXC_MATCH:
        if (PyTuple_Check(w)) {
            Py_ssize_t i, length;
            length = PyTuple_Size(w);
            for (i = 0; i < length; i += 1) {
                PyObject *exc = PyTuple_GET_ITEM(w, i);
                if (!PyExceptionClass_Check(exc)) {
                    _PyErr_SetString(tstate, PyExc_TypeError,
                                     CANNOT_CATCH_MSG);
                    return NULL;
                }
            }
        }
        else {
            if (!PyExceptionClass_Check(w)) {
                _PyErr_SetString(tstate, PyExc_TypeError,
                                 CANNOT_CATCH_MSG);
                return NULL;
            }
        }
        res = PyErr_GivenExceptionMatches(v, w);
        break;
    

    添加另一种类型将需要更多的内部控制流,从而减慢 Python 解释器内部的控制流。

    Python 容器的大小

    Tuples are lightweight arrays of pointers。列表也是如此,但它们可能会被分配额外的空间,以便您可以快速添加到它们(直到它们需要变得更大)。在 Linux 上的 Python 3.7.3 中:

    >>> from sys import getsizeof
    >>> getsizeof((1,2,3))
    72
    >>> getsizeof([1,2,3])
    88
    

    集合占用更多空间,因为它们是哈希表。它们既有所包含对象的哈希值,也有指向它们所指向对象的指针。

    结论

    这是由 CPython 核心开发团队讨论和决定的。

    但我的结论是,即使在 C 级别检查其他类型来减慢 Python 中的控制流,也会违背在 Python 模块中对控制流使用异常处理的策略。

    经过上面的推理,我不建议他们添加这个。

    【讨论】:

      【解决方案2】:

      @BrenBarn 感谢您在https://mail.python.org/pipermail/python-list/2012-January/619107.html 处提供讨论链接

      我认为最好和最清晰的回应来自 Steven D'Aprano 在https://mail.python.org/pipermail/python-list/2012-January/619120.html的回复

      为了方便阅读,我复制了以下内容。


      史蒂文的回复:

      简单。

      如果你也允许列表,那为什么不允许任意序列呢?什么 关于迭代器,你允许它们吗?这可能很尴尬,因为 迭代器只能运行一次。字典也是可迭代的, 所以一旦你允许任意迭代,你就会得到字典。整个东西 变得一团糟。最好保持简单,只允许一个 规范的集合类型,在 Python 中,该类型是元组,而不是列表。

      元组是规范的集合类型,因为它们有许多 理想的属性:

      • 元组很小且内存高效,使用最少的 存放物品所需的内存。列表通常带有一个块 备用内存,以便快速插入。

      • 因此,Python 虚拟机可以快速创建它们并且 高效。

      • 元组是不可变的,因此您不必担心将元组传递给 函数并让函数在你背后修改它。

      • 元组是有序的,在重要的时候。

      • 由于典型的用例是以固定顺序迭代项目, 无需为 dict 或 set 支付额外费用。

      • 元组写起来很简单:通常你只需要在 项目。有时,为了避免歧义或改变优先级 计算,你还需要圆括号(美国括号)。 except 子句就是其中之一。

      • Frozensets 和 Sets 由于历史原因被排除在外:它们没有 直到 Python 2.3 才存在。另外,你更愿意写哪一个?

        ("abc", "def") freezeset([abc", "def"])

      • 集合和列表被排除,因为它们是可变的,两者都需要 更多的内存,并且集合具有更重的计算负担。

      对我来说,后者在语义上更有意义——“捕获所有异常 列表中的类型”而不是“捕获由 三种异常类型”。

      那你是在误解下苦苦挣扎的。你没有抓到 元组,因为元组永远不会被抛出。你正在捕捉任何 该元组中包含的异常。

      列表和元组都是本身的东西。两个列表和 元组是容器:

      列表是包含其他事物的单一事物。

      元组是包含其他事物的单一事物。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-12-07
        • 2013-09-18
        • 2021-05-23
        • 2020-01-25
        • 2020-08-09
        • 1970-01-01
        • 2015-07-04
        • 1970-01-01
        相关资源
        最近更新 更多