2.26. Lançar erros

Uma função pode sinalizar um problema ao seu chamador lançando uma exceção. A palavra-chave é raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Chamar square_root(-1) para na linha raise, sai da função e procura um except correspondente no chamador. Se nenhum chamador o capturar, o script termina com um traceback.

2.26.1. Porquê lançar em vez de devolver um sentinel

Duas formas de reportar «entrada inválida»:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

A forma com exceção é geralmente melhor:

  • O chamador tem de deliberadamente tratar o caso de erro – seja com um try, seja deixando a exceção propagar. Sentinels são fáceis de esquecer e fáceis de confundir com um resultado normal.

  • A mensagem de erro viaja com a exceção; a abordagem com sentinel tem de anexar o diagnóstico noutro local.

  • O comportamento predefinido numa exceção não tratada é uma falha evidente com um traceback que aponta para a chamada ofensora. Devoluções silenciosas de None tornam-se bugs subtis mais tarde.

Recorra a sentinels apenas quando «não encontrado» é um resultado rotineiro e não excecional – dict.get() devolve None para uma chave ausente precisamente porque se espera que as pesquisas falhem por vezes.

2.26.2. Classes de exceção personalizadas

Para lançar um problema que o chamador possa querer distinguir dos erros incorporados, defina uma subclasse de Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

O corpo class vazio está correto – o nome em si é o que importa, porque os chamadores capturam por classe. Agrupe erros relacionados sob uma base comum se um chamador quiser capturar toda a família num único bloco.

2.26.2.1. Relançar

Um raise simples dentro de um bloco except relança a exceção atual para que se propague para o próximo tratador:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

Esta é a forma correta quando uma função quer observar um erro (registá-lo, contá-lo, desfazer uma alteração parcial) sem realmente o tratar.

2.26.3. Quando capturar e quando propagar

Uma regra prática útil:

  • Capture uma exceção ao nível que pode recuperar de forma significativa – substituir por um valor predefinido, tentar novamente, ignorar a entrada inválida.

  • Deixe-a propagar quando não há nada útil a fazer exceto falhar, ou quando a camada acima é a que sabe como recuperar.

Uma função no meio de uma pilha de chamadas que engole todos os erros e devolve silenciosamente torna as falhas impossíveis de rastrear. Prefira deixar as exceções propagar até chegarem ao código que genuinamente tem um plano.