代码之家  ›  专栏  ›  技术社区  ›  Waket Zheng

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

  •  9
  • Waket Zheng  · 技术社区  · 2 年前

    通常,如果您试图为同一个关键字参数传递多个值,则会得到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

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

    1 回复  |  直到 2 年前
        1
  •  9
  •   user2357112    2 年前

    这看起来像一个Python错误。

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

    由于异常实现中的更改,错误似乎在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 尝试合并两个dict,如果失败(并引发异常),则使用 format_kwargs_error 试图筹集 不同的 例外

    当第三个参数 _PyDict_MergeEx 是 2 ,该函数将引发 KeyError 对于重复的密钥,在 dict_merge helper函数。这就是 KeyError 来自。

    一旦 KeyError 被升高, format_kwargs_error 的工作是将其替换为 类型错误 。它试图用 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) {
    

    但此代码正在查找 未规范化 exception,一种表示未暴露于Python级别代码的异常的内部方式。它希望异常值是一个包含引发KeyError的键的1元素元组,而不是实际的异常对象。

    C代码内部引发的异常通常是未规范化的,但如果它们发生在Python处理另一个异常时,则不会进行规范化。无法处理未规范化的异常 exception chaining ,这对于异常处理程序内部引发的异常会自动发生。在这种情况下,内部 _PyErr_SetObject 例行遗嘱 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 通过,而不是提高 类型错误 应该是的。


    在Python 3.12上,情况有所不同。内部异常表示形式已更改,因此任何引发的异常都是 总是 归一化。因此,Python 3.12版本的 format_kwargs_error 查找规范化异常而不是未规范化异常,如果 _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) {