Usando try vs if em python

145

Existe uma justificativa para decidir qual tryou quais ifconstruções usar ao testar a variável para ter um valor?

Por exemplo, há uma função que retorna uma lista ou não retorna um valor. Quero verificar o resultado antes de processá-lo. Qual das alternativas a seguir seria mais preferível e por quê?

result = function();
if (result):
    for r in result:
        #process items

ou

result = function();
try:
    for r in result:
        #process items
except TypeError:
    pass;

Discussão relacionada:

Verificando a existência de membros em Python

artdanil
fonte
Lembre-se de que, se a sub-rotina de #process items for suscetível de lançar um TypeError, você precisará encerrar outra tentativa: exceto: block. Para este exemplo específico eu apenas usando um if:
Richo

Respostas:

237

Você costuma ouvir que o Python incentiva o estilo EAFP ("é mais fácil pedir perdão do que permissão") sobre o estilo LBYL ("veja antes de pular"). Para mim, é uma questão de eficiência e legibilidade.

No seu exemplo (digamos que, em vez de retornar uma lista ou uma sequência vazia, a função retornasse uma lista ou None), se você espera que 99% do tempo resultrealmente contenha algo iterável, eu usaria a try/exceptabordagem. Será mais rápido se as exceções forem realmente excepcionais. Se resultfor Nonemais de 50% do tempo, o uso ifprovavelmente será melhor.

Para suportar isso com algumas medidas:

>>> import timeit
>>> timeit.timeit(setup="a=1;b=1", stmt="a/b") # no error checking
0.06379691968322732
>>> timeit.timeit(setup="a=1;b=1", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.0829463709378615
>>> timeit.timeit(setup="a=1;b=0", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.5070195056614466
>>> timeit.timeit(setup="a=1;b=1", stmt="if b!=0:\n a/b")
0.11940114974277094
>>> timeit.timeit(setup="a=1;b=0", stmt="if b!=0:\n a/b")
0.051202772912802175

Portanto, enquanto uma ifdeclaração sempre lhe custa, é quase livre configurar um try/exceptbloco. Mas quando Exceptionrealmente ocorre, o custo é muito maior.

Moral:

  • É perfeitamente aceitável (e "pitônico") usar try/exceptno controle de fluxo,
  • mas faz mais sentido quando Exceptions são realmente excepcionais.

Nos documentos do Python:

EAFP

Mais fácil pedir perdão do que permissão. Esse estilo comum de codificação Python pressupõe a existência de chaves ou atributos válidos e captura exceções se a suposição for falsa. Este estilo limpo e rápido é caracterizado pela presença de muitos trye exceptdeclarações. A técnica contrasta com o estilo LBYL comum a muitos outros idiomas, como C.

Tim Pietzcker
fonte
1
Obrigado. Vejo que, neste caso, a lógica pode ser a expectativa do resultado.
Artdanil 02/12/2009
6
.... e essa é uma (de muitas) razões pelas quais é tão difícil fazer um JIT de otimização real para Python. Como o LuaJIT 2, recente na versão beta, prova, linguagens dinâmicas podem ser muito, muito rápidas; mas depende muito do design inicial da linguagem e do estilo que incentiva. (Em uma nota relacionada, design língua é por isso que até mesmo os melhores EIC JavaScirpt não se pode comparar LuaJIT 1, muito menos 2)
Javier
1
@ 2rs2ts: Eu também fiz horários semelhantes. No Python 3, try/exceptfoi 25% mais rápido do que if key in d:nos casos em que a chave estava no dicionário. Era muito mais lento quando a chave não estava no dicionário, como esperado, e consistente com esta resposta.
Tim Tim #
5
Eu sei que essa é uma resposta antiga, no entanto: usar declarações como 1/1com timeit não é uma ótima opção, porque elas serão otimizadas (veja dis.dis('1/1')e observe que nenhuma divisão ocorre).
Andrea Corbellini
1
Também depende da operação que você deseja executar. Uma única divisão é rápida, pegar um erro é um pouco caro. Mas mesmo nos casos em que o custo do tratamento de exceções é aceitável, pense cuidadosamente no que você está pedindo ao computador. Se a operação em si for muito cara, independentemente de seus resultados, e há uma maneira barata de saber se o sucesso é possível: use-a. Exemplo: Não copie metade de um arquivo apenas para perceber que não há espaço suficiente.
Bachsau
14

Sua função não deve retornar tipos mistos (ou seja, lista ou sequência vazia). Ele deve retornar uma lista de valores ou apenas uma lista vazia. Então você não precisaria testar nada, ou seja, seu código é recolhido para:

for r in function():
    # process items
Brandon
fonte
2
Concordo plenamente com você nesse ponto; no entanto, a função não é minha e estou apenas usando-a.
Artdanil 02/12/2009
2
@artdanil: Então você pode agrupar essa função em uma que você escreve que funciona como a que Brandon Corfman está pensando.
quamrana 02/12/2009
24
o que suscita a pergunta: o invólucro deve usar um if ou uma tentativa de lidar com o caso não iterável?
jcdyer
@quamrana, que é exatamente o que estou tentando fazer. Assim como o @jcd apontou, a questão é sobre como devo lidar com a situação na função wrapper.
Artdanil 03/12/2009
12

Ignore minha solução se o código que forneço não for óbvio à primeira vista e você precisar ler a explicação após o exemplo de código.

Posso assumir que o "nenhum valor retornado" significa que o valor de retorno é Nenhum? Se sim, ou se "no value" for False em termos de booleano, você pode fazer o seguinte, pois seu código trata essencialmente "no value" como "não iterate":

for r in function() or ():
    # process items

Se function()retornar algo que não é True, você itera sobre a tupla vazia, ou seja, não executa iterações. Isto é essencialmente LBYL.

tzot
fonte
4

Seu segundo exemplo está quebrado - o código nunca lançará uma exceção TypeError, pois você pode percorrer as strings e as listas. A iteração através de uma string ou lista vazia também é válida - ele executará o corpo do loop zero vezes.

Dave Kirby
fonte
4

Qual das alternativas a seguir seria mais preferível e por quê?

Look Before You Leap é preferível neste caso. Com a abordagem de exceção, um TypeError poderia ocorrer em qualquer lugar do corpo do loop e seria pego e jogado fora, o que não é o que você deseja e tornará a depuração complicada.

(Porém, concordo com Brandon Corfman: retornar Nenhum para 'sem itens' em vez de uma lista vazia está quebrado. É um hábito desagradável de codificadores Java que não devem ser vistos em Python. Ou Java.)

bobince
fonte
4

Geralmente, a impressão que tive é que as exceções devem ser reservadas para circunstâncias excepcionais. Se o resultesperado é que nunca fique vazio (mas pode ser, se, por exemplo, um disco travou, etc.), a segunda abordagem faz sentido. Se, por outro lado, um vazio resulté perfeitamente razoável em condições normais, testá-lo com uma ifdeclaração faz mais sentido.

Eu tinha em mente o cenário (mais comum):

# keep access counts for different files
file_counts={}
...
# got a filename somehow
if filename not in file_counts:
    file_counts[filename]=0
file_counts[filename]+=1

em vez do equivalente:

...
try:
    file_counts[filename]+=1
except KeyError:
    file_counts[filename]=1
Managu
fonte
Este é um exemplo da diferença entre o aproxima Tim Pietzcker menciona: A primeira é LBYL, o segundo é EAFP
Managu
Apenas uma observação rápida que ++não funciona em python, use-a += 1.
tgray 02/12/2009
3

bobince sabiamente ressalta que agrupar o segundo caso também pode capturar TypeErrors no loop, o que não é o que você deseja. Se você realmente deseja usar uma tentativa, pode testar se é iterável antes do loop

result = function();
try:
    it = iter(result)
except TypeError:
    pass
else:
    for r in it:
        #process items

Como você pode ver, é bastante feio. Eu não sugiro, mas deve ser mencionado para ser completo.

AFoglia
fonte
A meu ver, encontrar erros de tipo intencionalmente é sempre um estilo de codificação ruim. Um desenvolvedor deve saber o que esperar e estar preparado para lidar com esses valores sem exceções. Sempre que uma operação fez o que deveria fazer (do ponto de vista do programador) e os resultados esperados são uma lista ou None, sempre verifique o resultado com is Noneou is not None. Por outro lado, também é péssimo criar exceções para obter resultados legítimos. Exceções são para coisas inesperadas. Exemplo: str.find()retorna -1 se nada for encontrado, porque a própria pesquisa concluída sem erros.
Bachsau
1

No que diz respeito ao desempenho, usar o bloco try para código que normalmente não gera exceções é mais rápido do que usar a instrução if toda vez. Portanto, a decisão depende da probabilidade de casos excecionais.

grayger
fonte
-5

Como regra geral, você nunca deve usar try / catch ou qualquer exceção para manipular itens para controlar o fluxo. Mesmo que a iteração dos bastidores seja controlada através do aumento de StopIterationexceções, você ainda deve preferir seu primeiro trecho de código ao segundo.

ilowe
fonte
7
Isso é verdade em Java. O Python abraça bastante o EAFP.
Fengb 02/12/2009
3
Ainda é uma prática estranha. Os cérebros da maioria dos programadores estão ligados para pular o EAFP na minha experiência. Eles acabam se perguntando por que um determinado caminho foi selecionado. Dito isto, há um tempo e um lugar para quebrar todas as regras.
Ilowe 02/12/2009