Melhor 'tentar' alguma coisa e capturar a exceção ou testar se é possível primeiro evitar uma exceção?

139

Devo testar ifalgo válido ou apenas tryfazê-lo e capturar a exceção?

  • Existe alguma documentação sólida dizendo que uma maneira é preferida?
  • É uma maneira mais pitônica ?

Por exemplo, devo:

if len(my_list) >= 4:
    x = my_list[3]
else:
    x = 'NO_ABC'

Ou:

try:
    x = my_list[3]
except IndexError:
    x = 'NO_ABC'

Algumas reflexões ...
PEP 20 diz:

Os erros nunca devem passar silenciosamente.
A menos que seja explicitamente silenciado.

O uso de um em tryvez de um deve ifser interpretado como um erro passando silenciosamente? E se sim, você o está silenciando explicitamente usando-o dessa maneira, tornando-o correto?


Não estou me referindo a situações em que você só pode fazer as coisas de uma maneira; por exemplo:

try:
    import foo
except ImportError:
    import baz
chown
fonte

Respostas:

161

Você deve preferir try/exceptsobre if/elsese esse resultado em

  • acelerações (por exemplo, impedindo pesquisas extras)
  • código mais limpo (menos linhas / mais fácil de ler)

Muitas vezes, eles andam de mãos dadas.


acelerações

No caso de tentar encontrar um elemento em uma longa lista por:

try:
    x = my_list[index]
except IndexError:
    x = 'NO_ABC'

a tentativa, exceto é a melhor opção quando indexprovavelmente está na lista e o IndexError geralmente não é gerado. Dessa forma, você evita a necessidade de uma pesquisa extra por if index < len(my_list).

O Python incentiva o uso de exceções, que você manipula é uma frase do Dive Into Python . Seu exemplo não apenas lida com a exceção (normalmente), em vez de deixá-la passar silenciosamente , também ocorre apenas no caso excepcional de índice não encontrado (daí a palavra exceção !).


código mais limpo

A documentação oficial do Python menciona o EAFP : Mais fácil pedir perdão do que permissão e Rob Knight observa que capturar erros em vez de evitá-los pode resultar em um código mais limpo e fácil de ler. Seu exemplo diz assim:

Pior (LBYL 'olhe antes de pular') :

#check whether int conversion will raise an error
if not isinstance(s, str) or not s.isdigit():
    return None
elif len(s) > 10:    #too many digits for int conversion
    return None
else:
    return int(s)

Melhor (EAFP: Mais fácil pedir perdão do que permissão) :

try:
    return int(s)
except (TypeError, ValueError, OverflowError): #int conversion failed
    return None
Remi
fonte
if index in mylisttesta se o índice é um elemento da minha lista, não um índice possível. Você preferiria if index < len(mylist).
ychaouche
1
Isso faz sentido, mas onde encontro a documentação que deixa claro quais possíveis exceções podem ser lançadas para o int (). docs.python.org/3/library/functions.html#int Não consigo encontrar essas informações aqui.
BrutalSimplicity
Em oposto, você pode adicionar este caso (de fora doc. ) Para a sua resposta, quando você deve preferir if/elsequetry/catch
rappongy
20

Nesse caso específico, você deve usar algo totalmente diferente:

x = myDict.get("ABC", "NO_ABC")

Porém, em geral: se você espera que o teste falhe com freqüência, use if. Se o teste for caro em relação a apenas tentar a operação e capturar a exceção, se falhar, use try. Se nenhuma dessas condições se aplicar, siga o que for mais fácil.

duskwuff -inactive-
fonte
1
+1 para a explicação no exemplo de código, que está no local.
ktdrv
4
Eu acho que é bem claro que não é isso que ele estava perguntando, e agora ele editou o post para deixar ainda mais claro.
precisa saber é
9

O uso trye exceptdiretamente, e não dentro de um ifguarda, sempre deve ser feito se houver a possibilidade de uma condição de corrida. Por exemplo, se você deseja garantir a existência de um diretório, não faça o seguinte:

import os, sys
if not os.path.isdir('foo'):
  try:
    os.mkdir('foo')
  except OSError, e
    print e
    sys.exit(1)

Se outro encadeamento ou processo criar o diretório entre isdire mkdir, você sairá. Em vez disso, faça o seguinte:

import os, sys, errno
try:
  os.mkdir('foo')
except OSError, e
  if e.errno != errno.EEXIST:
    print e
    sys.exit(1)

Isso será encerrado apenas se o diretório 'foo' não puder ser criado.

John Cowan
fonte
7

Se for trivial verificar se algo falhará antes de você fazer isso, provavelmente você deve favorecer isso. Afinal, a construção de exceções (incluindo os rastreios associados) leva tempo.

Exceções devem ser usadas para:

  1. coisas inesperadas, ou ...
  2. coisas nas quais você precisa pular mais de um nível de lógica (por exemplo, onde a breaknão o leva longe o suficiente) ou ...
  3. coisas em que você não sabe exatamente o que vai lidar com a exceção antes do tempo, ou ...
  4. coisas em que a verificação antecipada de falhas é cara (em relação a apenas tentar a operação)

Observe que, muitas vezes, a resposta real é "nem" - por exemplo, no seu primeiro exemplo, o que você realmente deve fazer é usar apenas .get()para fornecer um padrão:

x = myDict.get('ABC', 'NO_ABC')
Âmbar
fonte
exceto que, if 'ABC' in myDict: x = myDict['ABC']; else: x = 'NO_ABC'na verdade, geralmente é mais rápido do que usar get, infelizmente. Não estou dizendo que esse é o critério mais importante, mas é algo para estar ciente.
AGF
@agf: Melhor escrever código claro e conciso. Se precisa de algo a ser otimizado depois, é fácil voltar e reescrevê-lo, mas c2.com/cgi/wiki?PrematureOptimization
Âmbar
Eu sei disso; meu argumento era que if / elsee try / exceptpode ter seu lugar mesmo quando existem alternativas específicas de caso, porque elas têm características de desempenho diferentes.
AGF
@agf, você sabe se o método get () será aprimorado em versões futuras para ser (pelo menos) tão rápido quanto procurar explicitamente? A propósito, ao procurar duas vezes (como em 'ABC' em d: d ['ABC']), é tentar: d ['ABC'], exceto KeyError: ... não é mais rápido?
Remi
2
@Remi A parte lenta .get()é a procura de atributo e a sobrecarga de chamada de função no nível Python; o uso de palavras-chave nos built-ins basicamente vai direto para C. Eu não acho que isso vai ficar muito mais rápido tão cedo. Em relação a ifvs. try, o método read dict.get () retorna um ponteiro que possui algumas informações de desempenho. A proporção de acertos / erros importa ( trypode ser mais rápida se a chave quase sempre existir), assim como o tamanho do dicionário.
AGF
5

Como os outros posts mencionam, isso depende da situação. Existem alguns perigos com o uso de try / exceto no lugar de verificar antecipadamente a validade dos seus dados, especialmente quando os são usados ​​em projetos maiores.

  • O código no bloco try pode ter uma chance de causar todo tipo de confusão antes que a exceção seja detectada - se você verificar proativamente antecipadamente com uma instrução if, poderá evitar isso.
  • Se o código chamado no seu bloco try gerar um tipo de exceção comum, como TypeError ou ValueError, talvez você não consiga realmente pegar a mesma exceção que esperava capturar - pode ser outra coisa que gera a mesma classe de exceção antes ou depois de chegar ao a linha onde sua exceção pode ser gerada.

por exemplo, suponha que você tenha:

try:
    x = my_list[index_list[3]]
except IndexError:
    x = 'NO_ABC'

O IndexError não diz nada sobre se ocorreu ao tentar obter um elemento de index_list ou my_list.

Peter
fonte
4

O uso de uma tentativa em vez de um se deve ser interpretado como um erro passando silenciosamente? E se sim, você o está silenciando explicitamente usando-o dessa maneira, tornando-o correto?

Usar tryé reconhecer que um erro pode passar, que é o oposto de fazê-lo passar silenciosamente. Usar exceptestá fazendo com que não passe.

O uso try: except:é preferido nos casos em que a if: else:lógica é mais complicada. Simples é melhor que complexo; complexo é melhor que complicado; e é mais fácil pedir perdão do que permissão.

O que "erros nunca devem passar silenciosamente" é o aviso, é o caso em que o código pode gerar uma exceção que você conhece e o design admite a possibilidade, mas você não projetou de maneira a lidar com a exceção. Silenciar explicitamente um erro, na minha opinião, seria algo semelhante a passum exceptbloco, o que deveria ser feito apenas com a compreensão de que "não fazer nada" realmente é o tratamento correto de erros na situação específica. (Essa é uma das poucas vezes em que sinto que um comentário em código bem escrito provavelmente é realmente necessário.)

No entanto, no seu exemplo específico, nenhum é apropriado:

x = myDict.get('ABC', 'NO_ABC')

A razão pela qual todos estão apontando isso - mesmo que você reconheça seu desejo de entender em geral e a incapacidade de apresentar um exemplo melhor - é que etapas secundárias equivalentes existem de fato em muitos casos, e procurá-las é o primeiro passo para resolver o problema.

Karl Knechtel
fonte
3

Sempre que você usa try/exceptpara controlar o fluxo, pergunte a si mesmo:

  1. É fácil ver quando o trybloco é bem-sucedido e quando falha?
  2. Você está ciente de todos os efeitos colaterais dentro do trybloco?
  3. Você conhece todos os casos em que o trybloco lança a exceção?
  4. Se a implementação do trybloco mudar, seu fluxo de controle ainda se comportará conforme o esperado?

Se a resposta para uma ou mais dessas perguntas for 'não', pode haver muito perdão a ser solicitado; provavelmente do seu futuro eu.


Um exemplo. Recentemente, vi código em um projeto maior que se parecia com isso:

try:
    y = foo(x)
except ProgrammingError:
    y = bar(x)

Conversando com o programador, constatou-se que o fluxo de controle pretendido era:

Se x é um número inteiro, faça y = foo (x).

Se x é uma lista de números inteiros, faça y = bar (x).

Isso funcionou porque foofez uma consulta ao banco de dados e a consulta seria bem-sucedida se xfosse um número inteiro e lançaria uma ProgrammingErrorse xfosse uma lista.

Usar try/excepté uma má escolha aqui:

  1. O nome da exceção ProgrammingError,, não revela o problema real (que xnão é um número inteiro), o que dificulta a visualização do que está acontecendo.
  2. O ProgrammingErroré gerado durante uma chamada de banco de dados, que perde tempo. As coisas ficariam realmente horríveis se acontecesse que fooalgo gravasse no banco de dados antes de lançar uma exceção ou alterasse o estado de outro sistema.
  3. Não está claro se ProgrammingErroré gerado apenas quando xhá uma lista de números inteiros. Suponha, por exemplo, que haja um erro de digitação na fooconsulta ao banco de dados. Isso também pode aumentar a ProgrammingError. A conseqüência é que bar(x)agora também é chamado quando xé um número inteiro. Isso pode gerar exceções enigmáticas ou produzir resultados imprevisíveis.
  4. O try/exceptbloco adiciona um requisito para todas as implementações futuras de foo. Sempre que mudamos foo, devemos agora pensar em como ele lida com as listas e garantir que gera um ProgrammingErrore não, digamos, um AttributeErrorou nenhum erro.
Elias Strehle
fonte
0

Para um significado geral, considere ler Expressões idiomáticas e Anti-expressões idiomáticas em Python: Exceções .

No seu caso particular, como outros declararam, você deve usar dict.get():

get (chave [, padrão])

Retorne o valor para key se key estiver no dicionário, caso contrário, será o padrão. Se o padrão não for fornecido, o padrão será Nenhum, para que esse método nunca gere um KeyError.

etuardu
fonte
Não acho que o link cubra essa situação. Seus exemplos são sobre o tratamento de coisas que representam erros reais , e não sobre o uso de tratamento de exceção para situações esperadas.
AGF
O primeiro link está desatualizado.
Timofei Bondarev 24/08