Python Iluminado

Erros, Exceções e Testes

É muito comum encontrarmos erros ao escrevermos programas, o Interpretador Python é capaz de gerar exceções quando encontra erros, por exemplo uma divisão por zero.

Um dos erros mais comuns de encontrarmos é o de sintaxe, também conhecidos como parsing errors, quando não seguimos a estrutura correta da linguagem, chamamos esse erro de erro de síntaxe. Por exemplo:

x = 10
y = 3
if x > y
print(f"{x} é maior que {y}")
# File "<stdin>", line 1
# if x > y
# ^
# SyntaxError: invalid syntax

Perceba, que de propósito, omitimos o : e o interpretador nos retorna o erro SyntaxError, indicando que a sintaxe é inválida.

Além disso, erros também podem acontecer em tempo de execução, nesse caso eles são conhecidos como exceções. Eles ocorrem, por exemplo quando um arquivo que estamos tentando abrir não existe FileNotFoundError, dividir por zero ZeroDivisionError, módulos que tentamos importar e não são encontrados ImportError e uma série de outros.

Quando esses erros em tempo de execução ocorrem, Python cria um objeto de exceção, e caso este não seja tratado de maneira própria, será impresso um traceback daquele erro e mais detalhes do motivo que ocorreu.

Vejamos então algumas dessas exceções. Vamos começar tentando dividir um número por 0:

x = 1/0
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ZeroDivisionError: division by zero

Tentando abrir um arquivo que não existe em nosso sistema de arquivos:

f = open("arquivo.txt")
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# FileNotFoundError: [Errno 2] No such file or directory: 'arquivo.txt'

Tentando trabalhar com uma variável que não foi definida:

10 * z
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# NameError: name 'z' is not defined

Realizando uma conversão inválida (não podemos converter um número complexo para inteiro):

int(complex(1,3))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: can't convert complex to int

Exceções Construídas no Python

Operações ilegais poderam gerar exceções, existem diversas exceções construídas em Python que são geradas quando erros correspondentes ocorrem, para visualizarmos elas precisamos usar a função locals():

e = locals()['__builtins__']
print(dir(e)) # Nos será trazido uma lista de exceções construídas
print(type(e)) # <class 'module'>

Como podemos ver, essa função nos retornará uma <class 'module'> com exceções construídas, funções e atributos. Algumas das exceções mais comuns são listadas na tabela a seguir:

ExceçãoCausa
AssertionErrorSurge quando o comando assert falha
AttributeErrorSurge quando uma atribuição ou referência falha
EOFErrorSurge quando a função input() chega na condição de fim de arquivo
FloatingPointErrorSurge quando uma operação de ponto flutuante falha
GeneratorExitSurge quando o método close() do gerador é chamado
ImportErrorSurge quando o módulo importado não é encontrado
IndexErrorSurge quando o índice de uma sequência está fora de alcance
KeyErrorSurge quando uma chave não é encontrada no dicionário
KeyboardInterruptSurge quando um usuário aperta o botão de interrupção (CTRL + C ou delete)
MemoryErrorSurge quando acaba a memória de uma operação
NameErrorSurge quando uma variável não é encontrada no escopo local ou global
NotImplementedErrorSurge por métodos abstratos
OSErrorSurge quando alguma operação de sistema causa algum erro de sistema
OverflowErrorSurge quando uma operação aritmética é muito grande para ser representada
ReferenceErrorSurge quando um proxy de referência fraco é usado para acessar um garbage collected referente
RuntimeErrorSurge quando um erro não se encaixa em nenhuma outra categoria
StopIterationSurge pela função next() para indicar que não há mais itens para o iterador retornar
SyntaxErrorSurge pelo parser quando um erro de síntaxe ocorre
IndentationErrorSurge quando não indentamos nosso código
TabErrorSurge quando a indentação consiste de espaços e tabs impróprios
SystemErrorSurge quando o interpretador detecta um erro interno
SystemExitSurge pela função sys.exit()
TypeErrorSurge quando uma função ou operação é aplicada a um objeto de tipo incorreto
UnboundLocalErrorSurge quando uma referência é feita para uma variável local em uma função ou método, porém nenhum valor está preso à variável
UnicodeErrorSurge quando existe um erro relacionado a Unicode codificação ou decodificação
UnicodeEncodeErrorSurge quando um erro de codificação de Unicode ocorre
UnicodeDecodeErrorSurge quando um erro de decodificação de Unicode ocorre
UnicodeTranslateErrorSurge quando um erro de tradução de Unicode ocorre
ValueErrorSurge quando uma função pega um argumento de tipo correto, porém de valor impróprio
ZeroDivisionErrorSurge quando a segunda divisão ou módulo é zero

Além delas, podemos definir nossas próprias exceções, caso seja necessário, e para lidarmos com elas utilizamos as palavras-chave try, except e finally.

Palavra-chaveDescrição
trynos permite testar um bloco de código por erros
exceptpermite lidarmos com erros
finallypermite executarmos códigos independente do resultado do try e do except

Lidando com Exceções

try:
print(x)
except:
print("Uma exceção ocorreu")

O bloco try irá gerar uma exceção, uma vez que x não está definida, uma vez gerada, o bloco except é executado, caso não tivéssemos o bloco try o programa iria "crashar" e um erro surgiria. Podemos também definir diversos blocos de exceção.

try:
print(x)
except NameError:
print("Variável x não está definida")
except:
print("Algum outro erro ocorreu")

O bloco finally, se especificado, será executado independente de surgir ou não um erro no try:

try:
print(x)
except:
print("Algum erro ocorreu")
finally:
print("O try e except foram finalizados")

Com input de usuário:

try:
a = int(input('Diga-me um número: '))
b = int(input('Diga-me um outro número: '))
print(a/b)
except:
print('erro no input do usuário')

Lidando com Exceções Específicas

Podemos ter claúsulas de except separadas para lidar com um tipo particular de exceção:

try:
a = int(input('Diga-me um número: '))
b = int(input('Diga-me um outro número: '))
print("a/b = ", a/b)
print("a+b = ", a+b)
except ValueError: # Só executa se esse erro surgir
print('Não podemos converter para um número')
except ZeroDivisionError: # Só executa se esse erro surgir
print('Não podemos dividir por zero')
except: # executa para todos os outros erros
print('algo de muito errado aconteceu')

O que fazer com Exceções?

O que fazer quando encontramos um erro? Temos algumas opções:

  • Falhar silenciosamente - Substituir valores padrão ou apenas continuar - Pode ser uma má ideia! Usuários não obtem avisos

  • Retornar um valor de erro - Qual valor escolher? - Complica o código tendo de checar por um valor especial

  • Parar a execução, condição de sinalização de erro - Em Python usar a palavra-chave: raise para disparar uma exceção - raise Exception('string descritiva do erro')

Testes, Debugging, Assertions

Programação Defensiva

A programação defensiva é uma forma de design defensivo destinada a garantir o funcionamento contínuo de uma parte do software em circunstâncias imprevistas. Práticas de programação defensivas são freqüentemente usadas onde alta disponibilidade, segurança ou proteção é necessária.

  • Escrever especificações (contratos) para as funções
  • Modularizar os programas, de forma a facilitar os testes
  • Checar condições em inputs/outputs (assertions)

Testando/Validando

Os testes de unidade são tipicamente testes automatizados escritos e executados por desenvolvedores de software para garantir que uma seção de um aplicativo (conhecida como "unidade") atenda ao seu design e se comporte conforme o esperado. Na programação procedural, uma unidade pode ser um módulo inteiro, mas é mais comumente uma função ou procedimento individual.

  • Comparar pares de input/output a uma especificação
  • "Não está funcionando!": Reescrever a unidade até passar nos testes
  • "Como eu posso 'quebrar' o meu programa?": Inputs que possam levar o programa a falhar

Debugging

Na programação de computadores e no desenvolvimento de software, o Debugging é o processo de localização e resolução de bugs (defeitos ou problemas que impedem a operação correta) em programas de computador, software ou sistemas.

  • Estudar Eventos que levam a um erro
  • "Por que não está funcionando?"
  • "Como eu posso consertar meu programa?"

O Debugging tem uma linha de aprendizado íngrime, seu objetivo é termos um programa livre de bugs.

Podemos usar Ferramentas para nos auxiliar no processo de Debugging:

  • Pré-construída no IDLE e Anaconda
  • Python Tutor
  • o statement print()

Mentalidade para Testing e Debugging

  • Dividir o programa em módulos que possam ser testados e debuggados individualmente
  • Documentar especificações nos módulos - O que você espera que o input seja? - O que você espera que o output seja?
  • Documentar suposições envolvidas no design do código

Quando você está pronto para Testar?

  • Tenha certeza que o código está rodando - Remova erros de sintaxe - Remova erros semântico estático - O interpretador Python normalmente pode encontrar esses erros para você
  • Tenha um conjunto de resultados esperados - Um conjunto de input - Para cada input, um output esperado

Classes de Testes

  • Teste Unitário - Valida cada peça do programa - Testa cada função separadamente

  • Testa de Regressão - Adicione testes para bugs conforme você os encontra - Capture erros reintroduzidos que foram previamente corrigidos

  • Teste de Integração - O programa completo funciona? - Normalmente tende-se a vir direto para esta etapa, mas não é o ideal

Statement Print

  • Uma boa maneira de testar hipóteses
  • Quando usar print()? - entrada da função - para imprimir parâmetros - resultados de funções

Passos no Debugging

  • Estude o código do programa - Não pergunte o que está errado - Pergunte como eu obtive o resultado inesperado

  • Método científico - Estude dados disponíveis - Formule hipóteses - Experimentos repetidos - Pegue um input simples para testar

Assertions

Em programação de computador, especificamente ao usar o paradigma de programação imperativo, uma assertion é um predicado (uma função de valor booleano sobre o espaço de estado, geralmente expressa como uma proposição lógica usando as variáveis de um programa) conectado a um ponto no programa, que sempre deve ser avaliado como verdadeiro naquele ponto da execução do código. As assertions podem ajudar um programador a ler o código, ajudar um compilador a compilá-lo ou ajudar o programa a detectar seus próprios defeitos.

  • Tenha certeza que suposições no estado da computação são como o esperado
  • Use um statement assert para invocar raise de uma exceção AssertionError caso as suposições não se concretizem
  • Um exemplo de boa programação defensiva

Exemplo

def media(notas):
# A função encerrará imediatamente caso o assertion não seja concretizado
assert len(notas) != 0, 'nenhum dado para notas'
return sum(notas)/len(notas)
  • raises um AssertionError se for fornecido uma lista vazia para a função media()
  • Caso contrário, irá rodar normal

Vamos agora criar algumas funções para testarmos:

def adicao(x, y):
"""Função de Adição"""
return x + y
def subtracao(x, y):
"""Função de Subtração"""
return x - y
def multiplicacao(x, y):
"""Função de Multiplicação"""
return x * y
def divisao(x, y):
"""Função de Divisão"""
if y == 0:
raise ValueError("Não é possível dividir por zero")
return x / y

Salvamos o arquivo como operacoes.py e agora vamos criar nosso arquivo de testes test_operacoes.py:

import unittest
import operacoes
class TestCalc(unittest.TestCase):
def test_adicao(self):
self.assertEqual(calc.add(10, 5), 15)
self.assertEqual(calc.add(-1, 1), 0)
self.assertEqual(calc.add(-1, -1), -2)
def test_subtracao(self):
self.assertEqual(calc.subtract(10, 5), 5)
self.assertEqual(calc.subtract(-1, 1), -2)
self.assertEqual(calc.subtract(-1, -1), 0)
def test_multiplicacao(self):
self.assertEqual(calc.multiply(10, 5), 50)
self.assertEqual(calc.multiply(-1, 1), -1)
self.assertEqual(calc.multiply(-1, -1), 1)
def test_divisao(self):
self.assertEqual(calc.divide(10, 5), 2)
self.assertEqual(calc.divide(-1, 1), -1)
self.assertEqual(calc.divide(-1, -1), 1)
self.assertEqual(calc.divide(5, 2), 2.5)
with self.assertRaises(ValueError): # Testando exceções -> ValueError
calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()

Você deve ter observado que tivemos que importar a biblioteca unittest em nosso script, ela é muito importante pois permite testar cada função de uma forma que atribuamos os resultados esperados, caso os testes venham a falhar, sabemos que há algo de errado com a nossa função.

Vamos agora executar o teste através do comando python -m unittest test_operacoes.py.

O resultado nos traz que os 4 testes foram executados com sucesso:

Ran 4 tests in 0.001s
OK

Além da biblioteca unittest também existem diversas outras, inclusive com funcionalidades mais avançadas, podemos citar por exemplo a biblioteca pytest.

Assertions como Programação Defensiva

  • Assertions não permitem o programador controlar a resposta para condições inesperadas
  • Tenha certeza que a execução páre quando uma condição esperada não é alcançada
  • Tipicamente usado para checar inputs a funções, porém pode ser usado em qualquer lugar
  • Pode ser usado para checar outputs de uma função para evitar propagação de valores ruins
  • Pode tornar mais fácil a localização da fonte de um bug

Onde usar Assertions?

  • Objetivo é encontrar bugs logo que introduzidos e tornar claro onde eles aconteceram
  • Use como um suplemento para testes
  • raise exceções caso usuários enviem dados de input ruins
  • Use assertions para: - checar tipo de argumentos ou valores - checar que invariantes em estruturas de dados são encontrados - checar definições em valores retornados - checar por violações de definições em um procedimento (exemplo: sem duplicatas em uma lista)

Vimos que existem muitos erros e possibilidades deles ocorrerem em nosso programas, por isso é importante que tenhamos em mente a capacidade que Python nos traz de lidarmos com estes erros e além disso é essencial que tenhamos em mente a necessidade de testarmos nossos programas para termos sempre a melhor qualidade possível de software.

Para mais detalhes esteja atento na documentação!