Categorias
Artigos

Gerando uma exceção em Python com um nome significativo, e uma mensagem significativa

Aqui está uma tarefa que é para ser simples mas que considero mal documentada no mundo Python.

O problema: estou desenvolvendo uma biblioteca para simular uma determinada topologia de ímãs permanentes. O usuário deve entrar com alguns parâmetros geométricos, mas antes de qualquer coisa o programa deve verificá-los e avisar quais são inválidos.

Em termos de programação, estamos falando de exceções: condições literalmente excepcionais que não devem acontecer mas, se acontecem, é melhor parar o programa para que o usuário possa verificar. O tutorial oficial de Python explica como criar exceções personalizadas, e a documentação de referência mostra os tipos de exceções que já existem em Python. Este tópico também é bem abordado em [1].

Pesquisando a documentação sobre as exceções já construídas, parece-me que a exceção aproximada que descreve esse problema é ValueError. O que eu quero então é definir um erro que diga que o “valor errado” é precisamente um “parâmetro inválido”. O problema na documentação que citei antes é precisamente que eu não estava conseguindo entender como a mensagem de erro pode ser customizada para indicar qual parâmetro é inválido, pois as exceções aparentemente não aceitam strings como argumentos…

…mas exceções aceitam uma tupla como argumento, e é isso que não estava claro para mim. Veja o código a seguir:

class InvalidParameterError(ValueError):
pass
view raw custom_errors.py hosted with ❤ by GitHub
from custom_errors import InvalidParameterError
def validate(p):
for k, v in p.items():
if v < 0:
emsg = "Parameter '%s' cannot be negative" %(k,)
raise InvalidParameterError(emsg)
view raw parse_parameters.py hosted with ❤ by GitHub
import pytest
from custom_error import InvalidParameterError
from parse_parameters import validate
def test_negative_parameters_raise_error():
sample_params = {
'a': 1.43,
'b': 57,
'c': -10
}
with pytest.raises(InvalidParameterError) as excinfo:
validate(sample_params)
# The object returned by this pytest context manager
# has a 'value' field assigned to the exception object itself.
# In addition, all built-in exceptions derived from Exception
# have an 'args' tuple field assigned to the passed arguments.
# Hence, when we create a custom error with a string as the first argument,
# this string is assigned to the first element of the tuple 'args'
exception_msg = excinfo.value.args[0]
expected_message = "Parameter 'c' cannot be negative".
assert exception_msg == expected_message

No primeiro programa, eu defino uma exceção que deriva de ValueError. No segundo, crio uma função de validação, que cria essa minha exceção personalizada, e a constrói com uma string de mensagem indicando qual foi o problema. E posso testar essa função usando pytest, conforme está nos comentários do terceiro programa: tudo que eu passei para InvalidParameterErrorfoi armazenado internamente como uma tupla args; se só passei uma string, esta foi armazenada no primeiro elemento, ou seja, em args[0]. E posso testar se a mensagem criada era a que eu estava esperando.

A minha biblioteca de ímãs permanentes é mais complexa que isto, mas a lógica é idêntica: se alguém tentar usá-la com um parâmetro inválido, o programa vai fazer uma verificação e imprimir uma mensagem de InvalidParameterError na tela.

É como falei no início: era para ser simples, mas no meio do caminho me diverti muito aprendendo sobre exceções em Python e escrevendo sobre elas. E agora vou voltar a realmente trabalhar na minha biblioteca, e estou muito mais seguro de que ela vai funcionar como deveria.

[1]: Beazley, David & Jones, Brian K. Python Cookbook. São Paulo: Novatec, 2013 (link afiliado para compra)