通常,如果您尝试为同一关键字参数传递多个值,您会收到 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 上重现。
应该引发
TypeError
的代码通过检测和替换初始
KeyError
来工作,但此代码无法正常工作。当异常发生在另一个异常处理程序中间时,应引发
TypeError
的代码无法识别
KeyError
。它最终让
KeyError
通过,而不是用
TypeError
替换它。
正如我们通过使用 模块检查
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
。