Самый эффективный способ сделать заявление if-elif-elif-else, когда else будет сделано больше всего?
У меня есть оператор if-elif-elif-else, в котором 99% времени выполняется оператор else:
if something == 'this':
doThis()
elif something == 'that':
doThat()
elif something == 'there':
doThere()
else:
doThisMostOfTheTime()
Эта конструкция сделана очень много, но, поскольку она проходит через все условия, прежде чем она достигнет другого, у меня возникает ощущение, что это не очень эффективно, не говоря уже о Pythonic. С другой стороны, ему необходимо знать, выполнено ли какое-либо из этих условий, поэтому оно должно проверить его в любом случае.
Кто-нибудь знает, если и как это можно сделать более эффективно, или это просто лучший способ сделать это?
9 ответов
Код...
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, который по-прежнему является значительным улучшением оригинальная конструкция.
Я бы создал словарь:
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()
Вы можете использовать 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
Это пример 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.
Я пробовал использовать оператор match, представленный в python 3.10:
5.py
something = 'something'
for i in range(10000000):
match something:
case "this":
the_thing = 1
case "that":
the_thing = 2
case "there":
the_thing = 3
case _:
the_thing = 4
Вот результаты, которые я получаю с 3.10.0:
1.py: 1.4s
2.py: 0.9s
3.py: 0.7s
4.py: 0.7s
5.py: 1.0s
Я думал, что получу что-то похожее на 1 .py, но это намного быстрее.
вы можете имитировать if-elif-else с типом switch-case, например, используя словарь и лямбда-функцию
Например:
x = 5
y = 5
operator = 'add'
def operation(operator, x, y):
return {
'add': lambda: x+y,
'sub': lambda: x-y,
'mul': lambda: x*y,
'div': lambda: x/y
}.get(operator, lambda: None)()
result = operation(operator, x, y)
print(result)
Люди предупреждают о 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])
Недавно я столкнулся с подходом, альтернативным "вложенному if else", который сокращает время работы моей функции с 2,5 часов до ~2 минут.. Баам! Давай начнем:
Ранее Код
bin = lambda x:"Неизвестно", если x==0 else("Высокий", если x>75, else ("Средний", если x>50 и x<=75, else ("Средний_низкий", если x>25 и x<=50 иначе "Низкий")))
col.apply(bin) Время ~2,5 часа
Оптимизировать код
Определите словарь, альтернативный вложению, если еще def dict_function(*args):
'Pass in a list of tuples, which will be key/value pairs'
ret = {}
for k,v in args:
for i in k:
ret[i] = v
return ret
Dict = dict_function(([0],"Unknown"),(range(1,25),"Low"),(range(25,50),"Medium_Low"),(range(50,75),"Medium"),(range(75,100),"High"))
col.apply(lambda x:Dict[x])
dict_function создает несколько пар key_value для заданного диапазона. Время ~2 минуты
Недавно у меня была такая же проблема, хотя и не в отношении производительности, но мне не нравится «API» создания функций и ручного добавления их в dict. Я хотел API, похожий на
functools.singledispatch
, но для отправки на основе значений, а не типов. Так ...
def value_dispatch(func):
"""value-dispatch function decorator.
Transforms a function into a function, that dispatches its calls based on the
value of the first argument.
"""
funcname = getattr(func, '__name__')
registry = {}
def dispatch(arg):
"""return the function that matches the argument"""
return registry.get(arg, func)
def register(arg):
def wrapper(func):
"""register a function"""
registry[arg] = func
return func
return wrapper
def wrapper(*args, **kwargs):
if not args:
raise ValueError(f'{funcname} requires at least 1 positional argument')
return dispatch(args[0])(*args, **kwargs)
wrapper.register = register
wrapper.dispatch = dispatch
wrapper.registry = registry
return wrapper
Используйте так:
@value_dispatch
def handle_something():
print("default")
@handle_something.register(1)
def handle_one():
print("one")
handle_something(1)
handle_something(2)
PS: Я создал сниппет на Gitlab для справки.