É uma boa prática usar o try-except-else no Python?

437

De tempos em tempos em Python, vejo o bloco:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

Qual é o motivo da existência do try-except-else?

Não gosto desse tipo de programação, pois está usando exceções para executar o controle de fluxo. No entanto, se estiver incluído no idioma, deve haver uma boa razão para isso, não é?

Entendo que as exceções não são erros e que devem ser usadas apenas para condições excepcionais (por exemplo, tento gravar um arquivo no disco e não há mais espaço, ou talvez não tenha permissão), e não para fluxo ao controle.

Normalmente, manejo exceções como:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

Ou se eu realmente não quiser retornar nada se uma exceção acontecer, então:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception
Juan Antonio Moriano, Gomez
fonte

Respostas:

665

"Não sei se é por ignorância, mas não gosto desse tipo de programação, pois está usando exceções para executar o controle de fluxo".

No mundo Python, o uso de exceções para controle de fluxo é comum e normal.

Até os desenvolvedores principais do Python usam exceções para controle de fluxo e esse estilo é fortemente incorporado à linguagem (ou seja, o protocolo iterador usa StopIteration para sinalizar o encerramento do loop).

Além disso, o estilo try-except é usado para evitar as condições de corrida inerentes a algumas das construções "olhar antes de pular" . Por exemplo, testar os.path.exists resulta em informações que podem estar desatualizadas quando você as usa. Da mesma forma, Queue.full retorna informações que podem estar obsoletas. O estilo try-except-else produzirá código mais confiável nesses casos.

"Entendo que exceções não são erros, elas devem ser usadas apenas para condições excepcionais"

Em alguns outros idiomas, essa regra reflete suas normas culturais, como refletidas em suas bibliotecas. A "regra" também se baseia em parte nas considerações de desempenho para esses idiomas.

A norma cultural Python é um pouco diferente. Em muitos casos, você deve usar exceções para o fluxo de controle. Além disso, o uso de exceções no Python não reduz a velocidade do código circundante e do código de chamada, como ocorre em algumas linguagens compiladas (ou seja, o CPython já implementa código para verificação de exceções a cada passo, independentemente de você usar ou não as exceções).

Em outras palavras, seu entendimento de que "exceções são excepcionais" é uma regra que faz sentido em outras linguagens, mas não no Python.

"No entanto, se estiver incluído no próprio idioma, deve haver uma boa razão para isso, não é?"

Além de ajudar a evitar condições de corrida, as exceções também são muito úteis para extrair loops externos de manipulação de erros. Essa é uma otimização necessária em linguagens interpretadas que não tendem a ter movimento de código invariável de loop automático .

Além disso, as exceções podem simplificar bastante o código em situações comuns em que a capacidade de lidar com um problema está muito longe de onde ele surgiu. Por exemplo, é comum ter um código de interface de usuário de nível superior que chame o código da lógica comercial, que por sua vez chama rotinas de baixo nível. As situações que surgem nas rotinas de baixo nível (como registros duplicados para chaves exclusivas nos acessos ao banco de dados) só podem ser tratadas no código de nível superior (como solicitar ao usuário uma nova chave que não entre em conflito com as chaves existentes). O uso de exceções para esse tipo de fluxo de controle permite que as rotinas de nível médio ignorem completamente o problema e sejam bem dissociadas desse aspecto do controle de fluxo.

Há um bom post sobre a indispensabilidade de exceções aqui .

Além disso, consulte esta resposta de estouro de pilha: as exceções são realmente para erros excepcionais?

"Qual é o motivo da tentativa, exceto outra coisa?"

A própria cláusula else é interessante. É executado quando não há exceção, mas antes da cláusula final. Esse é o seu objetivo principal.

Sem a cláusula else, a única opção para executar código adicional antes da finalização seria a prática desajeitada de adicionar o código à cláusula try. Isso é desajeitado porque corre o risco de gerar exceções no código que não foi projetado para ser protegido pelo bloco de tentativa.

O caso de uso da execução de código desprotegido adicional antes da finalização não ocorre com muita frequência. Portanto, não espere ver muitos exemplos no código publicado. É um tanto raro.

Outro caso de uso para a cláusula else é executar ações que devem ocorrer quando nenhuma exceção ocorre e que não ocorrem quando as exceções são tratadas. Por exemplo:

recip = float('Inf')
try:
    recip = 1 / f(x)
except ZeroDivisionError:
    logging.info('Infinite result')
else:
    logging.info('Finite result')

Outro exemplo ocorre em corredores mais unidos:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

Por fim, o uso mais comum de uma cláusula else em um bloco de teste é para um pouco de embelezamento (alinhando os resultados excepcionais e não excepcionais no mesmo nível de indentação). Esse uso é sempre opcional e não é estritamente necessário.

Raymond Hettinger
fonte
28
"Isso é desajeitado porque corre o risco de gerar exceções no código que não foi projetado para ser protegido pelo try-block". Este é o aprendizado mais importante aqui
Felix Dombek
2
Obrigado pela resposta. Para os leitores que procuram exemplos de try-exceto-else uso, dê uma olhada método copyfile de shutil github.com/python/cpython/blob/master/Lib/shutil.py#L244
suripoori
2
O ponto é que a cláusula else só é executada quando a cláusula try for bem-sucedida.
Jonathan
172

Qual é o motivo da existência do try-except-else?

Um trybloco permite que você lide com um erro esperado. O exceptbloco deve capturar apenas exceções com as quais você está preparado para lidar. Se você manipular um erro inesperado, seu código poderá fazer a coisa errada e ocultar bugs.

Uma elsecláusula será executada se não houver erros e, ao não executar esse código no trybloco, você evita a captura de um erro inesperado. Mais uma vez, capturar um erro inesperado pode ocultar bugs.

Exemplo

Por exemplo:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

O conjunto "try, except" possui duas cláusulas opcionais elsee finally. Então é verdade try-except-else-finally.

elseavaliará apenas se não houver exceção do trybloco. Permite simplificar o código mais complicado abaixo:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

portanto, se compararmos elsecom a alternativa (que pode criar bugs), veremos que ela reduz as linhas de código e podemos ter uma base de código mais legível, sustentável e com menos bugs.

finally

finally será executado independentemente do que for, mesmo que outra linha esteja sendo avaliada com uma declaração de retorno.

Dividido com pseudo-código

Pode ajudar a decompor isso, na menor forma possível que demonstre todos os recursos, com comentários. Suponha que esse pseudocódigo sintaticamente correto (mas não executável, a menos que os nomes sejam definidos) esteja em uma função.

Por exemplo:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

É verdade que, em vez disso, poderíamos incluir o código no elsebloco try, onde seria executado se não houvesse exceções, mas e se esse código em si gerar uma exceção do tipo que estamos capturando? Deixá-lo no trybloco esconderia esse bug.

Queremos minimizar as linhas de código no trybloco para evitar capturar exceções que não esperávamos, sob o princípio de que, se nosso código falhar, queremos que falhe alto. Essa é uma prática recomendada .

Entendo que exceções não são erros

No Python, a maioria das exceções são erros.

Podemos visualizar a hierarquia de exceções usando pydoc. Por exemplo, no Python 2:

$ python -m pydoc exceptions

ou Python 3:

$ python -m pydoc builtins

Nos dará a hierarquia. Podemos ver que muitos tipos de Exceptionerros, embora o Python os use para coisas como finalizar forloops ( StopIteration). Esta é a hierarquia do Python 3:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

Um comentarista perguntou:

Digamos que você tenha um método que envia uma API externa e deseja manipular a exceção em uma classe fora do wrapper da API. Você simplesmente retorna e do método na cláusula exceto onde e é o objeto de exceção?

Não, você não retorna a exceção, basta aumentá-la com um simples raisepara preservar o rastreamento de pilha.

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

Ou, no Python 3, você pode criar uma nova exceção e preservar o backtrace com o encadeamento de exceções:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

Eu elaboro em minha resposta aqui .

Aaron Hall
fonte
votado! o que você costuma fazer dentro da parte do cabo? digamos que você tenha um método que faz ping em uma API externa e deseja manipular a exceção em uma classe fora do wrapper da API, você simplesmente retorna e do método na cláusula exceto em que e é o objeto de exceção?
PirateApp 14/07/2018
1
@PirateApp obrigado! não, não devolva, você provavelmente deve reraise com um raiseencadeamento simples ou excepcional - mas isso é mais sobre o tópico e é abordado aqui: stackoverflow.com/q/2052390/541136 - provavelmente removerei esses comentários depois de ter visto que você os viu.
Aaron Hall
muito obrigado pelos detalhes! passando o post agora
PirateApp 14/07/2018
36

O Python não concorda com a idéia de que as exceções devem ser usadas apenas para casos excepcionais; na verdade, o idioma é 'peça perdão, não permissão' . Isso significa que o uso de exceções como parte rotineira do seu controle de fluxo é perfeitamente aceitável e, de fato, incentivado.

Isso geralmente é uma coisa boa, pois trabalhar dessa maneira ajuda a evitar alguns problemas (como um exemplo óbvio, as condições de corrida geralmente são evitadas) e tende a tornar o código um pouco mais legível.

Imagine que você tem uma situação em que recebe algumas informações do usuário que precisam ser processadas, mas tem um padrão que já está processado. A try: ... except: ... else: ...estrutura cria um código muito legível:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

Compare com o funcionamento em outros idiomas:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

Observe as vantagens. Não há necessidade de verificar se o valor é válido e analisá-lo separadamente, eles são feitos uma vez. O código também segue uma progressão mais lógica, o caminho principal do código é o primeiro, seguido por 'se não funcionar, faça isso'.

O exemplo é naturalmente um pouco artificial, mas mostra que há casos para essa estrutura.

Gareth Latty
fonte
15

É uma boa prática usar try-except-else em python?

A resposta para isso é que depende do contexto. Se você fizer isto:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

Isso demonstra que você não conhece muito bem o Python. Essa funcionalidade está encapsulada no dict.getmétodo:

item = d.get('item', 'default')

O bloco try/ excepté uma maneira muito mais visual e desordenada de escrever o que pode ser executado com eficiência em uma única linha com um método atômico. Existem outros casos em que isso é verdade.

No entanto, isso não significa que devemos evitar todo o tratamento de exceções. Em alguns casos, é preferível evitar as condições da corrida. Não verifique se existe um arquivo, apenas tente abri-lo e pegue o IOError apropriado. Por uma questão de simplicidade e legibilidade, tente encapsular isso ou fatorá-lo como apropriado.

Leia o Zen de Python , entendendo que existem princípios que estão em tensão, e tenha cuidado com o dogma que depende muito de qualquer uma das declarações nele contidas.

Aaron Hall
fonte
12

Veja o exemplo a seguir, que ilustra tudo sobre try-except-else-finalmente:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = {i}")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = {i}")
        print(f"\tNo error report and y equals {y}")
    finally:
        print("Try block is run.")

Implementá-lo e passar por:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.
Cálculo
fonte
3
Este é um exemplo simples e excelente que demonstra rapidamente a cláusula try completa sem exigir que alguém (que esteja com pressa) leia uma explicação abstrata e extensa. (É claro que, quando eles não estão com pressa mais, eles devem voltar e ler o resumo completo.)
GlobalSoftwareSociety
6

Você deve ter cuidado ao usar o bloco final, pois não é a mesma coisa que usar outro bloco na tentativa, exceto. O bloco final será executado independentemente do resultado da tentativa, exceto.

In [10]: dict_ = {"a": 1}

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

Como todos observaram, o uso do bloco else faz com que seu código seja mais legível e só é executado quando uma exceção não é lançada

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:
Greg
fonte
Eu sei que finalmente é sempre executado, e é por isso que pode ser usado em nossa vantagem, sempre definindo um valor padrão; portanto, em caso de exceção, ele é retornado; se não queremos retornar esse valor em caso de exceção, ele é suficiente para remover o bloco final. Btw, utilizando passagem sobre uma captura exceção é algo que eu nunca iria fazer :)
Juan Antonio Gomez Moriano
@ Juan Antonio Gomez Moriano, meu bloco de codificação é apenas para fins de exemplo. Eu provavelmente nunca usaria o passe também
Greg
4

Sempre que você vir isso:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

Ou até isso:

try:
    return 1 / x
except ZeroDivisionError:
    return None

Considere isso:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x
Rajiv Bakulesh Shah
fonte
1
Não responde à minha pergunta, pois esse foi simplesmente um exemplo, meu amigo.
Juan Antonio Gomez Moriano
No Python, exceções não são erros. Eles nem são excepcionais. No Python, é normal e natural usar exceções para controle de fluxo. Isso é evidenciado pela inclusão de contextlib.suppress () na biblioteca padrão. Veja a resposta de Raymond Hettinger aqui: stackoverflow.com/a/16138864/1197429 (Raymond é um contribuinte Python core, e uma autoridade em todas as coisas Pythonic!)
Rajiv Shah Bakulesh
4

Só porque ninguém mais postou essa opinião, eu diria

evitar elsecláusulas try/excepts porque não são familiares para a maioria das pessoas

Ao contrário das palavras-chave try, excepte finally, o significado da elsecláusula não é auto-evidente; é menos legível. Como não é usado com muita frequência, as pessoas que leem seu código desejam verificar novamente os documentos para garantir que entendem o que está acontecendo.

(Estou escrevendo esta resposta precisamente porque encontrei uma try/except/elsena minha base de código e isso causou um momento wtf e me forçou a pesquisar no Google).

Então, onde quer que eu veja códigos como o exemplo do OP:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

Eu preferiria refatorar para

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something
  • <1> retorno explícito, mostra claramente que, no caso de exceção, terminamos de trabalhar

  • <2> como um bom efeito colateral menor, o código que costumava estar no elsebloco é deduzido por um nível.

hwjp
fonte
1
Contra-argumento do argumento do diabo: quanto mais pessoas o usam, mais bem-adotado ele se tornará. Apenas alimento para reflexão, embora eu concorde que a legibilidade é importante. Dito isto, uma vez que alguém entenda a tentativa, eu argumentaria que é muito mais legível do que a alternativa em muitos casos.
bob
2

Este é meu trecho simples de como entender o bloco try-except-else-finalmente no Python:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

Vamos tentar div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

Vamos tentar div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done
zakiakhmad
fonte
1
Eu acho que esta falha para exemplificar por que você não pode simplesmente colocar o código de outra pessoa dentro do try
Mojimi
-4

OP, VOCÊ ESTÁ CORRETO. O resto após try / except em Python é feio . leva a outro objeto de controle de fluxo, onde não é necessário:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

Um equivalente totalmente claro é:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

Isso é muito mais claro do que uma cláusula else. O resto após try / except não é frequentemente escrito, portanto leva um momento para descobrir quais são as implicações.

Só porque você pode fazer uma coisa, não significa que você deve fazer uma coisa.

Muitos recursos foram adicionados aos idiomas porque alguém achou que poderia ser útil. O problema é que, quanto mais recursos, as coisas menos claras e óbvias são porque as pessoas geralmente não usam esses sinos e assobios.

Apenas meus 5 centavos aqui. Eu tenho que vir atrás e limpar muito código escrito por desenvolvedores de faculdades do primeiro ano que pensam que são inteligentes e querem escrever código de uma maneira super apertada e super eficiente, quando isso faz uma bagunça para tentar ler / modificar mais tarde. Eu voto pela legibilidade todos os dias e duas vezes aos domingos.

Kevin J. Rice
fonte
15
Você está certo. Isso é totalmente claro e equivalente ... a menos que seja sua printdeclaração que falha. O que acontece se x = blah()retornar a str, mas a sua declaração de impressão for print 'just succeeded with blah. x == %d' % x? Agora você tem um TypeErrorser gerado onde não está preparado para lidar com um; você está inspecionando x = blah()para encontrar a fonte da exceção, e ela nem está lá. Já fiz isso (ou o equivalente) mais de uma vez, onde elseme impediria de cometer esse erro. Agora eu sei melhor. :-D
Doug R.
2
... e sim, você está certo. A elsecláusula não é uma afirmação bonita e, até que você esteja acostumado, não é intuitiva. Mas, então, nem foi finallyquando eu comecei a usá-lo ...
Doug R.
2
Para repetir Doug R., não é equivalente, porque as exceções durante as instruções na elsecláusula não são capturadas pelo except.
Alastair
se ... exceto ... else for mais legível, caso contrário, você deverá ler "oh, após o bloco try, e sem exceção, vá para a instrução fora do bloco try", então usando else: tende a conectar semanticamente a sintaxe um pouco melhor imo. Além disso, é uma boa prática deixar suas instruções não capturadas fora do bloco de tentativa inicial.
cowbert
1
@DougR. "você está inspecionando x = blah()para encontrar a fonte da exceção". tracebackPor que você inspecionaria a fonte da exceção de um lugar errado?
Nehem