Der effizienteste Weg, eine if-elif-elif-else-Aussage zu machen, wenn das else am meisten erledigt ist?


99

Ich habe eine in if-elif-elif-else-Anweisung, in der 99% der Zeit die else-Anweisung ausgeführt wird:

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

Dieses Konstrukt wird viel gemacht , aber da es alle Bedingungen durchläuft, bevor es die anderen trifft, habe ich das Gefühl, dass dies nicht sehr effizient ist, geschweige denn Pythonic. Auf der anderen Seite muss es wissen, ob eine dieser Bedingungen erfüllt ist, also sollte es es trotzdem testen.

Weiß jemand, ob und wie dies effizienter gemacht werden könnte oder ist dies einfach der bestmögliche Weg, dies zu tun?


Können Sie sortdie Dinge, auf denen Sie Ihre if / else ... -Kette ausführen, so dass alle Elemente, für die eine der Bedingungen übereinstimmt, an einem Ende und alle anderen am anderen Ende sind? Wenn ja, könnten Sie sehen, ob das schneller / eleganter ist oder nicht. Denken Sie jedoch daran, dass es zu früh ist, sich über die Optimierung Gedanken zu machen, wenn es keine Leistungsprobleme gibt.
Patashu


4
Gibt es etwas, das die drei Sonderfälle gemeinsam haben? Sie können beispielsweise if not something.startswith("th"): doThisMostOfTheTime()einen weiteren Vergleich in der elseKlausel durchführen.
Tim Pietzcker

3
@ kramer65 Wenn es eine so lange Kette von if / elif ist ... könnte es langsam sein, aber stellen Sie sicher, dass Sie Ihren Code tatsächlich profilieren und optimieren Sie zunächst den Teil, der die meiste Zeit in Anspruch nimmt.
Jorgeca

1
Werden diese Vergleiche nur einmal pro Wert von durchgeführt somethingoder werden ähnliche Vergleiche mehrmals mit demselben Wert durchgeführt?
Chris Pitman

Antworten:


98

Der Code...

options.get(something, doThisMostOfTheTime)()

... sieht so aus, als ob es schneller sein sollte, aber es ist tatsächlich langsamer als das if... elif... elseKonstrukt, weil es eine Funktion aufrufen muss, was in einer engen Schleife einen erheblichen Leistungsaufwand bedeuten kann.

Betrachten Sie diese Beispiele ...

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]

... und notieren Sie die benötigte CPU-Zeit ...

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

... mit der Benutzerzeit von time(1).

Option 4 hat den zusätzlichen Speicheraufwand für das Hinzufügen eines neuen Elements für jeden einzelnen Schlüsselfehler. Wenn Sie also eine unbegrenzte Anzahl unterschiedlicher Schlüsselfehler erwarten, würde ich Option 3 wählen, die immer noch eine erhebliche Verbesserung darstellt das ursprüngliche Konstrukt.


2
Hat Python eine switch-Anweisung?
Nathan Hayfield

ugh ... nun, bis jetzt ist das das einzige, was ich über Python gehört habe, das mir egal ist ... denke, es musste etwas geben
Nathan Hayfield

2
-1 Sie sagen, dass die Verwendung von a dictlangsamer ist, aber dann zeigen Ihre Timings tatsächlich, dass es die zweitschnellste Option ist.
Marcin

11
@Marcin Ich sage, das dict.get()ist langsamer, was ist 2.py- das langsamste von allen.
Aya

Für die Aufzeichnung sind drei und vier auch dramatisch schneller als das Erfassen des Schlüsselfehlers in einem try / Except-Konstrukt.
Jeff

78

Ich würde ein Wörterbuch erstellen:

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

Verwenden Sie jetzt nur:

options.get(something, doThisMostOfTheTime)()

Wird somethingim optionsDiktat nicht gefunden, dict.getwird der Standardwert zurückgegebendoThisMostOfTheTime

Einige Timing-Vergleiche:

Skript:

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

Ergebnisse:

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

Für 10**5nicht vorhandene Schlüssel und 100 gültige Schlüssel:

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

Für ein normales Wörterbuch ist die Überprüfung des Schlüssels key in optionshier am effizientesten:

if key in options:
   options[key]()
else:
   doSomethingElse()

options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()ist geringfügig effizienter.
Aya

Coole Idee, aber nicht so lesbar. Außerdem möchten Sie wahrscheinlich das optionsDiktat trennen , um zu vermeiden, dass es neu erstellt wird, wodurch ein Teil (aber nicht die gesamte) Logik weit vom Verwendungsort entfernt wird. Trotzdem schöner Trick!
Anders Johansson

7
Wissen Sie , ob dies effizienter ist? Ich vermute, es ist langsamer, da es eher eine Hash-Suche als eine einfache bedingte Prüfung oder drei durchführt. Die Frage betrifft eher die Effizienz als die Kompaktheit des Codes.
Bryan Oakley

2
@BryanOakley Ich habe einige Timing-Vergleiche hinzugefügt.
Ashwini Chaudhary

1
Eigentlich sollte es effizienter sein try: options[key]() except KeyError: doSomeThingElse()(da if key in options: options[key]()Sie das Wörterbuch zweimal key
durchsuchen

8

Können Sie Pypy verwenden?

Wenn Sie Ihren Originalcode behalten, ihn aber auf Pypy ausführen, beschleunigt sich das für mich um das 50-fache.

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

Hallo Foz. Danke für den Tipp. Tatsächlich benutze ich bereits Pypy (liebe es), aber ich brauche noch Geschwindigkeitsverbesserungen .. :)
kramer65

Naja! Vorher habe ich versucht, einen Hash für 'dies', 'das' und 'dort' vorab zu berechnen - und dann Hash-Codes anstelle von Zeichenfolgen zu vergleichen. Das stellte sich als doppelt so langsam heraus wie das Original, so dass es aussieht, als wären String-Vergleiche intern bereits ziemlich gut optimiert.
Foz

3

Hier ein Beispiel für ein if mit dynamischen Bedingungen, das in ein Wörterbuch übersetzt wurde.

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]

Dies ist ein Weg, aber möglicherweise nicht der pythonischste, da er weniger lesbar ist und für den Python nicht fließend ist.


0

Die Leute warnen execaus Sicherheitsgründen, aber dies ist ein idealer Fall dafür.
Es ist eine einfache Zustandsmaschine.

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])
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.