DEV Community

Cover image for [Python] Exceções - Transformando bugs em features
Flávio Filipe
Flávio Filipe

Posted on

[Python] Exceções - Transformando bugs em features

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

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}'
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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 valor y == 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)
Enter fullscreen mode Exit fullscreen mode

O resultado será uma mensagem de erro:

Traceback (most recent call last):
    ...
ZeroDivisionError: division by zero
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Sendo assim, já podemos capturar nossa primeira exceção

try:
    print(diga_ola(123))
except  AttributeError:
    print('Insira apenas texto')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
joaomcteixeira profile image
João M.C. Teixeira

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.