A maneira mais eficiente de fazer uma instrução if-elif-elif-else quando o else é feito mais?

99

Eu tenho uma instrução in if-elif-elif-else na qual 99% das vezes, a instrução else é executada:

if something == 'this':
    doThis()
elif something == 'that':
    doThat()
elif something == 'there':
    doThere()
else:
    doThisMostOfTheTime()

Essa construção é feita muito , mas como ela examina todas as condições antes de atingir o else, tenho a sensação de que não é muito eficiente, muito menos Pythônico. Por outro lado, ele precisa saber se alguma dessas condições foi atendida, portanto, deve testá-la de qualquer maneira.

Alguém sabe se e como isso poderia ser feito de forma mais eficiente ou esta é simplesmente a melhor maneira possível de fazê-lo?

Kramer65
fonte
Você consegue sortacorrentar as coisas que está executando seu if / else ... de modo que todos os elementos que uma das condições corresponderá estejam em uma extremidade e todos os demais estejam na outra? Se sim, você pode ver se isso é mais rápido / mais elegante ou não. Mas lembre-se, se não houver problema de desempenho, é muito cedo para se preocupar com a otimização.
Patashu
4
Existe algo que os três casos especiais têm em comum? Por exemplo, você poderia fazer if not something.startswith("th"): doThisMostOfTheTime()e fazer outra comparação na elsecláusula.
Tim Pietzcker de
3
@ kramer65 Se for uma cadeia tão longa de if / elif ... pode ser lento, mas certifique-se de realmente criar o perfil de seu código e começar otimizando qualquer parte que leve mais tempo.
jorgeca de
1
Essas comparações são realizadas apenas uma vez por valor de something, ou comparações semelhantes são realizadas várias vezes no mesmo valor?
Chris Pitman de

Respostas:

98

O código...

options.get(something, doThisMostOfTheTime)()

... parece que deveria ser mais rápido, mas na verdade é mais lento do que a construção if... elif... else, porque tem que chamar uma função, o que pode ser uma sobrecarga de desempenho significativa em um loop fechado.

Considere estes exemplos ...

1.py

something = 'something'

for i in xrange(1000000):
    if something == 'this':
        the_thing = 1
    elif something == 'that':
        the_thing = 2
    elif something == 'there':
        the_thing = 3
    else:
        the_thing = 4

2.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    the_thing = options.get(something, 4)

3.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    if something in options:
        the_thing = options[something]
    else:
        the_thing = 4

4.py

from collections import defaultdict

something = 'something'
options = defaultdict(lambda: 4, {'this': 1, 'that': 2, 'there': 3})

for i in xrange(1000000):
    the_thing = options[something]

... e observe a quantidade de tempo de CPU que eles usam ...

1.py: 160ms
2.py: 170ms
3.py: 110ms
4.py: 100ms

... usando o tempo do usuário de time(1).

A opção nº 4 tem a sobrecarga de memória adicional de adicionar um novo item para cada falha de chave distinta, então se você está esperando um número ilimitado de falhas de chave distintas, eu escolheria a opção nº 3, que ainda é uma melhoria significativa em a construção original.

Aya
fonte
2
o python tem uma instrução switch?
nathan hayfield
ugh ... bem, até agora, essa é a única coisa que ouvi sobre python e não me interessa ... acho que deve haver algo
nathan hayfield
2
-1 Você diz que usar a dicté mais lento, mas então seus tempos mostram que é a segunda opção mais rápida.
Marcin
11
@Marcin, estou dizendo que dict.get()é mais lento, que é 2.py- o mais lento de todos.
Aya de
Para o registro, três e quatro também são dramaticamente mais rápidos do que capturar o erro de chave em uma construção try / except.
Jeff de
78

Eu criaria um dicionário:

options = {'this': doThis,'that' :doThat, 'there':doThere}

Agora use apenas:

options.get(something, doThisMostOfTheTime)()

Se somethingnão for encontrado no optionsdicionário, então dict.getretornará o valor padrãodoThisMostOfTheTime

Algumas comparações de tempo:

Roteiro:

from random import shuffle
def doThis():pass
def doThat():pass
def doThere():pass
def doSomethingElse():pass
options = {'this':doThis, 'that':doThat, 'there':doThere}
lis = range(10**4) + options.keys()*100
shuffle(lis)

def get():
    for x in lis:
        options.get(x, doSomethingElse)()

def key_in_dic():
    for x in lis:
        if x in options:
            options[x]()
        else:
            doSomethingElse()

def if_else():
    for x in lis:
        if x == 'this':
            doThis()
        elif x == 'that':
            doThat()
        elif x == 'there':
            doThere()
        else:
            doSomethingElse()

Resultados:

>>> from so import *
>>> %timeit get()
100 loops, best of 3: 5.06 ms per loop
>>> %timeit key_in_dic()
100 loops, best of 3: 3.55 ms per loop
>>> %timeit if_else()
100 loops, best of 3: 6.42 ms per loop

Para 10**5chaves inexistentes e 100 chaves válidas:

>>> %timeit get()
10 loops, best of 3: 84.4 ms per loop
>>> %timeit key_in_dic()
10 loops, best of 3: 50.4 ms per loop
>>> %timeit if_else()
10 loops, best of 3: 104 ms per loop

Portanto, para um dicionário normal, a verificação da chave key in optionsé a maneira mais eficiente aqui:

if key in options:
   options[key]()
else:
   doSomethingElse()
Ashwini Chaudhary
fonte
options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()é marginalmente mais eficiente.
Aya de
Idéia legal, mas não tão legível. Além disso, você provavelmente deseja separar o optionsdicionário para evitar reconstruí-lo, movendo assim parte (mas não toda) da lógica para longe do ponto de uso. Ainda assim, belo truque!
Anders Johansson
7
você sabe se isso é mais eficiente? Meu palpite é que é mais lento, pois está fazendo uma pesquisa de hash em vez de uma verificação condicional simples ou três. A questão é mais sobre eficiência do que compactação do código.
Bryan Oakley de
2
@BryanOakley Eu adicionei algumas comparações de tempo.
Ashwini Chaudhary de
1
na verdade, deve ser mais eficiente de fazer try: options[key]() except KeyError: doSomeThingElse()(já que if key in options: options[key]()você está pesquisando duas vezes no dicionário porkey
hardmooth
8

Você consegue usar o pypy?

Manter o código original, mas executá-lo em pypy, dá uma aceleração de 50x para mim.

CPython:

matt$ python
Python 2.6.8 (unknown, Nov 26 2012, 10:25:03)
[GCC 4.2.1 Compatible Apple Clang 3.0 (tags/Apple/clang-211.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from timeit import timeit
>>> timeit("""
... if something == 'this': pass
... elif something == 'that': pass
... elif something == 'there': pass
... else: pass
... """, "something='foo'", number=10000000)
1.728302001953125

Pypy:

matt$ pypy
Python 2.7.3 (daf4a1b651e0, Dec 07 2012, 23:00:16)
[PyPy 2.0.0-beta1 with GCC 4.2.1] on darwin
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``a 10th of forever is 1h45''
>>>>
>>>> from timeit import timeit
>>>> timeit("""
.... if something == 'this': pass
.... elif something == 'that': pass
.... elif something == 'there': pass
.... else: pass
.... """, "something='foo'", number=10000000)
0.03306388854980469
Foz
fonte
Oi Foz. Obrigado pela dica. Na verdade, já estou usando o pypy (adorei), mas ainda preciso de melhorias na velocidade .. :)
kramer65
Ah bem! Antes disso, tentei pré-calcular um hash para 'isto', 'aquilo' e 'ali' - e depois comparar códigos hash em vez de strings. Isso acabou sendo duas vezes mais lento que o original, então parece que as comparações de strings já estão muito bem otimizadas internamente.
foz
3

Aqui está um exemplo de um if com condições dinâmicas traduzido para um dicionário.

selector = {lambda d: datetime(2014, 12, 31) >= d : 'before2015',
            lambda d: datetime(2015, 1, 1) <= d < datetime(2016, 1, 1): 'year2015',
            lambda d: datetime(2016, 1, 1) <= d < datetime(2016, 12, 31): 'year2016'}

def select_by_date(date, selector=selector):
    selected = [selector[x] for x in selector if x(date)] or ['after2016']
    return selected[0]

É uma forma, mas pode não ser a forma mais pitônica de fazê-lo, porque é menos legível para quem não é fluente em Python.

Arthur julião
fonte
0

As pessoas alertam execpor motivos de segurança, mas este é um caso ideal para isso.
É uma máquina de estado fácil.

Codes = {}
Codes [0] = compile('blah blah 0; nextcode = 1')
Codes [1] = compile('blah blah 1; nextcode = 2')
Codes [2] = compile('blah blah 2; nextcode = 0')

nextcode = 0
While True:
    exec(Codes[nextcode])
user3319934
fonte