Existem exceções para as melhores práticas de controle de fluxo no Python?


15

Estou lendo "Learning Python" e deparei-me com o seguinte:

Exceções definidas pelo usuário também podem sinalizar condições sem erro. Por exemplo, uma rotina de pesquisa pode ser codificada para gerar uma exceção quando uma correspondência é encontrada, em vez de retornar um sinalizador de status para o chamador interpretar. A seguir, o manipulador de exceção try / except / else executa o trabalho de um testador de valor de retorno if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Como o Python é dinamicamente digitado e polimórfico até o núcleo, as exceções, em vez dos valores de retorno sentinela, são a maneira geralmente preferida de sinalizar tais condições.

Eu já vi esse tipo de coisa discutido várias vezes em vários fóruns e referências ao Python usando StopIteration para finalizar loops, mas não consigo encontrar muita coisa nos guias oficiais de estilo (o PEP 8 tem uma referência direta a exceções para controle de fluxo) ou declarações de desenvolvedores. Existe algo oficial que afirme que essa é uma prática recomendada para Python?

Isso (as exceções como fluxo de controle são consideradas um sério antipadrão? Em caso afirmativo, por quê? ) Também tem vários comentadores que afirmam que esse estilo é Pythonic. Em que isso se baseia?

TIA

Respostas:


25

O consenso geral "não use exceções!" Vem principalmente de outros idiomas e até mesmo algumas vezes está desatualizado.

  • No C ++, lançar uma exceção é muito caro devido ao “desenrolar da pilha”. Toda declaração de variável local é como uma withdeclaração em Python, e o objeto nessa variável pode executar destruidores. Esses destruidores são executados quando uma exceção é lançada, mas também ao retornar de uma função. Esse "idioma do RAII" é um recurso de linguagem integral e é super importante escrever código correto e robusto - portanto, o RAII versus as exceções baratas foi uma desvantagem que o C ++ decidiu pelo RAII.

  • No C ++ inicial, muitos códigos não eram gravados de maneira segura para exceções: a menos que você realmente use RAII, é fácil vazar memória e outros recursos. Portanto, lançar exceções tornaria esse código incorreto. Isso não é mais razoável, pois mesmo a biblioteca padrão C ++ usa exceções: você não pode fingir que não existem. No entanto, as exceções ainda são um problema ao combinar o código C com o C ++.

  • Em Java, toda exceção tem um rastreamento de pilha associado. O rastreamento da pilha é muito valioso ao depurar erros, mas desperdiça esforço quando a exceção nunca é impressa, por exemplo, porque foi usada apenas para o fluxo de controle.

Portanto, nesses idiomas, as exceções são "muito caras" para serem usadas como fluxo de controle. No Python, isso é menos problemático e as exceções são muito mais baratas. Além disso, a linguagem Python já sofre com alguma sobrecarga que torna o custo das exceções imperceptíveis em comparação com outras construções de fluxo de controle: por exemplo, verificar se existe uma entrada dict com um teste de associação explícito if key in the_dict: ...geralmente é exatamente tão rápido quanto simplesmente acessar a entrada the_dict[key]; ...e verificar se você obtenha um KeyError. Alguns recursos de linguagem integral (por exemplo, geradores) são projetados em termos de exceções.

Portanto, embora não haja razão técnica para evitar especificamente exceções no Python, ainda há a questão de se você deve usá-las em vez dos valores de retorno. Os problemas no nível do design, com exceções, são:

  • eles não são de todo óbvios. Você não pode olhar facilmente para uma função e ver quais exceções podem ser lançadas, portanto nem sempre sabe o que capturar. O valor de retorno tende a ser mais bem definido.

  • As exceções são o fluxo de controle não local, o que complica seu código. Quando você lança uma exceção, não sabe onde o fluxo de controle será retomado. Para erros que não podem ser tratados imediatamente, essa é provavelmente uma boa idéia, ao notificar o chamador sobre uma condição, isso é totalmente desnecessário.

A cultura Python geralmente é inclinada em favor de exceções, mas é fácil exagerar. Imagine uma list_contains(the_list, item)função que verifique se a lista contém um item igual a esse item. Se o resultado for comunicado por meio de exceções, é absolutamente irritante, porque temos que chamá-lo assim:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Retornar um bool seria muito mais claro:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Se a função já deve retornar um valor, retornar um valor especial para condições especiais é bastante suscetível a erros, porque as pessoas esquecem de verificar esse valor (provavelmente é a causa de 1/3 dos problemas em C). Uma exceção é geralmente mais correta.

Um bom exemplo é uma pos = find_string(haystack, needle)função que procura a primeira ocorrência da needlestring na string `haystack e retorna a posição inicial. Mas e se a corda do palheiro não contiver a agulha?

A solução de C e imitada pelo Python é retornar um valor especial. Em C, este é um ponteiro nulo; em Python, esse é -1. Isso levará a resultados surpreendentes quando a posição for usada como um índice de string sem verificação, especialmente como -1é um índice válido no Python. Em C, seu ponteiro NULL fornecerá pelo menos um segfault.

No PHP, um valor especial de um tipo diferente é retornado: o booleano em FALSEvez de um número inteiro. Acontece que isso não é realmente melhor devido às regras implícitas de conversão da linguagem (mas observe que no Python também os booleanos podem ser usados ​​como ints!). Funções que não retornam um tipo consistente são geralmente consideradas muito confusas.

Uma variante mais robusta seria lançar uma exceção quando a cadeia não puder ser encontrada, o que garante que durante o fluxo de controle normal seja impossível usar acidentalmente o valor especial no lugar de um valor comum:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Como alternativa, sempre é possível retornar um tipo que não pode ser usado diretamente, mas que primeiro deve ser desembrulhado, por exemplo, uma tupla de resultado-bool em que o booleano indica se ocorreu uma exceção ou se o resultado é utilizável. Então:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Isso obriga a lidar com problemas imediatamente, mas fica irritante muito rapidamente. Também impede que você encadeie a função facilmente. Toda chamada de função agora precisa de três linhas de código. Golang é uma linguagem que acha que esse incômodo vale a pena.

Então, para resumir, as exceções não são totalmente isentas de problemas e podem definitivamente ser superutilizadas, especialmente quando substituem um valor de retorno "normal". Porém, quando usado para sinalizar condições especiais (não necessariamente apenas erros), as exceções podem ajudá-lo a desenvolver APIs limpas, intuitivas, fáceis de usar e difíceis de mau uso.


1
Obrigado pela resposta detalhada. Eu venho de um background de outras linguagens como Java, onde "exceções são apenas para condições excepcionais", então isso é novo para mim. Existem desenvolvedores principais do Python que já declararam isso dessa maneira com outras diretrizes do Python, como EAFP, EIBTI etc. (em uma lista de discussão, postagem no blog etc.) Eu gostaria de poder citar algo oficial se outro desenvolvedor / chefe pergunta. Obrigado!
JB

Ao sobrecarregar o método fillInStackTrace da sua classe de exceção personalizada para retornar imediatamente, você pode fazer com que seja muito barato aumentar, pois a caminhada pela pilha é cara e é aqui que é feita.
John Cowan

Seu primeiro marcador na introdução foi confuso no início. Talvez você possa alterá-lo para algo como "O código de manipulação de exceção no C ++ moderno é de custo zero; mas lançar uma exceção é caro"? (por lurkers: stackoverflow.com/questions/13835817/... ) [editar: editar proposto]
phresnel

Observe que, para operações de pesquisa de dicionário usando um código collections.defaultdictou my_dict.get(key, default)torna o código muito mais claro do quetry: my_dict[key] except: return default
Steve Barnes

11

NÃO! - não em geral - as exceções não são consideradas boas práticas de controle de fluxo, com exceção da única classe de código. O único lugar em que as exceções são consideradas uma maneira razoável ou até melhor de sinalizar uma condição são as operações do gerador ou do iterador. Essas operações podem retornar qualquer valor possível como resultado válido, portanto, é necessário um mecanismo para sinalizar um acabamento.

Considere ler um arquivo binário do fluxo, um byte de cada vez - absolutamente qualquer valor é um resultado potencialmente válido, mas ainda precisamos sinalizar o final do arquivo. Portanto, temos uma escolha, retorne dois valores (o valor do byte e um sinalizador válido), sempre ou crie uma exceção quando não houver mais o que fazer. Nos dois casos, o código de consumo pode se parecer com:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

alternativamente:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Mas isso, desde que o PEP 343 foi implementado e portado, tudo foi cuidadosamente incluído na withdeclaração. O acima se torna, o próprio python:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

Em python3 isso se tornou:

for val in open(source, 'rb').read():
     processbyte(val)

Peço fortemente que você leia PEP 343, que fornece os antecedentes, justificativas, exemplos etc.

Também é comum usar uma exceção para sinalizar o final do processamento ao usar as funções do gerador para sinalizar o final.

Gostaria de acrescentar que seu exemplo de pesquisador é quase certamente reverso; essas funções devem ser geradoras, retornando a primeira correspondência na primeira chamada, depois chamadas substituintes retornando a próxima correspondência e gerando uma NotFoundexceção quando não houver mais correspondências.


Você diz: "NÃO! - não em geral - as exceções não são consideradas boas práticas de controle de fluxo, com exceção da classe única de código". Onde está a lógica dessa afirmação enfática? Essa resposta parece focar estreitamente no caso da iteração. Existem muitos outros casos em que as pessoas podem pensar em usar exceções. Qual é o problema de fazê-lo nesses outros casos?
Daniel Waltrip

@DanielWaltrip Eu vejo muito código, em várias linguagens de programação, onde são usadas exceções, geralmente muito extensas, quando um simples ifseria melhor. Quando se trata de depuração e teste ou mesmo de raciocínio sobre o comportamento dos seus códigos, é melhor não confiar em exceções - elas devem ser a exceção. Eu vi, no código de produção, um cálculo que retornou por meio de um throw / catch em vez de um retorno simples, o que significa que, quando o cálculo atinge um erro de divisão por zero, ele retorna um valor aleatório em vez de falhar, quando o erro aconteceu, causa várias horas de depuração.
Steve Barnes

Hey @SteveBarnes, o exemplo de captura de erro de divisão por zero soa como programação ruim (com uma captura muito geral que não gera novamente a exceção).
wTyeRogers 22/08/19

Sua resposta parece se resumir a: A) "NÃO!" B) existem exemplos válidos de onde o fluxo de controle via exceções funciona melhor que as alternativas C) uma correção do código da pergunta para usar geradores (que usam exceções como fluxo de controle e o fazem bem). Embora sua resposta seja geralmente informativa, nenhum argumento parece existir para apoiar o item A, que, sendo em negrito e aparentemente a principal declaração, eu esperaria algum apoio. Se fosse suportado, não recusaria sua resposta. Ai ...
wTyeRogers
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.