Самый эффективный способ сделать оператор if-elif-elif-else, когда else выполняется больше всего?

99

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

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

Эта конструкция выполняется много раз , но поскольку она перебирает все условия, прежде чем попадет в else, я чувствую, что это не очень эффективно, не говоря уже о Pythonic. С другой стороны, ему действительно нужно знать, выполняется ли какое-либо из этих условий, поэтому он все равно должен его протестировать.

Кто-нибудь знает, можно ли и как это сделать более эффективно, или это просто лучший способ сделать это?

kramer65
источник
Можете ли вы использовать sortте вещи, на которых работает ваша цепочка if / else ..., чтобы все элементы, которым соответствует одно из условий, находились на одном конце, а все остальные - на другом? Если это так, вы можете увидеть, быстрее / элегантнее это или нет. Но помните, если нет проблем с производительностью, об оптимизации волноваться рано.
Patashu
4
Есть ли что-то общее в этих трех особых случаях? Например, вы можете сделать if not something.startswith("th"): doThisMostOfTheTime()еще одно сравнение в elseпредложении.
Тим Пицкер
3
@ kramer65 Если это такая длинная цепочка if / elif ... это может быть медленным, но убедитесь, что вы действительно профилируете свой код и начните с оптимизации той части, которая занимает больше всего времени.
jorgeca
1
Эти сравнения выполняются только один раз для каждого значения somethingили аналогичные сравнения выполняются несколько раз для одного и того же значения?
Крис Питман

Ответы:

99

Код...

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 есть оператор switch?
Натан Хейфилд
тьфу ... ну пока что это единственное, что я слышал о питоне, который меня не волнует ... думаю, что-то должно было быть
натан хейфилд
2
-1 Вы говорите, что использовать a dictмедленнее, но тогда ваши тайминги фактически показывают, что это второй самый быстрый вариант.
Marcin
11
@Marcin Я говорю, что dict.get()это медленнее, а это 2.py- самый медленный из всех.
Aya
Для записи, три и четыре также значительно быстрее, чем захват ключевой ошибки в конструкции try / except.
Джефф
78

Я бы создал словарь:

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

Теперь используйте только:

options.get(something, doThisMostOfTheTime)()

Если somethingне найден в optionsdict, то 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]()немного более эффективен.
Aya
Классная идея, но не такая читабельная. Также вы, вероятно, захотите разделить optionsdict, чтобы избежать его перестройки, тем самым перемещая часть (но не всю) логики подальше от точки использования. Тем не менее, хороший трюк!
Андерс Йоханссон
7
знаете ли вы , эффективнее ли это? Я предполагаю, что это медленнее, поскольку он выполняет поиск хэша, а не простую условную проверку или три. Вопрос скорее в эффективности, чем в компактности кода.
Bryan Oakley
2
@BryanOakley Я добавил несколько сравнений по времени.
Ашвини Чаудхари
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
foz
источник
Привет, Фоз. Спасибо за чаевые. На самом деле я уже использую 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])
user3319934
источник