Verificar primeiro vs tratamento de exceção?

88

Estou trabalhando no livro "Head First Python" (é minha língua para aprender este ano) e cheguei a uma seção em que eles discutem sobre duas técnicas de código:
Verificando o tratamento First vs Exception.

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 .splitfunção. O segundo apenas permite que o manipulador de exceções 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 capturará todos os erros, onde a verificação primeiro capturará apenas o que você pensa (e você perde os casos extremos). Fui ensinado a verificar primeiro, então meu instinto inicial era fazer isso, mas a ideia deles é interessante. Eu nunca pensei em usar o tratamento de exceções para lidar com casos.

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

jmq
fonte
12
Essa seção do livro não é inteligente. Se você estiver em um loop e estiver lançando exceções repetidamente, é muito caro. Tentei descrever alguns pontos positivos de quando fazer isso.
Jason Sebring
9
Apenas não caia na armadilha "verificação de arquivo existe". O arquivo existe! = Tem acesso ao arquivo ou ele existirá nos 10 ms necessários para acessar minha chamada de arquivo etc. blogs.msdn.com/b/jaredpar/archive/2009/04/27/…
Billy ONeal
11
As exceções são pensadas de maneira diferente no Python e em outros idiomas. Por exemplo, a maneira de iterar através de uma coleção é chamar .next () até gerar uma exceção.
WuHoUnited
4
@ emeraldcode.com Isso não é inteiramente verdade sobre o Python. Não sei as especificidades, mas a linguagem foi construída em torno desse paradigma; portanto, lançar exceções não é tão caro quanto em outras línguas.
Izkata
Dito isto, para este exemplo, eu usaria uma declaração de guarda: if -1 == eachLine.find(":"): continueo restante do loop também não seria recuado.
Izkata 12/03/12

Respostas:

68

No .NET, é 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 o uso excessivo é que pode ser muito difícil ler código que depende muito deles. A entrada de Joel Spolsky no blog faz um bom trabalho ao descrever o problema.

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

O raciocínio é que considero as exceções não melhores que as "goto's", consideradas prejudiciais desde a década de 1960, na medida em que criam um salto abrupto de um ponto do código para outro. Na verdade, eles são significativamente piores que os de Goto:

1. Eles são invisíveis no código fonte . Observando um bloco de código, incluindo funções que podem ou não gerar exceções, não há como ver quais exceções podem ser lançadas e de onde. Isso significa que mesmo uma inspeção cuidadosa do código não revela possíveis erros.

2. Eles criam muitos pontos de saída possíveis para uma função. Para escrever o código correto, você realmente precisa pensar em todos os caminhos de código possíveis através de sua função. Toda vez que você chama uma função que pode gerar uma exceção e não capturá-la no local, cria oportunidades para erros surpreendentes causados ​​por funções que foram encerradas abruptamente, deixando os dados em um estado inconsistente ou outros caminhos de código que você não identificou pense sobre.

Pessoalmente, lanço exceções quando meu código não pode fazer o que é contratado. Costumo usar try / catch quando estou prestes a lidar com algo fora dos limites do meu processo, por exemplo, uma chamada SOAP, uma chamada de banco de dados, E / S de arquivo ou uma chamada de 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 básicas sobre exceções. Meu favorito?

Você não deve lançar exceções para coisas que acontecem o tempo todo. Então eles seriam "ordinários".

Kyle Hodgson
fonte
5
aqui está outro ponto: se o log de exceções estiver ativado em todo o aplicativo, é melhor usar a exceção apenas para condições excepcionais, não para ordinários. Caso contrário, o log ficará confuso e os motivos reais que causam erros serão obscurecidos.
Rwong 11/03/12
2
Boa resposta. Observe que as exceções têm um impacto de alto desempenho na maioria das plataformas. No entanto, como você observou com meus comentários sobre outras respostas, o desempenho não é considerado no caso de decidir uma regra geral sobre como codificar algo.
mattnz
1
A citação de Scott Hanselman descreve melhor a atitude .Net em relação às exceções do que o "uso excessivo". O desempenho é frequentemente mencionado, mas o argumento real é o inverso do motivo pelo qual você DEVE usar exceções - torna o código mais difícil de entender e lidar quando uma condição comum resulta em uma exceção. Quanto a Joel, o ponto 1 é realmente positivo (invisível significa que o código mostra o que faz, não o que não mostra) e o ponto 2 é irrelevante (você já está em um estado inconsistente ou não deve haver uma exceção) . Ainda assim, +1 em "não pode fazer o que foi solicitado".
jmoreno
5
Embora esta resposta seja adequada para .Net, não é muito pitônica , portanto, como essa é uma pergunta python, não vejo por que a resposta da Ivc não foi votada mais.
Mark Booth
2
@IanGoldby: não. O tratamento de exceção é realmente melhor descrito como recuperação de exceção. Se você não conseguir se recuperar de uma exceção, provavelmente não deverá ter nenhum código de tratamento de exceções. Se o método A chamar o método B, que chama C e L, C provavelmente, A ou B deve se recuperar, não os dois. A decisão "se eu não puder executar o X, executarei o Y" deve ser evitada se Y exigir que outra pessoa conclua a tarefa. Se você não conseguir concluir a tarefa, tudo o que resta é limpeza e registro. A limpeza em .net deve ser automática, o log deve ser centralizado.
jmoreno
78

Em Python, em particular, geralmente é considerado uma boa prática capturar a exceção. Tende a ser chamado de mais fácil pedir perdão do que permissão (EAFP), em comparação com o Look Before You Leap (LBYL). Há casos em que o LBYL fornece erros sutis em alguns casos.

No entanto, tenha cuidado com as except:instruções simples e com o overbroad, exceto as instruções, pois elas também podem mascarar erros - 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())
lvc
fonte
8
Como programador .NET, eu me encolho nisso. Mas, novamente, vocês fazem tudo estranho. :)
Phil
Isso é excepcionalmente frustrante (trocadilho não intencional) quando as APIs não são consistentes sobre quais exceções são lançadas sob quais circunstâncias ou quando vários tipos diferentes de falhas são lançados no mesmo tipo de exceção.
31415 Jack
Então você acaba usando o mesmo mecanismo para erros inesperados e tipo de retorno esperado. Isso é tão bom quanto usar 0 como um número, um falso booleano E um ponteiro inválido que encerrará seu processo com um código de saída 128 + SIGSEGV, porque, como conveniente, você não precisa de coisas diferentes agora. Como o spork! Ou sapatos com dedos ...
yeoman
2
@yeoman quando lançar uma exceção é uma pergunta diferente, esta é sobre usar try/ em exceptvez de configurar um condicional para "é o seguinte que provavelmente lançará uma exceção", e a prática do Python definitivamente prefere a anterior. Não prejudica que essa abordagem seja (provavelmente) mais eficiente aqui, pois, no caso em que a divisão é bem-sucedida, você apenas caminha a corda uma vez. Quanto a se splitdeve lançar uma exceção aqui, eu diria que definitivamente deveria - uma regra comum é que você deve lançar quando não puder fazer o que seu nome diz e não puder se dividir em um delimitador ausente.
Lvc
Não acho ruim, lento ou terrível, especialmente porque apenas uma exceção específica é detectada. Resp Eu realmente gosto de Python. É engraçado como às vezes não mostra bom gosto, como disse o uso C do número zero, o Spork e os sapatos favoritos de todos os tempos de Randall Munroe com dedos :) Além disso, quando estou em Python e uma API diz que isso é a maneira de fazê-lo, eu irei em frente :) Verificar as condições com antecedência nunca é uma boa idéia, devido à concorrência, corotinas ou uma daquelas que estão sendo adicionadas mais adiante ...
yeoman
27

Uma abordagem pragmática

Você deve estar na defensiva, mas até certo ponto. Você deve escrever o tratamento de exceções, mas até certo ponto. Vou usar a programação da Web como exemplo, porque é onde moro.

  1. Suponha que todas as entradas do usuário sejam ruins e grave defensivamente apenas no ponto de verificação do tipo de dados, verificação de padrões e injeção maliciosa. A programação defensiva deve ser algo que potencialmente pode acontecer com muita frequência que você não pode controlar.
  2. Escreva tratamento de exceção para serviços de rede que podem falhar às vezes e manipule normalmente para obter 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 após a validação dos dados de entrada. É uma perda de tempo e incha seu aplicativo. Deixe explodir porque é algo muito raro que não vale a pena manusear ou significa que você precisa examinar as etapas 1 e 2 com mais cuidado.
  4. Nunca escreva o tratamento de exceções no código principal que não depende de um dispositivo em rede. Fazer isso é uma programação ruim e onerosa para o desempenho. Por exemplo, escrever um try-catch em caso de uma matriz fora dos limites em um loop significa que você não programou o loop corretamente em primeiro lugar.
  5. Deixe tudo ser tratado pelo log de erros central que captura exceções em um só lugar após seguir os procedimentos acima. Você não pode capturar todos os casos extremos, 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 é bom porque, de certa forma, é atraente para você sem inchaço, o que significa dar a você alguma garantia de operação normal.
  7. O bônus é usar uma ferramenta de cobertura de código, por exemplo, Istambul é boa para o nó, pois isso mostra onde você não está testando.
  8. A ressalva de tudo isso é a exceção do desenvolvedor . Por exemplo, um idioma seria lançado se você usasse a sintaxe errada e explicasse o porquê. O seu utilitário também deve bibliotecas das quais depende a maior parte do seu código.

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

Uma Analogia

Imagine se você usasse um traje espacial dentro da ISS o tempo todo. Seria difícil ir ao banheiro ou comer. Seria super volumoso dentro do módulo espacial se movimentar. Seria péssimo. Escrever várias tentativas dentro do seu código é assim. Você tem que ter um ponto em que diz: ei, eu garanti a ISS e meus astronautas estão bem, então não é prático usar um traje espacial para todos os cenários que possam acontecer.

Jason Sebring
fonte
4
O problema com o ponto 3 é que ele assume o programa e os programadores que trabalham nele são perfeitos. Eles não são, então é melhor programar defensivamente com isso em mente. Quantidades apropriadas na conjuntura principal podem tornar o software muito mais confiável do que a mentalidade "Se as entradas são verificadas, tudo está perfeito".
mattnz
é para isso que serve o teste.
Jason Sebring
3
Testar não é um problema. Ainda não vi uma suíte de testes com código 100% e cobertura "ambiental".
Marjan Venema
1
@emeraldcode: Você quer um emprego comigo? Eu adoraria ter alguém na equipe que sempre, com exceção, testa todas as permutações de todos os casos extremos que o software alguma vez executará. Deve ser bom saber com certeza abosoluite que seu código é perfeitamente testado.
mattnz
1
Aceita. Existem cenários em que tanto a programação defensiva quanto o tratamento de exceções funcionam bem e mal, e nós, programadores, devemos aprender a reconhecê-los e escolher a técnica que melhor se ajusta. Gosto do ponto 3 porque acredito que precisamos assumir, em um determinado nível do código, que algumas condições contextuais devem ser satisfeitas. Essas condições são satisfeitas pela codificação defensiva na camada externa do código, e acho que o tratamento de exceções é adequado quando essas suposições são quebradas na camada interna.
yaobin
15

O principal argumento do livro é que a versão de exceção do código é melhor, pois ela captura tudo o que você pode ter esquecido se tentar escrever sua própria verificação de erro.

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

Não há dúvida de que levantar exceções é uma prática sólida e segura. Você deve fazer isso sempre que achar que há algo no estado atual do programa com o qual você (como desenvolvedor) não pode ou não deseja lidar.

Seu exemplo, no entanto, é sobre a captura de exceções. Se você capturar uma exceção, não estará se protegendo de cenários que possa ter esquecido. 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 bem em capturá-la (e, assim, impedir que o programa saia, como qualquer exceção não capturada).

Usando a abordagem de exceção, se você vir a ValueErrorexceção, pule uma linha. Usando a abordagem tradicional de não exceção, você conta o número de valores retornados splite, se for menor que 2, pula uma linha. Você deve se sentir mais seguro com a abordagem de exceção, pois pode ter esquecido outras situações de "erro" em sua verificação de erro tradicional e as except ValueErrorcapturaria para você?

Isso depende da natureza do seu programa.

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

Se você estiver escrevendo um aplicativo em que a correção é importante (como software comercial ou de engenharia), essa seria uma abordagem terrível. Se você se esqueceu de algum cenário que levanta ValueError, a pior coisa a fazer é ignorar silenciosamente esse cenário desconhecido e simplesmente pular a linha. É assim que erros muito sutis e caros acabam no software.

Você pode pensar que a única maneira de ver ValueErrorneste código é se for splitretornado apenas um valor (em vez de dois). Mas e se a sua printdeclaração mais tarde começar a usar uma expressão que aumenta ValueErrorsob algumas condições? Isso fará com que você pule algumas linhas não porque elas falham :, mas porque printfalham nelas. Este é um exemplo de um bug sutil ao qual me referi anteriormente - você não notaria nada, apenas perderia algumas linhas.

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

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

Se você usar exceções para lidar com condições que ocorrem rotineiramente, em alguns casos poderá 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 na validação mínima (por exemplo, sintaxe). Ou você pode esperar que uma exceção seja levantada (o que ocorre apenas depois que o servidor remoto analisa seu comando e encontra um problema). Obviamente, o primeiro é ordens de magnitude mais rápidas. Outro exemplo simples: você pode verificar se um número é zero ~ 10 vezes mais rápido do que tentar executar a divisão e capturar a exceção ZeroDivisionError.

Essas considerações são importantes apenas se você enviar seqüências de comandos malformadas para servidores remotos ou receber argumentos de valor zero que você usa para divisão.

Nota: Presumo que você usaria em except ValueErrorvez do justo except; como outros apontaram, e como o próprio livro diz em algumas páginas, você nunca deve usar nada except.

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

max
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 exemplo).

Ian
fonte
2

Use o que sempre funciona bem ..

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

O tratamento de exceções e a programação defensiva são maneiras diferentes de expressar a mesma intenção.

Sri
fonte
0

TBH, não importa se você usa o try/exceptmecânico ou uma ifverificação de instrução. Você geralmente vê o EAFP e o LBYL na maioria das linhas de base do Python, sendo o EAFP um pouco mais comum. Às vezes, o EAFP é muito mais legível / idiomático, mas, nesse caso em particular, acho que é bom de qualquer maneira.

Contudo...

Seria cuidadoso ao usar sua referência atual. Alguns problemas flagrantes com seu código:

  1. O descritor de arquivo está vazado. As versões modernas do CPython (um intérprete específico do Python) na verdade o fecharão, já que é um objeto anônimo que só está no escopo durante o loop (o gc irá desativá-lo após o loop). No entanto, outros intérpretes não têm essa garantia. Eles podem vazar o descritor completamente. Você quase sempre deseja usar o withidioma ao ler arquivos no Python: existem muito poucas exceções. Este não é um deles.
  2. O manuseio de exceção de Pokémon é desaprovado , pois oculta erros (ou seja, uma exceptdeclaração simples que não captura uma exceção específica)
  3. Nit: Você não precisa de parênteses para desembalar a tupla. Pode apenas fazerrole, lineSpoken = eachLine.split(":",1)

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

A versão LBYL não é necessariamente tão eficiente quanto a versão do EAFP, pelo que dizer que lançar exceções é "caro em termos de desempenho" é categoricamente falso. Realmente depende do tipo de strings 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
Matt Messersmith
fonte
-4

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

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

Elalfer
fonte
7
-1: o desempenho é um motivo extremamente ruim para regras gerais.
mattnz
3
Não, as exceções não têm nenhuma relação com o POO.
Pubby
-6

Eu acho que a programação defensiva prejudica o desempenho. Você também deve capturar apenas as exceções a serem tratadas; deixe o tempo de execução lidar com a exceção que você não sabe manipular.

Manoj
fonte
7
No entanto, anotehr -1 por se preocupar com desempenho e legibilidade, facilidade de manutenção bla bla bla. O desempenho não é uma razão.
mattnz
Posso saber por que você está distribuindo -1s sem explicar? Programação defensiva significa mais linhas de código, o que significa pior desempenho. Alguém quer explicar antes de abater o placar?
Manoj
3
@Manoj: A menos que você tenha medido com um criador de perfil e encontrado um bloco de código inaceitavelmente lento, código de legibilidade e manutenção muito antes do desempenho.
Daenyth 11/03/12
O que o @Manoj disse com a adição de que menos código significa universalmente menos trabalho ao depurar e manter. O impacto no tempo do desenvolvedor de algo menos que o código perfeito é extremamente alto. Estou assumindo (como eu) que você não escreve um código perfeito, me perdoe se eu estiver errado.
mattnz
2
Obrigado pelo link - Interessante, leia que eu tenho que concordar com isso, até certo ponto ... Trabalhando em sistemas críticos para a vida, como eu faço "O sistema imprimiu o rastreamento da pilha, para que saibamos exatamente por que essas 300 pessoas morreram desnecessariamente. .... "realmente não vai cair muito bem no banco das testemunhas. Suponho que seja uma daquelas coisas em que toda situação tem uma resposta apropriada diferente.
mattnz