Verificar primeiro versus tratamento de exceções?

84

Estou trabalhando no livro "Head First Python" (este é meu idioma para aprender este ano) e Eu cheguei a uma seção onde eles discutem sobre duas técnicas de código:
Verificando o manuseio de Primeira contra Exceção.

Aqui está uma amostra do código Python:

# Checking First
for eachLine in open("../../data/sketch.txt"):
    if eachLine.find(":") != -1:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

# Exception handling        
for eachLine in open("../../data/sketch.txt"):
    try:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    except:
        pass

O primeiro exemplo lida diretamente com um problema na função .split . O segundo apenas permite que o manipulador de exceção lide com ele (e ignora o problema).

Eles argumentam no livro para usar o tratamento de exceções em vez de verificar primeiro. O argumento é que o código de exceção detectará todos os erros, em que a verificação primeiro trará apenas as coisas em que você pensa (e você perderá os casos). Eu fui ensinado a checar primeiro, então meu instinto inicial era fazer isso, mas a idéia deles é interessante. Eu nunca tinha pensado em usar o tratamento de exceções para lidar com casos.

Qual das duas é geralmente considerada a melhor prática?

    
por jmq 11.03.2012 / 03:53
fonte

9 respostas

64

No .NET, é uma prática comum evitar o uso excessivo de exceções. Um argumento é o desempenho: no .NET, lançar uma exceção é computacionalmente caro.

Outro motivo para evitar seu uso excessivo é que pode ser muito difícil ler códigos que dependam muito deles. A entrada no blog do Joel Spolsky faz um bom trabalho ao descrever o assunto.

No centro do argumento está a seguinte citação:

The reasoning is that I consider exceptions to be no better than "goto's", considered harmful since the 1960s, in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto's:

1. They are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn't reveal potential bugs.

2. They create too many possible exit points for a function. To write correct code, you really have to think about every possible code path through your function. Every time you call a function that can raise an exception and don't catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn't think about.

Pessoalmente, eu faço exceções quando meu código não consegue fazer o que é contratado. Eu costumo usar try / catch quando estou prestes a lidar com algo fora do meu limite de processo, por exemplo, uma chamada SOAP, uma chamada de banco de dados, arquivo IO ou uma chamada do sistema. Caso contrário, tento codificar defensivamente. Não é uma regra difícil e rápida, mas é uma prática geral.

Scott Hanselman também escreve sobre exceções no .NET aqui . Neste artigo ele descreve várias regras práticas relacionadas a exceções. Meu favorito?

You shouldn't throw exceptions for things that happen all the time. Then they'd be "ordinaries".

    
por 11.03.2012 / 04:42
fonte
76

Em Python em particular, geralmente é considerado uma prática melhor capturar a exceção. Ele tende a ser chamado de Mais fácil pedir perdão do que a permissão (EAFP), em comparação com o Look Before You Leap (LBYL). Há casos em que a LBYL dará a você bugs sutis em alguns casos.

No entanto, tenha cuidado com as instruções except: nuas , bem como com as declarações sem autorização. , já que eles também podem mascarar bugs - algo assim seria melhor:

for eachLine in open("../../data/sketch.txt"):
    try:
        role, lineSpoken = eachLine.split(":",1)
    except ValueError:
        pass
    else:
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    
por 11.03.2012 / 07:06
fonte
25

Uma abordagem pragmática

Você deve estar na defensiva, mas em um ponto. Você deve escrever manipulação de exceção, mas em um ponto. Vou usar a programação da web como um exemplo, porque é onde eu moro.

  1. Suponha que todas as entradas do usuário sejam incorretas e escreva defensivamente apenas no ponto de verificação do tipo de dados, verificações de padrão e injeção maliciosa. A programação defensiva deve ser algo que pode acontecer com muita frequência e que você não pode controlar.
  2. Grave o tratamento de exceções para serviços em rede que podem falhar, às vezes, e manipular normalmente o feedback do usuário. A programação de exceção deve ser usada para coisas em rede que podem falhar de tempos em tempos, mas geralmente são sólidas e você precisa manter seu programa funcionando.
  3. Não se preocupe em escrever defensivamente em seu aplicativo depois que os dados de entrada tiverem sido validados. É um desperdício de tempo e incha seu aplicativo. Deixe explodir porque ou é algo muito raro que não vale a pena ser manuseado ou significa que você precisa olhar os passos 1 e 2 com mais cuidado.
  4. Nunca escreva o tratamento de exceções em seu código principal que não seja dependente de um dispositivo em rede. Fazer isso é uma programação ruim e cara para o desempenho. Por exemplo, escrever um try-catch em caso de array fora dos limites em um loop significa que você não programou o loop corretamente em primeiro lugar.
  5. Deixe tudo ser tratado pelo registro de erros central, que captura exceções em um lugar depois de seguir os procedimentos acima. Você não pode capturar todos os casos de borda, pois isso pode ser infinito, você só precisa escrever um código que lide com a operação esperada. É por isso que você usa o tratamento central de erros como último recurso.
  6. O TDD é legal porque de certa forma é um desafio para você sem inchaço, o que significa dar a você alguma garantia de operação normal.
  7. Pontos de bônus é usar uma ferramenta de cobertura de código por exemplo, Istambul é boa para o nó, já que isso mostra onde você não está testando.
  8. A ressalva de tudo isso são exceções compatíveis com o desenvolvedor . Por exemplo, uma linguagem seria lançada se você usasse a sintaxe errada e explicasse o motivo. O mesmo deve acontecer com as bibliotecas utility das quais depende a maior parte do seu código.

Isto é da experiência de trabalhar em grandes cenários de equipe.

Analogy

Imagine se você usasse um traje espacial dentro da ISS TODO o tempo. Seria difícil ir ao banheiro ou comer. Seria super volumoso dentro do módulo espacial se movimentar. Seria uma droga. Escrever um monte de try-catches dentro do seu código é mais ou menos assim. Você tem que ter algum ponto onde você diz, ei eu assegurei que a ISS e meus astronautas dentro estão OK, então não é prático usar um traje espacial para cada cenário que poderia acontecer.

    
por 11.03.2012 / 06:22
fonte
14

O argumento principal do livro é que a versão de exceção do código é melhor porque ele detectará qualquer coisa que você possa ter ignorado se você tentou escrever sua própria verificação de erros.

Acho que essa afirmação é verdadeira apenas em circunstâncias muito específicas - onde você não se importa se a saída está correta.

Não há dúvida de que as exceções aumento são uma prática segura e saudável. Você deve fazer isso sempre que sentir que há algo no estado atual do programa com o qual você (como desenvolvedor) não pode ou não quer lidar.

Seu exemplo, no entanto, é sobre exceções pegar . Se você pegar uma exceção, não está se protegendo de situações que talvez tenha ignorado. Você está fazendo exatamente o oposto: assume que não ignorou nenhum cenário que possa ter causado esse tipo de exceção e, portanto, está confiante de que está tudo certo capturá-lo (e, assim, impedir que o programa saia, como qualquer exceção não detectada).

Usando a abordagem de exceção, se você vir ValueError exception, pula uma linha. Usando a abordagem tradicional sem exceção, você conta o número de valores retornados de split e, se for menor que 2, pula uma linha. Se você se sentir mais seguro com a abordagem de exceção, já que você pode ter esquecido algumas outras situações de "erro" em sua verificação de erro tradicional, e except ValueError as pegaria para você?

Isso depende da natureza do seu programa.

Se você estiver escrevendo, por exemplo, um navegador da Web ou um player de vídeo, um problema com as entradas não deverá causar falha em uma exceção não identificada. É muito melhor produzir algo remotamente sensato (mesmo que, estritamente falando, incorreto) do que sair.

Se você estiver escrevendo um aplicativo em que questões corretas sejam importantes (como software comercial ou de engenharia), essa seria uma abordagem terrível. Se você esqueceu de algum cenário que aumenta ValueError , a pior coisa que você pode fazer é silenciosamente ignorar esse cenário desconhecido e simplesmente ignorar a linha. É assim que bugs muito sutis e caros acabam no software.

Você pode pensar que a única maneira de ver ValueError neste código é se split retornou apenas um valor (em vez de dois). Mas e se sua instrução print mais tarde começar a usar uma expressão que aumente ValueError sob algumas condições? Isso fará com que você pule algumas linhas não porque elas perdem : , mas porque print falha nelas. Este é um exemplo de um bug sutil que eu estava me referindo anteriormente - você não notaria nada, apenas perderia algumas linhas.

Minha recomendação é evitar pegar (mas não aumentar!) exceções no código em que produzir saída incorreta é pior do que sair. A única vez que eu pegaria uma exceção em tal código é quando eu tenho uma expressão verdadeiramente trivial, então eu posso facilmente raciocinar o que pode causar cada um dos tipos de exceção possíveis.

Quanto ao impacto no desempenho do uso de exceções, ele é trivial (em Python), a menos que exceções sejam encontradas com frequência.

Se você usar exceções para lidar com condições de ocorrência rotineira, poderá, em alguns casos, pagar um enorme custo de desempenho. Por exemplo, suponha que você execute remotamente algum comando. Você pode verificar se o texto do seu comando passa pelo menos a validação mínima (por exemplo, sintaxe). Ou você poderia esperar por uma exceção a ser levantada (o que acontece somente depois que o servidor remoto analisa seu comando e encontra um problema com ele). Obviamente, o primeiro é de magnitude mais rápida. Outro exemplo simples: você pode verificar se um número é zero ~ 10 vezes mais rápido do que tentar executar a divisão e, em seguida, capturar a exceção ZeroDivisionError.

Essas considerações só importam se você envia frequentemente cadeias de comandos malformadas para servidores remotos ou recebe argumentos de valor zero que você usa para divisão.

Nota: Suponho que você usaria except ValueError em vez de apenas except ; como outros apontaram, e como o próprio livro diz em poucas páginas, você nunca deve usar except nua.

Outra observação: a abordagem adequada sem exceção é contar o número de valores retornados por split , em vez de pesquisar : . O último é muito lento, pois repete o trabalho feito por split e pode quase dobrar o tempo de execução.

    
por 23.04.2013 / 02:54
fonte
6

Como regra geral, se você souber que uma instrução pode gerar um resultado inválido, teste-a e lide com ela. Use exceções para coisas que você não espera; coisas que são "excepcionais". Isso torna o código mais claro em um sentido contratual ("não deve ser nulo" como um exemplo).

    
por 11.03.2012 / 08:58
fonte
2

Use o que já funciona bem em ...

  • sua linguagem de programação escolhida em termos de legibilidade e eficiência de código
  • sua equipe e o conjunto de convenções de código acordadas

Tanto a manipulação de exceção quanto a programação defensiva são formas diferentes de expressar a mesma intenção.

    
por 11.03.2012 / 16:39
fonte
0

TBH, não importa se você usa a verificação try/except mechanic ou if . Você geralmente vê o EAFP e o LBYL na maioria das linhas de base do Python, com o EAFP sendo um pouco mais comum. Às vezes o EAFP é muito mais legível / idiomático, mas neste caso em particular eu acho que está bem de qualquer forma.

No entanto ...

Eu tomaria cuidado ao usar sua referência atual. Alguns problemas gritantes com o código deles:

  1. O descritor de arquivo é vazado. As versões modernas do CPython (um interpretador específico Python) na verdade o fecham, já que é um objeto anônimo que está apenas no escopo durante o loop (gc nuke após o loop). No entanto, outros intérpretes não têm essa garantia. Eles podem vazar o descritor diretamente. Você quase sempre quer usar o with idiom ao ler arquivos no Python: existem pouquíssimas exceções. Este não é um deles.
  2. O tratamento de exceções de Pokemon é desaprovado , uma vez que mascara os erros (ou seja, a declaração bare except que não captura uma exceção específica)
  3. Nit: Você não precisa de parênteses para a remoção de uma tupla. Pode apenas fazer role, lineSpoken = eachLine.split(":",1)

O Ivc tem uma boa resposta sobre isso e EAFP, mas também está vazando o descritor.

A versão LBYL não é necessariamente tão eficiente quanto a versão EAFP, portanto, dizer que lançar exceções é "caro em termos de desempenho" é categoricamente falso. Isso realmente depende do tipo de string que você está processando:

In [33]: def lbyl(lines):
    ...:     for line in lines:
    ...:         if line.find(":") != -1:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = line.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:

In [34]: def eafp(lines):
    ...:     for line in lines:
    ...:         try:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = eachLine.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:         except:
    ...:             pass
    ...:

In [35]: lines = ["abc:def", "onetwothree", "xyz:hij"]

In [36]: %timeit lbyl(lines)
100000 loops, best of 3: 1.96 µs per loop

In [37]: %timeit eafp(lines)
100000 loops, best of 3: 4.02 µs per loop

In [38]: lines = ["a"*100000 + ":" + "b", "onetwothree", "abconetwothree"*100]

In [39]: %timeit lbyl(lines)
10000 loops, best of 3: 119 µs per loop

In [40]: %timeit eafp(lines)
100000 loops, best of 3: 4.2 µs per loop
    
por 29.10.2018 / 22:48
fonte
-4

Basicamente, tratamento de exceções deve ser mais apropriado para idiomas OOP.

O segundo ponto é o desempenho, porque você não precisa executar eachLine.find para cada linha.

    
por 11.03.2012 / 04:29
fonte
-6

Eu acho que a programação defensiva prejudica o desempenho. Você também deve pegar apenas as exceções que você vai tratar, deixe o tempo de execução lidar com a exceção que você não sabe como lidar.

    
por 11.03.2012 / 05:37
fonte