Najbardziej efektywny sposób tworzenia instrukcji if-elif-elif-else, gdy wykonano najwięcej w inny sposób?

99

Mam instrukcję if-elif-elif-else, w której w 99% przypadków wykonywana jest instrukcja else:

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

Ta konstrukcja jest wykonywana bardzo dużo , ale ponieważ sprawdza każdy warunek, zanim trafi w inny, mam wrażenie, że nie jest to zbyt wydajne, nie mówiąc już o Pythonie. Z drugiej strony musi wiedzieć, czy któryś z tych warunków jest spełniony, więc i tak powinien go przetestować.

Czy ktoś wie, czy i jak można to zrobić skuteczniej, czy też jest to po prostu najlepszy możliwy sposób?

kramer65
źródło
Czy możesz sorturuchomić łańcuch if / else ... w taki sposób, że wszystkie elementy, do których będzie pasował jeden z warunków, znajdują się na jednym końcu, a cała reszta na drugim? Jeśli tak, możesz sprawdzić, czy to jest szybsze / bardziej eleganckie, czy nie. Pamiętaj jednak, że jeśli nie ma problemu z wydajnością, jest zbyt wcześnie, aby martwić się optymalizacją.
Patashu
4
Czy jest coś, co łączy te trzy szczególne przypadki? Na przykład możesz zrobić if not something.startswith("th"): doThisMostOfTheTime()i zrobić inne porównanie w elseklauzuli.
Tim Pietzcker
3
@ kramer65 Jeśli jest to taki długi łańcuch if / elif ... może być wolny, ale upewnij się, że faktycznie profilujesz swój kod i zacznij od optymalizacji dowolnej części, która zajmuje najwięcej czasu.
jorgeca
1
Czy te porównania są wykonywane tylko raz dla danej wartości something, czy też podobne porównania są wykonywane wielokrotnie na tej samej wartości?
Chris Pitman

Odpowiedzi:

98

Kod...

options.get(something, doThisMostOfTheTime)()

... wygląda na to, że powinno być szybsze, ale w rzeczywistości jest wolniejsze niż if... elif... elsekonstrukcja, ponieważ musi wywołać funkcję, co może być znaczącym narzutem wydajności w ciasnej pętli.

Rozważ te przykłady ...

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]

... i zwróć uwagę, ile czasu procesora używają ...

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

... używając czasu użytkownika od time(1).

Opcja nr 4 wiąże się z dodatkowym narzutem pamięci polegającym na dodawaniu nowego elementu dla każdego wyraźnego chybienia klawisza, więc jeśli spodziewasz się nieograniczonej liczby wyraźnych chybień klawiszy, wybrałbym opcję nr 3, która wciąż jest znaczącym ulepszeniem oryginalna konstrukcja.

Aya
źródło
2
czy python ma instrukcję switch?
Nathan Hayfield
ugh ... cóż, do tej pory to jedyna rzecz, jaką słyszałem o Pythonie, na której mi nie zależy ... chyba coś
musiało
2
-1 Mówisz, że używanie a dictjest wolniejsze, ale wtedy twoje czasy faktycznie pokazują, że jest to druga najszybsza opcja.
Marcin
11
@Marcin Mówię, że dict.get()jest wolniej, czyli 2.pynajwolniej ze wszystkich.
Aya
Dla przypomnienia, trzy i cztery są również znacznie szybsze niż przechwytywanie błędu klucza w konstrukcji try / except.
Jeff
78

Stworzyłbym słownik:

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

Teraz użyj tylko:

options.get(something, doThisMostOfTheTime)()

Jeśli somethingnie zostanie znaleziony w optionsdict dict.get, zwróci wartość domyślnądoThisMostOfTheTime

Niektóre porównania czasu:

Scenariusz:

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

Wyniki:

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

W 10**5przypadku nieistniejących kluczy i 100 ważnych kluczy:

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

Tak więc, w przypadku normalnego słownika, sprawdzanie klucza key in optionsjest tutaj najbardziej wydajnym sposobem:

if key in options:
   options[key]()
else:
   doSomethingElse()
Ashwini Chaudhary
źródło
options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()jest nieznacznie bardziej wydajny.
Aya
Fajny pomysł, ale nie tak czytelny. Prawdopodobnie chciałbyś również oddzielić optionsdyktando, aby uniknąć jego przebudowy, przenosząc w ten sposób część (ale nie całość) logiki daleko od punktu użycia. Mimo wszystko niezła sztuczka!
Anders Johansson
7
pan wiedzieć , czy to jest bardziej wydajny? Domyślam się, że jest wolniejszy, ponieważ wykonuje wyszukiwanie hash, a nie proste sprawdzenie warunkowe lub trzy. Pytanie dotyczy raczej wydajności niż zwartości kodu.
Bryan Oakley
2
@BryanOakley Dodałem kilka porównań czasowych.
Ashwini Chaudhary
1
w rzeczywistości powinno to być bardziej wydajne try: options[key]() except KeyError: doSomeThingElse()(ponieważ if key in options: options[key]()przeszukujesz słownik dwa razykey
hardmooth
8

Czy możesz używać pypy?

Zachowanie oryginalnego kodu, ale uruchomienie go na pypy daje mi 50-krotne przyspieszenie.

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
źródło
Cześć Foz. Dzięki za wskazówkę. Właściwie używam już pypy (uwielbiam to), ale nadal potrzebuję poprawy szybkości .. :)
kramer65
No cóż! Wcześniej próbowałem wstępnie obliczyć skrót dla „tego”, „tamtego” i „tam” - a następnie porównać kody skrótów zamiast ciągów. Okazało się, że było to dwa razy wolniejsze niż oryginał, więc wygląda na to, że porównania ciągów są już dość dobrze zoptymalizowane wewnętrznie.
foz
3

Oto przykład if z warunkami dynamicznymi przetłumaczonymi na słownik.

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]

Jest to sposób, ale może nie być najbardziej pythonowy, ponieważ jest mniej czytelny dla osób nieposiadających biegle w Pythonie.

Arthur Julião
źródło
0

Ludzie ostrzegają execze względów bezpieczeństwa, ale to jest do tego idealny przypadek.
To prosta maszyna stanowa.

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
źródło