Como limpar try / except / else aninhado?

8

Ao escrever código, geralmente quero fazer algo assim:

try:
    foo()
except FooError:
    handle_foo()
else:
    try:
        bar()
    except BarError:
        handle_bar()
    else:
        try:
            baz()
        except BazError:
            handle_baz()
        else:
            qux()
finally:
    cleanup()

Obviamente, isso é completamente ilegível. Mas está expressando uma idéia relativamente simples: execute uma série de funções (ou trechos de código curto), com um manipulador de exceção para cada uma, e pare assim que uma função falhar. Eu imagino que o Python poderia fornecer açúcar sintático para esse código, talvez algo como isto:

# NB: This is *not* valid Python
try:
    foo()
except FooError:
    handle_foo()
    # GOTO finally block
else try:
    bar()
except BarError:
    handle_bar()
    # ditto
else try:
    baz()
except BazError:
    handle_baz()
    # ditto
else:
    qux()
finally:
    cleanup()

Se nenhuma exceção for gerada, isso será equivalente a foo();bar();baz();qux();cleanup(). Se as exceções são geradas, elas são tratadas pelo manipulador de exceções apropriado (se houver) e pulamos para cleanup(). Em particular, se bar()gerar a FooErrorou BazError, a exceção não será capturada e será propagada para o chamador. Isso é desejável, de modo que apenas capturamos exceções que realmente esperamos tratar.

Independentemente da feiura sintática, esse tipo de código é apenas uma má idéia em geral? Se sim, como você o refatoraria? Imagino que os gerentes de contexto possam ser usados ​​para absorver parte da complexidade, mas não entendo realmente como isso funcionaria no caso geral.

Kevin
fonte
Que tipo de coisas você está fazendo nas handle_*funções?
Winston Ewert

Respostas:

8
try:
    foo()
except FooError:
    handle_foo()
else:
    ...
finally:
    cleanup()

O que handle_foofaz? Existem algumas coisas que normalmente fazemos nos blocos de manipulação de exceções.

  1. Limpeza após o erro: Mas, neste caso, foo () deve limpar depois de si mesmo, não nos deixe fazer isso. Além disso, a maioria dos trabalhos de limpeza é melhor gerenciada comwith
  2. Recupere para o caminho feliz: mas você não está fazendo isso porque não continua com o restante das funções.
  3. Traduz o tipo de exceção: mas você não está lançando outra exceção
  4. Registre o erro: mas não deve haver necessidade de blocos de exceção especiais para cada tipo.

Parece-me que você está fazendo algo estranho no tratamento de exceções. Sua pergunta aqui é um sintoma simples do uso de exceções de maneira incomum. Você não está caindo no padrão típico, e é por isso que isso se tornou estranho.

Sem uma idéia melhor do que você está fazendo nessas handle_funções, é tudo o que posso dizer.

Winston Ewert
fonte
Este é apenas um código de exemplo. As handle_funções podem ser facilmente trechos curtos de código que (digamos) registram o erro e retornam valores de fallback, ou geram novas exceções ou fazem várias outras coisas. Enquanto isso, o foo(), bar(), etc. funções poderiam também ser pequenos trechos de código. Por exemplo, podemos ter spam[eggs]e precisamos pegar KeyError.
21714 Kevin
1
@ Kevin, certo, mas isso não muda minha resposta. Se o seu manipulador returned ou raised alguma coisa, o problema que você menciona não surgiria. O restante da função seria ignorado automaticamente. De fato, uma maneira simples de resolver seu problema seria retornar em todos os blocos de exceção. O código é estranho porque você não está fazendo uma fiança ou aumento habitual para indicar que está desistindo.
Winston Ewert
Uma boa resposta em geral, mas tenho que discordar do seu ponto 1 - que evidência há que foodeveria estar fazendo sua própria limpeza? fooestá sinalizando que algo deu errado e não tem conhecimento de qual deve ser o procedimento de limpeza correto.
Ethan Furman
@ EthanFurman, acho que podemos estar pensando em itens diferentes sob o título de limpeza. Eu acho que se o foo abrir arquivos, conexões com o banco de dados, alocar memória, etc., então é responsabilidade do foo garantir que eles sejam fechados / desalocados antes de retornar. Isso é o que eu quis dizer com limpeza.
Winston Ewert
Ah, nesse caso, eu concordo plenamente com você. :)
Ethan Furman
3

Parece que você tem uma sequência de comandos que podem gerar uma exceção que precisa ser tratada antes de retornar. Tente agrupar seu código e manipulação de exceções em locais separados. Eu acredito que isso faz o que você pretende.

try:
    foo()
    bar()
    baz()
    qux()

except FooError:
    handle_foo()
except BarError:
    handle_bar()
except BazError:
    handle_baz()

finally:
    cleanup()
BillThor
fonte
2
Fui ensinado a tentar evitar colocar mais código do que o absolutamente necessário no try:bloco. Nesse caso, eu ficaria preocupado em bar()gerar a FooError, que seria tratado incorretamente com esse código.
21714 Kevin
2
Isso pode ser razoável se todos os métodos gerarem exceções muito específicas, mas isso geralmente não é verdade. Eu recomendo fortemente contra essa abordagem.
Winston Ewert
@ Kevin A quebra automática de cada linha de código em um bloco Try Catch Else torna-se rapidamente ilegível. Isso é feito ainda mais pelas regras de indentação do Python. Pense até que ponto isso acabaria se o código tivesse 10 linhas. Seria legível ou sustentável. Enquanto o código seguir a responsabilidade única, eu ficaria com o exemplo acima. Caso contrário, o código deve ser reescrito.
BillThor
@WinstonEwert Em um exemplo da vida real, eu esperaria alguma sobreposição nos erros (exceções). No entanto, eu também esperaria que o tratamento fosse o mesmo. BazError pode ser uma interrupção do teclado. Eu esperaria que fosse tratado adequadamente para todas as quatro linhas de código.
BillThor
1

Existem algumas maneiras diferentes, dependendo do que você precisa.

Aqui está uma maneira com loops:

try:
    for func, error, err_handler in (
            (foo, FooError, handle_foo),
            (bar, BarError, handle_bar),
            (baz, BazError, handle_baz),
        ):
        try:
            func()
        except error:
            err_handler()
            break
finally:
    cleanup()

Aqui está uma maneira com uma saída após o error_handler:

def some_func():
    try:
        try:
            foo()
        except FooError:
            handle_foo()
            return
        try:
            bar()
        except BarError:
            handle_bar()
            return
        try:
            baz()
        except BazError:
            handle_baz()
            return
        else:
            qux()
    finally:
        cleanup()

Pessoalmente, acho que a versão em loop é mais fácil de ler.

Ethan Furman
fonte
IMHO, esta é uma solução melhor do que a aceita, pois 1) tenta uma resposta e 2) torna a sequência muito clara.
bob
0

Antes de tudo, o uso apropriado de withmuitas vezes pode reduzir ou até eliminar grande parte do código de manipulação de exceções, melhorando a capacidade de manutenção e a legibilidade.

Agora, você pode reduzir o aninhamento de várias maneiras; outros pôsteres já forneceram alguns, então aqui está minha própria variação:

for _ in range(1):
    try:
        foo()
    except FooError:
        handle_foo()
        break
    try:
        bar()
    except BarError:
        handle_bar()
        break
    try:
        baz()
    except BazError:
        handle_baz()
        break
    qux()
cleanup()
Rufflewind
fonte