Le moyen le plus efficace de faire une instruction if-elif-elif-else lorsque l'autre est le plus utilisé?

99

J'ai une instruction in if-elif-elif-else dans laquelle 99% du temps, l'instruction else est exécutée:

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

Cette construction est beaucoup faite , mais comme elle passe en revue toutes les conditions avant qu'elle ne touche l'autre, j'ai le sentiment que ce n'est pas très efficace, encore moins Pythonic. D'un autre côté, il a besoin de savoir si l'une de ces conditions est remplie, il devrait donc le tester de toute façon.

Est-ce que quelqu'un sait si et comment cela pourrait être fait plus efficacement ou est-ce simplement la meilleure façon de le faire?

kramer65
la source
Pouvez-vous sortles choses sur lesquelles vous exécutez votre chaîne if / else ..., de telle sorte que tous les éléments auxquels l'une des conditions correspondra soient à une extrémité et tous les autres à l'autre? Si c'est le cas, vous pouvez voir si c'est plus rapide / plus élégant ou non. Mais rappelez-vous, s'il n'y a pas de problème de performances, il est trop tôt pour s'inquiéter de l'optimisation.
Patashu
4
Y a-t-il quelque chose que les trois cas particuliers ont en commun? Par exemple, vous pouvez faire if not something.startswith("th"): doThisMostOfTheTime()et faire une autre comparaison dans la elseclause.
Tim Pietzcker
3
@ kramer65 Si c'est une si longue chaîne de if / elif ... cela pourrait être lent, mais assurez-vous de profiler votre code et commencez par optimiser la partie qui prend le plus de temps.
jorgeca
1
Ces comparaisons sont-elles effectuées une seule fois par valeur de something, ou des comparaisons similaires sont-elles effectuées plusieurs fois sur la même valeur?
Chris Pitman

Réponses:

99

Le code...

options.get(something, doThisMostOfTheTime)()

... semble devoir être plus rapide, mais il est en fait plus lent que la construction if... elif... else, car il doit appeler une fonction, ce qui peut être une surcharge de performances significative dans une boucle serrée.

Considérez ces exemples ...

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]

... et notez la quantité de temps CPU qu'ils utilisent ...

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

... en utilisant le temps utilisateur de time(1) .

L'option n ° 4 a la surcharge de mémoire supplémentaire liée à l'ajout d'un nouvel élément pour chaque clé manquante distincte, donc si vous vous attendez à un nombre illimité de clés manquées distinctes, j'irais avec l'option n ° 3, qui est toujours une amélioration significative sur la construction originale.

Aya
la source
2
python a-t-il une instruction switch?
nathan hayfield
euh ... eh bien jusqu'à présent, c'est la seule chose que j'ai entendue à propos de python que je m'en fous ... je suppose qu'il y avait forcément quelque chose
nathan hayfield
2
-1 Vous dites que l'utilisation de a dictest plus lente, mais votre minutage montre en fait que c'est la deuxième option la plus rapide.
Marcin
11
@Marcin Je dis que dict.get()c'est plus lent, qui est 2.py- le plus lent de tous.
Aya
Pour mémoire, trois et quatre sont également considérablement plus rapides que la capture de l'erreur clé dans une construction try / except.
Jeff
78

Je créerais un dictionnaire:

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

Maintenant, utilisez simplement:

options.get(something, doThisMostOfTheTime)()

Si somethingn'est pas trouvé dans le optionsdict alors dict.getretournera la valeur par défautdoThisMostOfTheTime

Quelques comparaisons de temps:

Scénario:

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()

Résultats:

>>> 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

Pour 10**5les clés inexistantes et 100 clés valides:

>>> %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

Donc, pour un dictionnaire normal, vérifier la clé en utilisant key in optionsest le moyen le plus efficace ici:

if key in options:
   options[key]()
else:
   doSomethingElse()
Ashwini Chaudhary
la source
options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()est légèrement plus efficace.
Aya
Bonne idée, mais pas aussi lisible. Vous voudrez probablement aussi séparer le optionsdict pour éviter de le reconstruire, déplaçant ainsi une partie (mais pas la totalité) de la logique loin du point d'utilisation. Encore, belle astuce!
Anders Johansson
7
vous savez si cela est plus efficace? Je suppose que c'est plus lent car il fait une recherche de hachage plutôt qu'une simple vérification conditionnelle ou trois. La question porte sur l'efficacité plutôt que sur la compacité du code.
Bryan Oakley
2
@BryanOakley J'ai ajouté quelques comparaisons de temps.
Ashwini Chaudhary
1
en fait, cela devrait être plus efficace à faire try: options[key]() except KeyError: doSomeThingElse()(car avec if key in options: options[key]()vous recherchez le dictionnaire deux fois pourkey
hardmooth
8

Pouvez-vous utiliser pypy?

Garder votre code d'origine mais l'exécuter sur pypy me donne une accélération de 50x.

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
la source
Salut Foz. Merci pour le conseil. En fait, j'utilise déjà pypy (j'adore), mais j'ai encore besoin d'améliorations de vitesse .. :)
kramer65
Tant pis! Avant cela, j'ai essayé de pré-calculer un hachage pour «ceci», «cela» et «là» - puis comparer des codes de hachage au lieu de chaînes. Cela s'est avéré être deux fois plus lent que l'original, il semble donc que les comparaisons de chaînes sont déjà assez bien optimisées en interne.
foz
3

Voici un exemple de if avec des conditions dynamiques traduites dans un dictionnaire.

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]

C'est un moyen, mais ce n'est peut-être pas le moyen le plus pythonique de le faire car il est moins lisible pour qui ne parle pas couramment Python.

Arthur Julião
la source
0

Les gens mettent en garde execpour des raisons de sécurité, mais c'est un cas idéal pour cela.
C'est une machine à états facile.

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
la source