通常,如果您try 为同一关键字参数传递多个值,则会收到TypHelp:

In [1]: dict(id=1, **{'id': 2})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <cell line: 1>()
----> 1 dict(id=1, **{'id': 2})

TypeError: dict() got multiple values for keyword argument 'id'

但如果您进行while handling another exception次操作,则会得到KeyHelp:

In [2]: try:
   ...:     raise ValueError('foo') # no matter what kind of exception
   ...: except:
   ...:     dict(id=1, **{'id': 2}) # raises: KeyError: 'id'
   ...: 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [2], in <cell line: 1>()
      1 try:
----> 2     raise ValueError('foo') # no matter what kind of exception
      3 except:

ValueError: foo

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
Input In [2], in <cell line: 1>()
      2     raise ValueError('foo') # no matter what kind of exception
      3 except:
----> 4     dict(id=1, **{'id': 2})

KeyError: 'id'

这里发生了什么?完全无关的异常如何影响dict(id=1, **{'id': 2})引发的哪种异常?

就上下文而言,我在调查以下错误报告时发现了这种行为:https://github.com/tortoise/tortoise-orm/issues/1583

这已在CPython 3.11.8、3.10.5和3.9.5上复制.

推荐答案

这看起来像是Python错误.

本应提高TypeError的代码通过检测和替换初始KeyError来工作,但此代码不正常工作.当异常发生在另一个异常处理程序中间时,应该引发TypeError的代码无法识别KeyError.它最终会让KeyError通过,而不是用TypeError取代它.

由于异常实现的更改,该错误似乎在3.12上消失了.


以下是CPython 3.11.8源代码的深入研究.3.10和3.9上也存在类似的代码.

正如我们通过使用dis模块判断dict(id=1, **{'id': 2})的字节码所看到的那样:

In [1]: import dis

In [2]: dis.dis("dict(id=1, **{'id': 2})")
  1           0 LOAD_NAME                0 (dict)
              2 LOAD_CONST               3 (())
              4 LOAD_CONST               0 ('id')
              6 LOAD_CONST               1 (1)
              8 BUILD_MAP                1
             10 LOAD_CONST               0 ('id')
             12 LOAD_CONST               2 (2)
             14 BUILD_MAP                1
             16 DICT_MERGE               1
             18 CALL_FUNCTION_EX         1
             20 RETURN_VALUE

Python使用DICT_MERGE操作码合并两个dict,以构建最终的关键字参数dict.

DICT_MERGE code的相关部分如下:

            if (_PyDict_MergeEx(dict, update, 2) < 0) {
                format_kwargs_error(tstate, PEEK(2 + oparg), update);
                Py_DECREF(update);
                goto error;
            }

它使用_PyDict_MergeEx来try 合并两个指令,如果合并失败(并引发异常),它使用format_kwargs_error来try 引发different异常.

_PyDict_MergeEx的第三个参数为2时,该函数将在dict_merge助手函数内为重复的键引发KeyError.这就是KeyError的来源.

一旦提出KeyErrorformat_kwargs_error就会用TypeError替换它.它try 用the following code:

    else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
        PyObject *exc, *val, *tb;
        _PyErr_Fetch(tstate, &exc, &val, &tb);
        if (val && PyTuple_Check(val) && PyTuple_GET_SIZE(val) == 1) {

但此代码正在寻找unnormalized异常,这是一种表示异常的内部方式,不会expose 于Python级别代码.它期望异常值是一个包含引发KeyHelp的键的1元素数组,而不是实际的异常对象.

C代码中引发的异常通常是未规范化的,但如果它们发生在Python处理另一个异常时,则不会.非规范化异常无法处理exception chaining,对于异常处理程序内部引发的异常,这会自动发生.在这种情况下,内部_PyErr_SetObject routine 将automatically normalize异常:

    exc_value = _PyErr_GetTopmostException(tstate)->exc_value;
    if (exc_value != NULL && exc_value != Py_None) {
        /* Implicit exception chaining */
        Py_INCREF(exc_value);
        if (value == NULL || !PyExceptionInstance_Check(value)) {
            /* We must normalize the value right now */

由于KeyError已正常化,format_kwargs_error不明白它正在查看什么.它让KeyError通过,而不是提高它应该提高的TypeError.


在Python 3.12上,情况有所不同.内部异常表示已更改,因此任何引发的异常都将被规范化always.因此,format_kwargs_error的Python 3.12版本会查找规范化异常而不是未规范化异常,如果_PyDict_MergeEx引发了KeyError,代码就会识别它:

    else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
        PyObject *exc = _PyErr_GetRaisedException(tstate);
        PyObject *args = ((PyBaseExceptionObject *)exc)->args;
        if (exc && PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) {

Python相关问答推荐

判断两极中N(N 2)列水平是否相等

单击cookie按钮,但结果不一致

如何修复fpdf中的线路出血

在Docker中运行HAProxy时无法获得503服务

Flask主机持续 bootstrap 本地IP| Python

替换字符串中的点/逗号,以便可以将其转换为浮动

Python主进程和分支进程如何共享gc信息?

Class_weight参数不影响RandomForestClassifier不平衡数据集中的结果

我必须将Sigmoid函数与r2值的两种类型的数据集(每种6个数据集)进行匹配,然后绘制匹配函数的求导.我会犯错

如何使用scipy从频谱图中回归多个高斯峰?

按顺序合并2个词典列表

如何在Raspberry Pi上检测USB并使用Python访问它?

如何使用表达式将字符串解压缩到Polars DataFrame中的多个列中?

所有列的滚动标准差,忽略NaN

将输入聚合到统一词典中

如何并行化/加速并行numba代码?

如何在Python中使用Pandas将R s Tukey s HSD表转换为相关矩阵''

将scipy. sparse矩阵直接保存为常规txt文件

如何在BeautifulSoup/CSS Select 器中处理regex?

合并与拼接并举