Найефективніший спосіб зробити заяву if-elif-elif-else, коли інше зроблено найбільше?


99

У мене є оператор if-elif-elif-else, в якому 99% часу виконується оператор else:

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

Ця конструкція робиться багато , але оскільки вона перевершує кожну умову, перш ніж потрапляє на інше, я відчуваю, що це не дуже ефективно, не кажучи вже про пітонічне. З іншого боку, йому потрібно знати, чи виконується будь-яка з цих умов, тому слід все-таки перевірити це.

Хтось знає, чи можна і як це зробити ефективніше, чи це просто найкращий спосіб зробити це?


Чи можете ви sortті речі, якими ви керуєте, ланцюг if / else ..., так що всі елементи, для яких відповідатиме одна з умов, знаходяться в одному кінці, а всі інші - в іншому? Якщо так, ви можете побачити, чи це швидше / елегантніше чи ні. Але пам’ятайте, що якщо не виникає проблем з продуктивністю, переживати за оптимізацію зарано.
Паташу


4
Чи є щось спільне у трьох особливих випадках? Наприклад, ви можете зробити if not something.startswith("th"): doThisMostOfTheTime()і зробити інше порівняння в elseпункті.
Тім Піцкер

3
@ kramer65 Якщо це такий довгий ланцюжок if / elif ..., це може бути повільним, але переконайтеся, що насправді профільний код і почніть, оптимізуючи будь-яку частину, яка займає найбільше часу.
jorgeca

1
Чи ці порівняння проводяться лише один раз на значення something, або подібні порівняння проводяться кілька разів на одне значення?
Кріс Пітман

Відповіді:


98

Код...

options.get(something, doThisMostOfTheTime)()

... схоже, що це повинно бути швидше, але насправді повільніше, ніж if... elif... elseконструкція, тому що вона повинна викликати функцію, яка може бути значною продуктивністю накладних витрат в тісному циклі.

Розглянемо ці приклади ...

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]

... і відзначте кількість використовуваного процесора часу ...

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

... використовуючи час користувача від time(1).

Варіант №4 має додатковий обсяг пам'яті для додавання нового елемента для кожної пропущеної чіткої клавіші, тому якщо ви очікуєте необмеженої кількості чітко пропущених ключів, я б пішов із варіантом №3, який все ще значно покращить оригінальна конструкція.


2
у python є оператор перемикача?
nathan hayfield

Тьфу ... ну поки що це єдине, що я чув про python, про який мені все одно ... здогадуюсь, щось повинно було бути
nathan hayfield

2
-1 Ви говорите, що використання a dictповільніше, але тоді ваші таймінги фактично показують, що це другий найшвидший варіант.
Марцін

11
@Marcin Я кажу, що dict.get()це повільніше, а це 2.py- найповільніше з усіх.
Айя

Для запису, три та чотири також значно швидше, ніж фіксація помилки ключа у спробі / крім конструкції.
Джефф

78

Я б створив словник:

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

Тепер використовуйте лише:

options.get(something, doThisMostOfTheTime)()

Якщо somethingв optionsдиктаті не знайдеться, то dict.getповерне значення за замовчуваннямdoThisMostOfTheTime

Деякі порівняння часу:

Сценарій:

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

Результати:

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

Для 10**5неіснуючих ключів та 100 дійсних ключів ::

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

Отже, для звичайного словника перевірка використання ключа key in optionsє найбільш ефективним способом тут:

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

options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()незначно ефективніше.
Айя

Класна ідея, але не така читабельна. Крім того, ви, мабуть, хочете відокремити optionsдікт, щоб уникнути його перебудови, тим самим перемістивши частину (але не всю) логіки далеко від точки використання. Все-таки приємний трюк!
Андерс Йоханссон

7
чи знаєте ви , чи це ефективніше? Я здогадуюсь, це повільніше, оскільки він робить хеш-пошук, а не просту умовну перевірку чи три. Питання полягає в ефективності, а не компактності коду.
Брайан Оуклі

2
@BryanOakley Я додав кілька порівнянь часу.
Ashwini Chaudhary

1
насправді це повинно бути ефективніше try: options[key]() except KeyError: doSomeThingElse()(оскільки if key in options: options[key]()ви два рази шукаєте словникkey
hardmooth

8

Чи можете ви використовувати pypy?

Якщо зберегти свій початковий код, але запустити його на pypy, для мене буде 50-кратне прискорення.

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

Привіт Фоз. Дякую за пораду. Насправді я вже використовую pypy (люблю це), але мені все одно потрібні покращення швидкості .. :)
kramer65

Що ж, добре! Перед цим я спробував попередньо обчислити хеш для "цього", "того" та "там", а потім порівняв хеш-коди замість рядків. Це виявилося вдвічі повільніше, ніж оригінал, тому, схоже, порівняння рядків уже досить добре оптимізовано внутрішньо.
foz

3

Ось приклад if із динамічними умовами, переведеними до словника.

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]

Це спосіб, але може бути не самим пітонічним способом це зробити, тому що він менш читабельний, для кого не вільно володіє Python.


0

Люди попереджають про це execз міркувань безпеки, але це ідеальний випадок для цього.
Це легка державна машина.

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])
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.