Il modo più efficiente di fare un'istruzione if-elif-elif-else quando l'altro è fatto di più?


99

Ho un'istruzione in if-elif-elif-else in cui il 99% delle volte viene eseguita l'istruzione else:

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

Questo costrutto è fatto molto , ma poiché supera ogni condizione prima di colpire il resto, ho la sensazione che non sia molto efficiente, figuriamoci Pythonic. D'altra parte, ha bisogno di sapere se una di queste condizioni è soddisfatta, quindi dovrebbe comunque testarla.

Qualcuno sa se e come questo potrebbe essere fatto in modo più efficiente o è semplicemente il miglior modo possibile per farlo?


Puoi sortincatenare le cose su cui stai eseguendo il tuo if / else ..., in modo tale che tutti gli elementi per i quali una delle condizioni corrisponderà siano a un'estremità e tutto il resto all'altra? Se è così, potresti vedere se è più veloce / più elegante o meno. Ma ricorda, se non ci sono problemi di prestazioni, è troppo presto per preoccuparsi dell'ottimizzazione.
Patashu


4
C'è qualcosa che accomuna i tre casi speciali? Ad esempio, potresti fare if not something.startswith("th"): doThisMostOfTheTime()e fare un altro confronto nella elseclausola.
Tim Pietzcker

3
@ kramer65 Se è una catena così lunga di if / elif ... potrebbe essere lenta, ma assicurati di profilare effettivamente il tuo codice e iniziare ottimizzando qualsiasi parte richieda più tempo.
jorgeca

1
Questi confronti vengono eseguiti solo una volta per valore di somethingoppure confronti simili vengono eseguiti più volte sullo stesso valore?
Chris Pitman

Risposte:


98

Il codice...

options.get(something, doThisMostOfTheTime)()

... sembra che dovrebbe essere più veloce, ma in realtà è più lento del costrutto if... elif... else, perché deve chiamare una funzione, che può essere un significativo sovraccarico delle prestazioni in un ciclo stretto.

Considera questi esempi ...

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]

... e nota la quantità di tempo della CPU che usano ...

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

... utilizzando l'ora utente da time(1).

L'opzione n. 4 ha l'overhead di memoria aggiuntivo di aggiungere un nuovo elemento per ogni chiave mancata distinta, quindi se ti aspetti un numero illimitato di mancate chiavi distinte, sceglierei l'opzione n. 3, che è ancora un miglioramento significativo su il costrutto originale.


2
python ha un'istruzione switch?
nathan hayfield

ugh ... beh finora questa è l'unica cosa che ho sentito su Python che non mi interessa ... immagino che ci sarebbe stato qualcosa
nathan hayfield

2
-1 Dici che usare a dictè più lento, ma i tuoi tempi mostrano effettivamente che è la seconda opzione più veloce.
Marcin

11
@ Marcin sto dicendo che dict.get()è più lento, ovvero 2.pyil più lento di tutti.
Aya

Per la cronaca, tre e quattro sono anche notevolmente più veloci di catturare l'errore chiave in un costrutto try / tranne.
Jeff

78

Creerei un dizionario:

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

Ora usa solo:

options.get(something, doThisMostOfTheTime)()

Se somethingnon viene trovato nel optionsdict dict.get, restituirà il valore predefinitodoThisMostOfTheTime

Alcuni confronti temporali:

Script:

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

Risultati:

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

Per 10**5chiavi inesistenti e 100 chiavi valide:

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

Quindi, per un normale dizionario, il controllo della chiave key in optionsè il modo più efficiente qui:

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

options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()è leggermente più efficiente.
Aya

Bella idea, ma non così leggibile. Inoltre probabilmente vorrai separare il optionsdict per evitare di ricostruirlo, spostando così parte (ma non tutta) della logica lontano dal punto di utilizzo. Comunque, bel trucco!
Anders Johansson

7
si fa a sapere se questo è più efficiente? La mia ipotesi è che sia più lento poiché sta eseguendo una ricerca hash piuttosto che un semplice controllo condizionale o tre. La domanda riguarda l'efficienza piuttosto che la compattezza del codice.
Bryan Oakley

2
@BryanOakley ho aggiunto alcuni confronti temporali.
Ashwini Chaudhary

1
in realtà dovrebbe essere più efficiente da fare try: options[key]() except KeyError: doSomeThingElse()(dato if key in options: options[key]()che stai cercando due volte nel dizionario perkey
hardmooth

8

Sei in grado di usare pypy?

Mantenere il codice originale ma eseguirlo su pypy mi dà una velocità 50x per me.

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

Ciao Foz. Grazie per il consiglio. In effetti sto già usando pypy (lo adoro), ma ho ancora bisogno di miglioramenti della velocità .. :)
kramer65

Oh bene! Prima di questo ho provato a pre-calcolare un hash per "questo", "quello" e "lì" e quindi confrontare i codici hash invece delle stringhe. Si è rivelato due volte più lento dell'originale, quindi sembra che i confronti tra stringhe siano già abbastanza ben ottimizzati internamente.
foz

3

Ecco un esempio di un if con condizioni dinamiche tradotto in un dizionario.

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]

È un modo, ma potrebbe non essere il modo più pitonico per farlo perché è meno leggibile per chi non parla fluentemente Python.


0

Le persone mettono in guardia execper motivi di sicurezza, ma questo è un caso ideale.
È una semplice macchina a stati.

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])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.