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

end*_*and 13 python error-handling exception python-2.7

请考虑以下示例:

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')
Run Code Online (Sandbox Code Playgroud)

元组是处理多种异常类型的正确方法.使用多个异常类型的列表不会导致处理.

我想知道为什么Python语法需要多个异常类型的元组.该文件说,它使用一个元组,因此,也许它只是"从来就没有使用列表,而不是一个元组的实现."

对我来说似乎也是合理的,至少在概念上也可以在这种情况下使用列表.

有没有理由为什么Python使用元组而不是列表来处理这种情况?

Aar*_*all 7

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

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

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

核心开发团队似乎并没有专门解决扩展这个以允许集合或列表的问题,但如果可以找到它,我会很乐意引用它。在Python 邮件列表上有一个讨论,推测很多(这里的另一个答案详细引用了一个回复)。

在执行以下分析后,并在邮件列表讨论的上下文中,我认为推理是显而易见的。我不建议建议添加其他容器。

列表失败与元组的演示

exceptions = TypeError, RuntimeError
list_of_exceptions = list(exceptions)
Run Code Online (Sandbox Code Playgroud)

捕获异常元组确实有效:

try:
    raise TypeError('foo')
except exceptions as error:
    print(error)
Run Code Online (Sandbox Code Playgroud)

输出:

foo
Run Code Online (Sandbox Code Playgroud)

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

try:
    raise TypeError('foo')
except list_of_exceptions as error:
    print(error)
Run Code Online (Sandbox Code Playgroud)

印刷:


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

Run Code Online (Sandbox Code Playgroud)

这表明我们正在对元组的特殊情况进行类型检查。添加另一种类型来检查肯定会使代码变慢,并且核心开发人员一直在说在 Python 中使用异常处理进行控制流一段时间是一件好事。

源代码分析

对来源的分析同意上述结论。

语法

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

拆卸

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

def catch(exceptions):
    try:
        raise Exception
    except exceptions:
        pass

import dis
dis.dis(catch)
Run Code Online (Sandbox Code Playgroud)

哪些输出:

      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)
    ...
Run Code Online (Sandbox Code Playgroud)

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

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

CPython 控制流首先检查该值是否为元组。如果是这样,它会使用元组特定代码遍历元组 - 寻找值是异常:

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;
Run Code Online (Sandbox Code Playgroud)

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

Python 容器的大小

元组是轻量级的指针数组。列表也是如此,但它们可能会被分配额外的空间,以便您可以快速添加到它们中(直到它们需要变大为止)。在 Linux 上的 Python 3.7.3 中:

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;
Run Code Online (Sandbox Code Playgroud)

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

结论

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

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

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


Boo*_*oon 3

@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虚拟机可以快速有效地创建它们。

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

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

  • 由于典型的用例是按固定顺序迭代项目,因此无需为字典或集合支付额外费用。

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

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

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

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

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

那么你就在一种误解下劳作。您没有捕获元组,因为元组永远不会被抛出。您正在捕获该元组中包含的任何异常。

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

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

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