una - sentencias en python
La forma más eficiente de hacer una declaración if-elif-elif-else cuando se hace más lo demás? (4)
Tengo una instrucción if-elif-elif-else en la que el 99% de las veces se ejecuta la instrucción else:
if something == ''this'':
doThis()
elif something == ''that'':
doThat()
elif something == ''there'':
doThere()
else:
doThisMostOfTheTime()
Este constructo está hecho mucho , pero como repasa todas las condiciones antes de que llegue al otro, tengo la sensación de que no es muy eficiente, y mucho menos de Pythonic. Por otro lado, necesita saber si se cumple alguna de esas condiciones, por lo que debe probarlo de todos modos.
¿Alguien sabe si y cómo se puede hacer de manera más eficiente o es simplemente la mejor manera de hacerlo?
¿Eres capaz de usar pypy?
Mantener el código original pero ejecutarlo en pypy me da una velocidad 50 veces mayor.
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
Crearía un diccionario:
options = {''this'': doThis,''that'' :doThat, ''there'':doThere}
Ahora usa solo:
options.get(something, doThisMostOfTheTime)()
Si no se encuentra something
en el dict de options
entonces dict.get
devolverá el valor predeterminado doThisMostOfTheTime
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**5
claves 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
Por lo tanto, para un diccionario normal, verificar la clave usando key in options
es la forma más eficiente aquí:
if key in options:
options[key]()
else:
doSomethingElse()
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 constructo if
... elif
... else
, porque tiene que llamar a una función, que puede ser una sobrecarga de rendimiento significativa en un ciclo cerrado.
Considera 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 tenga en cuenta la cantidad de tiempo de CPU que utilizan ...
1.py: 160ms
2.py: 170ms
3.py: 110ms
4.py: 100ms
... usando el tiempo del usuario desde el time(1)
.
La opción n. ° 4 tiene la sobrecarga de memoria adicional de agregar un nuevo ítem por cada falla clave, así que si está esperando un número ilimitado de fallas de teclas distintas, iría con la opción n. ° 3, que sigue siendo una mejora significativa en la construcción original.
Ese es un ejemplo de un si con condiciones dinámicas traducidas 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 no ser la forma más pitónica de hacerlo, porque es menos legible para quienes no dominan Python.