Обычно, если вы пытаетесь передать несколько значений для одного и того же аргумента ключевого слова, вы получаете ошибку 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.
@blues: Их не смущает наличие ошибки. Они не понимают, почему это другая ошибка, чем обычно.
Не по делу, но единственное исключение: это плохая практика. Вместо этого используйте конкретное исключение, которое вы ожидаете, т. е. except ValueError:
. Тем не менее, это не влияет на проблему, я просто хотел ее исключить.
CPython 3.8.10 также затронут.
id
тоже сбивает с толку, потому что это встроенная функция Python.
@qwr Он используется как ключ dict, я не думаю, что это проблема/сбивает с толку, и для объектов вполне обычно/нормально иметь поле id
. Кроме того, встроенная функция id
используется редко.
@nocomment то, что встроенная функция используется редко, именно поэтому это сбивает с толку.
@qwr Как это сбивает с толку?
@nocomment я что-то упустил, или ключ dict — это функциональный объект id
, а не строка «id»?
@qwr Это строка «id». При использовании dict(key=value)
ключ действует как ключевое слово параметра и, таким образом, является строкой. Например, таким образом вы не сможете использовать целое число в качестве ключа; dict(1=2)
не удастся: при использовании dict()
все ключи должны быть строками. Таким образом, id
в данном контексте является строкой.
@ 9769953 о, я понимаю. он отличается от синтаксиса литерала dict с фигурными скобками.
@qwr Да, это одна из тех ошибок Python. Ситуация ухудшается, когда вы используете итерацию в качестве аргумента, например. dict(zip(range(10), range(20, 30)))
, что позволяет использовать все виды нестроковых объектов; это эквивалентно более четкому пониманию списка: {key: value for key, value in zip(range(10), range(20, 30))}
.
Это похоже на ошибку 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: Это не инструкция RETURN_CONST
. Они изменили представление исключений. Ненормализованных исключений сейчас практически нет. format_kwargs_error
теперь ищет нормализованное исключение и находит его.
Понимаете, почему
dict(id=1, id=2)
— ошибка? Ваш код фактически тот же