Quando e como devo usar exceções?

20

A configuração

Costumo ter problemas para determinar quando e como usar exceções. Vamos considerar um exemplo simples: suponha que eu esteja copiando uma página da Web, diga " http://www.abevigoda.com/ ", para determinar se Abe Vigoda ainda está vivo. Para fazer isso, basta baixar a página e procurar os horários em que a frase "Abe Vigoda" aparece. Retornamos a primeira aparição, pois isso inclui o status de Abe. Conceitualmente, ficará assim:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Onde parse_abe_status(s)pega uma sequência da forma "Abe Vigoda é alguma coisa " e retorna a parte " alguma coisa ".

Antes de argumentar que existem maneiras muito melhores e mais robustas de classificar esta página para o status de Abe, lembre-se de que este é apenas um exemplo simples e artificial usado para destacar uma situação comum em que estou.

Agora, onde esse código pode encontrar problemas? Entre outros erros, alguns "esperados" são:

  • download_pagepode não conseguir baixar a página e lança um IOError.
  • O URL pode não apontar para a página correta, ou a página é baixada incorretamente e, portanto, não há ocorrências. hitsé a lista vazia, então.
  • A página da web foi alterada, possivelmente fazendo com que nossas suposições sobre a página estivessem erradas. Talvez esperemos 4 menções a Abe Vigoda, mas agora encontramos 5.
  • Por alguns motivos, hits[0]pode não ser uma sequência do formato "Abe Vigoda é alguma coisa " e, portanto, não pode ser analisada corretamente.

O primeiro caso não é realmente um problema para mim: um IOErroré lançado e pode ser tratado pelo chamador da minha função. Então, vamos considerar os outros casos e como eu posso lidar com eles. Mas primeiro, vamos supor que implementamos parse_abe_statusda maneira mais estúpida possível:

def parse_abe_status(s):
    return s[13:]

Ou seja, ele não realiza nenhuma verificação de erro. Agora, para as opções:

Opção 1: Retorno None

Posso dizer ao chamador que algo deu errado ao retornar None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Se o interlocutor receber Noneda minha função, ele deve assumir que não houve menções a Abe Vigoda e, portanto, algo deu errado. Mas isso é bastante vago, certo? E isso não ajuda no caso em hits[0]que não é o que pensávamos.

Por outro lado, podemos colocar em algumas exceções:

Opção 2: Usando exceções

Se hitsestiver vazio, um IndexErrorserá lançado quando tentarmos hits[0]. Mas não se deve esperar que o responsável pela chamada lide com uma IndexErrorfunção lançada por minha função, pois ele não tem ideia de onde isso IndexErrorveio; poderia ter sido jogado por find_all_mentions, pelo que ele sabe. Portanto, criaremos uma classe de exceção personalizada para lidar com isso:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Agora, e se a página for alterada e houver um número inesperado de ocorrências? Isso não é catastrófico, pois o código ainda pode funcionar, mas um chamador pode querer ter um cuidado extra ou registrar um aviso. Então, eu vou lançar um aviso:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Por fim, podemos descobrir que statusnão está vivo ou morto. Talvez, por alguma estranha razão, hoje tenha sido comatose. Então não quero voltar False, pois isso implica que Abe está morto. O que devo fazer aqui? Lance uma exceção, provavelmente. Mas que tipo? Devo criar uma classe de exceção personalizada?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Opção 3: em algum lugar no meio

Eu acho que o segundo método, com exceções, é preferível, mas não tenho certeza se estou usando exceções corretamente nele. Estou curioso para ver como os programadores mais experientes lidariam com isso.

jme
fonte

Respostas:

17

A recomendação no Python é usar exceções para indicar falha. Isso é verdade mesmo que você espere falhas regularmente.

Veja da perspectiva do chamador do seu código:

my_status = get_abe_status(my_url)

E se retornarmos Nenhum? Se o chamador não tratar especificamente do caso em que get_abe_status falhou, ele simplesmente tentará continuar com my_stats como None. Isso pode produzir um erro difícil de diagnosticar mais tarde. Mesmo se você verificar None, esse código não tem idéia do motivo pelo qual get_abe_status () falhou.

Mas e se levantarmos uma exceção? Se o chamador não tratar especificamente do caso, a exceção será propagada para cima, eventualmente atingindo o manipulador de exceções padrão. Pode não ser o que você deseja, mas é melhor introduzir um bug sutil em outro lugar do programa. Além disso, a exceção fornece informações sobre o que deu errado e que é perdido na primeira versão.

Do ponto de vista do chamador, é simplesmente mais conveniente obter uma exceção do que um valor de retorno. E esse é o estilo python, para usar exceções para indicar condições de falha e não retornar valores.

Alguns adotam uma perspectiva diferente e argumentam que você só deve usar exceções para casos que você nunca espera que aconteçam. Eles argumentam que normalmente correr não deve gerar exceções. Uma razão dada para isso é que as exceções são grosseiramente ineficientes, mas isso não é verdade para o Python.

Alguns pontos no seu código:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Essa é uma maneira realmente confusa de procurar uma lista vazia. Não induza uma exceção apenas para verificar alguma coisa. Use um if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Você percebe que a linha logger.warning nunca funcionará certo?

Winston Ewert
fonte
1
Obrigado (tardiamente) pela sua resposta. Além de examinar o código publicado, ele melhorou minha percepção de quando e como gerar uma exceção.
JME
4

A resposta aceita merece ser aceita e responde à pergunta. Escrevo isso apenas para fornecer um pouco de experiência extra.

Um dos credos de Python é: é mais fácil pedir perdão do que permissão. Isso significa que normalmente você apenas faz as coisas e, se espera exceções, as trata. Em vez de fazer os testes antes da mão para garantir que você não receberá uma exceção.

Quero fornecer um exemplo para mostrar o quão dramática é a diferença de mentalidade em C ++ / Java. Um loop for em C ++ normalmente se parece com:

for(int i = 0; i != myvector.size(); ++i) ...

Uma maneira de pensar sobre isso: acessar myvector[k]onde k> = myvector.size () causará uma exceção. Assim, você poderia, em princípio, escrever isso (de maneira muito embaraçosa) como uma tentativa de captura.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Ou algo parecido. Agora, considere o que está acontecendo em um loop python for:

for i in range(1):
    ...

Como isso está funcionando? O loop for pega o resultado do intervalo (1) e chama iter () nele, pegando um iterador nele.

b = range(1).__iter__()

Em seguida, ele chama em seguida a cada iteração do loop, até ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Em outras palavras, um loop for em python é na verdade uma tentativa, exceto disfarçado.

Quanto à questão concreta, lembre-se de que as exceções interrompem a execução normal da função e devem ser tratadas separadamente. No Python, você deve jogá-los livremente sempre que não houver razão para executar o restante do código em sua função e / ou nenhum dos retornos refletir corretamente o que aconteceu na função. Observe que retornar cedo de uma função é diferente: retornar cedo significa que você já descobriu a resposta e não precisa do restante do código para descobrir a resposta. Estou dizendo que exceções devem ser lançadas quando a resposta não é conhecida e o restante do código para determinar a resposta não pode ser razoavelmente executado. Agora, "refletir corretamente", como quais exceções você escolhe lançar, é tudo uma questão de documentação.

No caso de seu código específico, eu diria que qualquer situação que faça com que os hits sejam uma lista vazia deve ser lançada. Por quê? Bem, da maneira como sua função é configurada, não há como determinar a resposta sem analisar os hits. Portanto, se os hits não são analisáveis, porque o URL está ruim ou porque os hits estão vazios, a função não pode responder à pergunta e, de fato, nem mesmo pode tentar.

Nesse caso em particular, eu argumentaria que, mesmo que você consiga analisar e não obtenha uma resposta razoável (viva ou morta), ainda deve jogar. Por quê? Porque, a função retorna um booleano. Retorno Nenhum é muito perigoso para o seu cliente. Se eles fizerem uma verificação se Não, não haverá falha, ela será silenciosamente tratada como Falso. Portanto, seu cliente basicamente sempre terá que fazer uma verificação de Se é Nenhum, de qualquer forma, se ele não quiser falhas silenciosas ... então você provavelmente deve apenas jogar.

Nir Friedman
fonte
2

Você deve usar exceções quando algo excepcional ocorrer. Ou seja, algo que não deve ocorrer devido ao uso adequado do aplicativo. Se for permitido e esperado que o consumidor do seu método procure por algo que não será encontrado, "não encontrado" não é um caso excepcional. Nesse caso, você deve retornar nulo ou "Nenhum" ou {}, ou algo que indique um conjunto de retorno vazio.

Se, por outro lado, você realmente espera que os consumidores do seu método sempre encontrem o que está sendo pesquisado (a não ser que eles tenham estragado de alguma maneira), não descobrir que seria uma exceção e você deve seguir com isso.

A chave é que o tratamento de exceções pode ser caro - as exceções devem coletar informações sobre o estado do seu aplicativo quando elas ocorrerem, como um rastreamento de pilha, para ajudar as pessoas a decifrar por que elas ocorreram. Eu não acho que é isso que você está tentando fazer.

Matthew Flynn
fonte
1
Se você decidir que não é permitido encontrar um valor, tenha cuidado com o que usar para indicar que foi o que aconteceu. Se seu método deve retornar Stringae você escolher "Nenhum" como seu indicador, isso significa que você deve ter cuidado para que "Nenhum" nunca seja um valor válido. Observe também que há uma diferença entre olhar os dados e não encontrar um valor e não conseguir recuperá-los, portanto, não podemos encontrá-los. Ter o mesmo resultado para esses dois casos significa que você não tem visibilidade quando não obtém valor quando espera que exista um.
Unholysampler
Os blocos de código embutido são marcados com reticências (`), talvez seja isso que você pretendia fazer com o" Nenhum "?
amigos estão dizendo sobre Izkata
3
Receio que isso seja absolutamente falso em Python. Você está aplicando o raciocínio no estilo C ++ / Java para outro idioma. Python usa exceções para indicar o final de um loop for; isso é bastante excepcional.
Nir Friedman
2

Se eu estivesse escrevendo uma função

 def abe_is_alive():

Eu o escreveria return Trueou Falsenos casos em que tenho absoluta certeza de um ou de outro, e raiseum erro em qualquer outro caso (por exemplo raise ValueError("Status neither 'dead' nor 'alive'")). Isso ocorre porque a função que chama minha está esperando um valor booleano, e se eu não puder fornecer isso com certeza, o fluxo regular do programa não deve continuar.

Algo como o seu exemplo de obter um número diferente de "hits" do que o esperado, eu provavelmente ignoraria; desde que um dos hits ainda corresponda ao meu padrão "Abe Vigoda está {morto | vivo}", tudo bem. Isso permite que a página seja reorganizada, mas ainda obtém as informações apropriadas.

Ao invés de

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Eu verificaria explicitamente:

if not hits:
    raise NotFoundError

como isso tende a ser "mais barato" do que configurar o try.

Eu concordo com você IOError; Também não tentaria lidar com o erro ao conectar-me ao site - se não pudermos, por algum motivo, este não for o local apropriado para lidar com ele (pois não nos ajuda a responder à nossa pergunta) e deve passar para a função de chamada.

jonrsharpe
fonte