¿La forma más eficiente de hacer una declaración if-elif-elif-else cuando más se hace el else?

99

Tengo una declaración if-elif-elif-else en la que el 99% de las veces, se ejecuta la declaración else:

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

Esta construcción se hace mucho , pero dado que repasa todas las condiciones antes de llegar a las demás, tengo la sensación de que no es muy eficiente, y mucho menos Pythonic. Por otro lado, necesita saber si se cumple alguna de esas condiciones, por lo que debería probarlo de todos modos.

¿Alguien sabe si esto se podría hacer de manera más eficiente y cómo hacerlo o es simplemente la mejor manera de hacerlo?

kramer65
fuente
¿Pueden sortlas cosas en las que está ejecutando su if / else ... encadenar, de modo que todos los elementos con los que una de las condiciones coincidirá estén en un extremo y el resto en el otro? Si es así, puede ver si es más rápido / más elegante o no. Pero recuerde, si no hay problemas de rendimiento, es demasiado pronto para preocuparse por la optimización.
Patashu
4
¿Hay algo que los tres casos especiales tengan en común? Por ejemplo, podría hacer if not something.startswith("th"): doThisMostOfTheTime()y hacer otra comparación en la elsecláusula.
Tim Pietzcker
3
@ kramer65 Si es una cadena tan larga de if / elif ... podría ser lento, pero asegúrese de perfilar su código y comience optimizando cualquier parte que requiera más tiempo.
jorgeca
1
¿Se realizan estas comparaciones solo una vez por valor de something, o se realizan comparaciones similares varias veces en el mismo valor?
Chris Pitman

Respuestas:

98

El código...

options.get(something, doThisMostOfTheTime)()

... parece que debería ser más rápido, pero en realidad es más lento que el if... elif... elseconstructo, porque tiene que llamar a una función, que puede ser una sobrecarga de rendimiento significativa en un bucle cerrado.

Considere estos ejemplos ...

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]

... y observe la cantidad de tiempo de CPU que usan ...

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

... usando el tiempo de usuario de time(1).

La opción n. ° 4 tiene la sobrecarga de memoria adicional de agregar un nuevo elemento por cada error de clave distinto, por lo que si espera una cantidad ilimitada de errores de clave distintos, optaría por la opción n. ° 3, que sigue siendo una mejora significativa en la construcción original.

Aya
fuente
2
¿Python tiene una declaración de cambio?
Nathan Hayfield
ugh ... bueno, hasta ahora eso es lo único que he escuchado sobre Python que no me importa ... supongo que seguramente habrá algo
Nathan Hayfield
2
-1 Dices que usar a dictes más lento, pero tus tiempos realmente muestran que es la segunda opción más rápida.
Marcin
11
@Marcin Estoy diciendo que dict.get()es más lento, que es 2.pyel más lento de todos.
Aya
Para el registro, tres y cuatro también son dramáticamente más rápidos que capturar el error clave en una construcción try / except.
Jeff
78

Crearía un diccionario:

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

Ahora usa solo:

options.get(something, doThisMostOfTheTime)()

Si somethingno se encuentra en el optionsdict dict.get, devolverá el valor predeterminadodoThisMostOfTheTime

Algunas comparaciones de tiempo:

Guión:

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**5claves inexistentes y 100 claves 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

Entonces, para un diccionario normal, la verificación de la clave key in optionses la forma más eficiente aquí:

if key in options:
   options[key]()
else:
   doSomethingElse()
Ashwini Chaudhary
fuente
options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()es marginalmente más eficiente.
Aya
Buena idea, pero no tan legible. También es probable que desee separar el optionsdict para evitar reconstruirlo, moviendo así parte (pero no toda) de la lógica lejos del punto de uso. Aún así, ¡buen truco!
Anders Johansson
7
¿ Sabes si esto es más eficiente? Supongo que es más lento ya que está haciendo una búsqueda de hash en lugar de una simple verificación condicional o tres. La pregunta es sobre la eficiencia en lugar de la compacidad del código.
Bryan Oakley
2
@BryanOakley He agregado algunas comparaciones de tiempo.
Ashwini Chaudhary
1
en realidad, debería ser más eficiente hacerlo try: options[key]() except KeyError: doSomeThingElse()(ya if key in options: options[key]()que está buscando en el diccionario dos veceskey
hardmooth
8

¿Puedes usar pypy?

Mantener su código original pero ejecutarlo en pypy me da una aceleración de 50 veces.

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
fuente
Hola Foz. Gracias por el consejo. De hecho, ya estoy usando pypy (me encanta), pero todavía necesito mejoras de velocidad .. :)
kramer65
¡Oh bien! Antes de esto, intenté precalcular un hash para 'esto', 'eso' y 'allí', y luego comparé códigos hash en lugar de cadenas. Resultó ser dos veces más lento que el original, por lo que parece que las comparaciones de cadenas ya están bastante bien optimizadas internamente.
foz
3

Aquí un ejemplo de un if con condiciones dinámicas traducido a un diccionario.

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]

Es una forma, pero puede que no sea la forma más pitónica de hacerlo porque es menos legible para quien no domina Python con fluidez.

Arthur Julião
fuente
0

La gente advierte execpor razones de seguridad, pero este es un caso ideal para ello.
Es una 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])
usuario3319934
fuente