为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError?

问题描述 投票:0回答:1

通常,如果您尝试为同一关键字参数传递多个值,您会收到 TypeError:

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'

但是如果你在处理另一个异常时这样做,你会得到一个KeyError:

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

这已在 Python 3.11、3.10 和 3.9 上重现。

python
1个回答
0
投票
这看起来像是一个 Python bug。

应该引发

TypeError

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


这里是深入探讨。

正如我们通过使用

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_MERGE

代码相关部分如下:

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

_PyDict_MergeEx

 尝试合并两个字典,如果失败(并引发异常),它使用 
format_kwargs_error
 尝试引发 
different 异常。

_PyDict_MergeEx

 的第三个参数是 
2
 时,该函数将在 
KeyError
辅助函数内针对重复键引发
dict_merge
。这就是
KeyError
的由来。

一旦

KeyError

 升起,
format_kwargs_error
 就可以将其替换为 
TypeError
。它尝试使用
以下代码来做到这一点:

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) {
但此代码正在寻找“非标准化”异常,这是一种表示未暴露给 Python 级代码的异常的内部方法。它期望异常值是一个 1 元素元组,其中包含引发 KeyError 的键,而不是实际的异常对象。

C 代码中引发的异常通常是非规范化的,但如果它们在 Python 处理另一个异常时发生则不是。非规范化异常无法处理“异常链接”,异常链接会在异常处理程序内引发的异常自动发生。在这种情况下,内部_PyErr_SetObject

例程将

自动标准化异常:

    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
© www.soinside.com 2019 - 2024. All rights reserved.