Почему `dict(id=1, **{'id': 2})` иногда вызывает `KeyError: 'id'` вместо TypeError?

Обычно, если вы пытаетесь передать несколько значений для одного и того же аргумента ключевого слова, вы получаете ошибку 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.

Понимаете, почему dict(id=1, id=2) — ошибка? Ваш код фактически тот же

blues 04.05.2024 08:23

@blues: Их не смущает наличие ошибки. Они не понимают, почему это другая ошибка, чем обычно.

user2357112 04.05.2024 08:33

Не по делу, но единственное исключение: это плохая практика. Вместо этого используйте конкретное исключение, которое вы ожидаете, т. е. except ValueError:. Тем не менее, это не влияет на проблему, я просто хотел ее исключить.

wjandrea 04.05.2024 19:59

CPython 3.8.10 также затронут.

wjandrea 04.05.2024 20:00
id тоже сбивает с толку, потому что это встроенная функция Python.
qwr 04.05.2024 23:15

@qwr Он используется как ключ dict, я не думаю, что это проблема/сбивает с толку, и для объектов вполне обычно/нормально иметь поле id. Кроме того, встроенная функция id используется редко.

no comment 04.05.2024 23:48

@nocomment то, что встроенная функция используется редко, именно поэтому это сбивает с толку.

qwr 05.05.2024 00:00

@qwr Как это сбивает с толку?

no comment 05.05.2024 00:08

@nocomment я что-то упустил, или ключ dict — это функциональный объект id, а не строка «id»?

qwr 06.05.2024 14:40

@qwr Это строка «id». При использовании dict(key=value) ключ действует как ключевое слово параметра и, таким образом, является строкой. Например, таким образом вы не сможете использовать целое число в качестве ключа; dict(1=2) не удастся: при использовании dict() все ключи должны быть строками. Таким образом, id в данном контексте является строкой.

9769953 06.05.2024 16:56

@ 9769953 о, я понимаю. он отличается от синтаксиса литерала dict с фигурными скобками.

qwr 06.05.2024 17:16

@qwr Да, это одна из тех ошибок Python. Ситуация ухудшается, когда вы используете итерацию в качестве аргумента, например. dict(zip(range(10), range(20, 30))), что позволяет использовать все виды нестроковых объектов; это эквивалентно более четкому пониманию списка: {key: value for key, value in zip(range(10), range(20, 30))}.

9769953 06.05.2024 17:24
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
49
12
1 577
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это похоже на ошибку 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_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, чтобы попытаться вызвать другое исключение.

Если третий аргумент _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, как предполагалось.


В Python 3.12 все по-другому. Внутреннее представление исключений было изменено, поэтому любое возникающее исключение всегда нормализуется. Таким образом, версия 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) {

Кстати, похоже, это решено. Добавлена ​​инструкция ( RETURN_CONST). try..except дает TypeError в 3.12.1.

Timeless 04.05.2024 09:42

@Timeless: Это не инструкция RETURN_CONST. Они изменили представление исключений. Ненормализованных исключений сейчас практически нет. format_kwargs_error теперь ищет нормализованное исключение и находит его.

user2357112 04.05.2024 09:43

Другие вопросы по теме