Normalmente, se você tentar passar vários valores para o mesmo argumento de palavra-chave, obterá um 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'
Mas se você fizer isso enquanto lida com outra exceção , você receberá um 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'
O que está acontecendo aqui? Como uma exceção completamente não relacionada poderia afetar o tipo de exceção dict(id=1, **{'id': 2})
lançada?
Para contextualizar, descobri esse comportamento ao investigar o seguinte relatório de bug: https://github.com/tortoise/tortoise-orm/issues/1583
Isso foi reproduzido no CPython 3.11.8, 3.10.5 e 3.9.5.
Isso parece um bug do Python.
O código que deveria aumentar
TypeError
funciona detectando e substituindo um inicialKeyError
, mas esse código não funciona direito. Quando a exceção ocorre no meio de outro manipulador de exceção, o código que deveria gerar oTypeError
arquivoKeyError
. Acaba deixando passarKeyError
, em vez de substituí-lo por um arquivoTypeError
.O bug parece ter desaparecido na versão 3.12, devido a mudanças na implementação da exceção.
Aqui está o aprofundamento do código-fonte do CPython 3.11.8. Código semelhante existe em 3.10 e 3.9.
Como podemos ver usando o
dis
módulo para examinar o bytecode paradict(id=1, **{'id': 2})
:Python usa o
DICT_MERGE
opcode para mesclar dois dictos, para construir o dict do argumento da palavra-chave final.A parte relevante do
DICT_MERGE
código é a seguinte:Ele tenta
_PyDict_MergeEx
mesclar dois dictos e, se isso falhar (e gerar uma exceção), ele tentaformat_kwargs_error
gerar uma exceção diferente .Quando o terceiro argumento
_PyDict_MergeEx
for2
, essa função gerará umKeyError
para chaves duplicadas, dentro dadict_merge
função auxiliar. É daí queKeyError
vem.Uma vez que o
KeyError
é gerado,format_kwargs_error
tem a função de substituí-lo por umTypeError
. Ele tenta fazer isso com o seguinte código :mas este código está procurando uma exceção não normalizada , uma forma interna de representar exceções que não está exposta ao código em nível Python. Ele espera que o valor da exceção seja uma tupla de 1 elemento contendo a chave para a qual KeyError foi gerado, em vez de um objeto de exceção real.
Exceções levantadas dentro do código C geralmente não são normalizadas, mas não se ocorrerem enquanto o Python estiver lidando com outra exceção. Exceções não normalizadas não podem tratar o encadeamento de exceções , que ocorre automaticamente para exceções geradas dentro de um manipulador de exceções. Neste caso, a
_PyErr_SetObject
rotina interna normalizará automaticamente a exceção:Como o
KeyError
foi normalizado,format_kwargs_error
não entende o que está vendo. Ele deixaKeyError
passar, em vez de aumentar oTypeError
que deveria.No Python 3.12, as coisas são diferentes. A representação da exceção interna foi alterada, portanto qualquer exceção levantada é sempre normalizada. Assim, a versão Python 3.12
format_kwargs_error
procura uma exceção normalizada em vez de uma exceção não normalizada e, se_PyDict_MergeEx
gerou umKeyError
, o código irá reconhecê-lo: