Neste post veremos como transformar os bugs em features, em outras palavras, como lidar com "erros" ao desenvolvermos nossas regras.
Sendo assim, aqui falaremos sobre como lançar exceções com o rise
, como tratá-las utilizando o try/except
e como criar exceções personalizadas para melhorar a legibilidade do nosso código.
Estaremos utilizando a linguagem Python, mas os conceitos abordados servirão para qualquer linguagem de programação que siga estes princípios de tratamento e lançamento de exceções.
Índice
- Erros e Exceções
- Bug vs Exceções
- Tratando erros com o try/except
- Lançando exceções
- Exceções personalizadas
- Conclusão
- Bônus - Pytest
Erros e Exceções
Por que lançar e tratar exceções?
É muito comum encontrar códigos como este:
def diga_ola(nome):
if len(nome) < 5:
return false
elif len(nome) > 100:
return false
elif name.isnumeric():
return false
else:
return f'ola {nome}'
Bom, claramente essas verificações estará relacionada com nossa regra de negócio. Sendo assim, para entender seu funcionamento teremos que ler e interpretar o que está acontecendo e em qual ordem.
O problema é quando executamos a função e ela retorna um false
na nossa cara. Mas e aí?! Onde falhou?? Em qual parte da regra ela não passou?? E é nessas horas que fazemos o velho print('aqui 1')
, print('aqui 2')
, print('aqui N')
para saber até onde o código está chegando.
Na tentativa de "melhorar" ainda fazemos assim:
'''
Função para dizer olá
'''
def diga_ola(nome):
// Verifica se o nome é menor que 5
if len(nome) < 5:
return false
// Verifica se o nome é maior que 100
elif len(nome) > 100:
return false
// Verifica se o nome é um número
elif name.isnumeric():
return false
// Retornao nome formatado: Olá + nome
else:
return f'ola {nome}'
Se antes já estava ruim, agora que piorou. Nada pythonico!
Vamos ver formas melhores de organizar este código.
Bug vs Exceções
Resumidamente
- Exceções são erros esperados, previsíveis e conhecidos de alguma situação. Ex: Divisão entre dois números
x / y
. Já é esperado que ocorra um erro caso o valory == 0
, logo podemos criar formas de tratar antes que trave o sistema. - Já o bug é um erro desconhecido, algo que não estava sendo esperado em uma funcionalidade. Ex: Exibir um alerta no navegador do usuário. Poderá acontecer N situações, como tamanhos de telas diferentes, navegadores diferentes, erro de conexão com a internet, desabilitar a execução de scripts no navegador, etc. Apesar de imarginarmos e tentar tratar algum deles, em algum momento essa "exceção não prevista" poderá travar o funcionamento de uma página.
Um bug conhecido poderá ser uma exceção tratada!
Caso queira se aprofundar nas terminologias poderá pesquisar sobre o padrão IEEE nº 610.12-1990 ou neste artigo da Dev Media Gestão de defeitos
Tratando erros com o try/except
Tente executar o cód a baixo
print(1/0)
O resultado será uma mensagem de erro:
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Podemos melhorar esta mensagem tratando a exceção e isolando-a numa função
def divide_numeros(dividendo, divisor):
try:
print(dividendo/divisor)
except:
print ('O divisor não pode ser 0.')
divide_numeros(1, 0)
O resultado será: O divisor não pode ser 0.
Mas o que acontece se executarmos: divide_numeros(1, 'teste')
?
O resultado será exatamente a mesma mensagem! Portanto, precisamos tratar as exceções individualmente e exibir a mensagem adequada.
Podemos reescrever assim:
def divide_numeros(dividendo, divisor):
try:
print(dividendo/divisor)
except ZeroDivisionError:
print ('O divisor não pode ser 0.')
except TypeError:
print('Insira apenas números')
Lançando exceções
Vamos falar agora de exceções relacionados a nossa regra de negócio.
Vamos rever o cenário onde nosso sistema terá uma mensagem de saudação para um usuário. Porém teremos algumas regras:
- O nome não poderá ser números;
- O nome deverá ter menos que 100 caracteres
- O nome deverá ter mais que 5 caracteres
Como já vimos antes, encher o código de if e else não é legal. Por isso faremos um código com tratamento de exceções. Começaremos criando nossa função:
def diga_ola(name: str) -> str:
return f'Olá {nome}'
O método acima recebe uma string por parâmetro e retorna uma string como resposta. Então poderíamos executar da seguinte maneira: diga_ola('Flávio')
.
Como estamos especificando que o parâmetro recebido deverá ser uma string, ao tentar passar um número teremos um erro: diga_ola(123)
AttributeError: 'int' object has no attribute 'isnumeric'
Sendo assim, já podemos capturar nossa primeira exceção
try:
print(diga_ola(123))
except AttributeError:
print('Insira apenas texto')
Para deixarmos dinâmico, vamos pedir através de um input para que o nome seja inserido:
try:
name = input('Insira seu nome: ')
print(diga_ola(name))
except AttributeError:
print('Insira apenas texto')
Faça o seguinte teste:
- Execute seu código
- Insira um o número
123
- O resultado será:
Olá 123
Como assim? Acabamos de tratar este erro...
O problema é que quando se trata de um input todo dado é uma string, logo atende ao nosso requesido. Por isso, precisaremos tratar dentro de nosso método e "lançar" uma nova exceção.
Lançando uma exceção
Para lançar uma nova exceção utilizaremos a palavra reservada rise
e dizer qual é seu tipo e a mensagem. Vamos modificar nosso método:
def diga_ola(name: str) -> str:
if name.isnumeric():
raise TypeError('Apenas letras')
return f'Olá {nome}'
No trecho raise TypeError('Apenas letras')
estamos lançando um exceção do tipo TypeError
e passando por parâmetro a mensagem que será exibido.
Conheça a lista de exceções padrões que poderá ser lançada que são nativas da linguagem Python na docs.
Já seguindo este curso, vamos implementar as outras regras:
def diga_ola(name: str) -> str:
if name.isnumeric():
raise TypeError('Apenas letras.')
if len(name) > 100:
raise OverflowError('Nome muito grante.')
if len(name) < 5:
raise ValueError('Nome pequeno.')
return f'Olá {name}'
E podemos chamá-lo assim:
try:
name = input('Insira seu nome: ')
print(diga_ola(name))
except AttributeError:
print('Insira apenas texto')
except ValueError as error:
print(error)
Está bem melhor de entender, mas ainda podemos melhorar.
Ao verificar se o parâmetro é menor que 5, nós lançamos uma função genérica e exibindo sua mensagem. Será que poderíamos criar "Exceções Personalizadas" e tratá-las da maneira ideal e não de forma genérica?
Exceções personalizadas
Criar exceções personalizadas nos ajudará a deixar nosso código mais organizado e legível.
Vamos criar um arquivo chamado exceptions.py
com o seguinte conteúdo:
class NomePequenoException(ValueError):
pass
Aqui nós estamos criando uma classe que estende a classe ValueError
. Apesar de não executar nada, nos será muito útil na leitura do código. Veja nosso código final completo, onde faremos a importação da exceção e tratamos de forma individual:
import Exceptions
def diga_ola(name: str) -> str:
if name.isnumeric():
raise ValueError('Apenas letras.')
if len(name) > 100:
raise OverflowError('Nome muito grante.')
if len(name) < 5:
raise Exceptions.NomePequenoException('Nome pequeno.')
return f'Olá {name}'
def main():
try:
name = input('Insira seu nome: ')
print(diga_ola(name))
except AttributeError:
print('Insira apenas texto')
except Exceptions.NomePequenoException:
print('Insira nomes com 5 ou mais letras')
except ValueError as error:
print(error)
if __name__ == '__main__':
main()
Conclusão
Desenvolver um bom código não é só "o que funciona", mas também é se preocupar com a organização e legibilidade para facilitar a manutenção. Boa parte do tempo de desenvolvimento é encontrando falhas. Por isso, organizar as exceções ao desenvolver novas features nos ajudará a rastrear o erro e facilitará no processo de teste.
No exemplo que criamos temos o caso onde a função será chamada em apenas um lugar. Mas em um sistema maior, onde será chamado em vários lugares, por várias pessoas, poderemos ter formas diferentes de tratar estas exceções: fazendo um print, registrando um log, retornando a mensagem para um dashboard em um frontend, etc. Por isso, é uma boa prática deixar a cargo de quem executa o método decidir o que fazer quando houver a exceção.
Dica: Ficamos presos em criar o menor número de linhas, a menor quantidade de arquivos, os nomes mais compactos de variáveis e métodos. Mas hoje, com a grande capacidade computacional e diversas ferramentas para compactar nosso código, a quantidades de bytes em um arquivo é o menor dos problemas. Sendo assim, não exite em criar arquivos de exceções, testes e escrever nomes coerentes para seus componentes.
Bônus: pytest
Deixo aqui minha sugestão off topic para o estudo de testes automatizados. A baixo terá as instruções para executar os testes aplicados a nosso exemplo.
Criando ambiente
venv
python3 -m venv venv
Caso esteja no Windows e tenha erro ao encontrar o Python, tente executar assim ou consulte a documentação:
py -m venv venv
Ative o ambiente
Linux: source venv/bin/activate
Windows: venv/scripts/activate
Instalando o pytest
pip install pytest
Arquivo te testes
Crie o arquivo test_exceptions.py
(todos os testes deverão começar com test_*)
import pytest
import random
import Exceptions
from main import diga_ola
def test_quando_o_nome_for_composto_maior_que_5_letras():
assert diga_ola('Flávio Filipe') == 'Olá Flávio Filipe'
def test_quando_o_nome_for_menor_que_5_letras_nao_deve_permitir():
with pytest.raises(Exceptions.NomePequenoException):
diga_ola('abc')
def teste_quando_inserir_numero_nao_deve_permitir():
with pytest.raises(TypeError):
diga_ola('123')
def test_nome_maior_que_100_letras():
with pytest.raises(OverflowError):
nome = 'a'*100+'a'
diga_ola(nome)
Os trechos com
with pytest.raises(...):
irá verificar se o retorno do contexto analisado será o erro especificado por parâmetro.Executando os testes
Execute na raiz do projeto:
pytest
Top comments (1)
Não li o artigo, más só um pequeno truque: quando usares os 3 ' para representar código no markdown, se escreveres python à frente dos primeiros 3 ficas com syntax highlight.