В 20 тысяч раз быстрее: 5 приемов для ускорения кода на Python
Несмотря на то, что Python — один из самых популярных языков программирования в мире, он не лишен недостатков. Самый большой из них, о котором, вероятно, известно всем — это скорость. О пяти способах улучшить Python-код в блоге на Dice рассказал разработчик программного обеспечения Дэвид Болтон.
Примечание: автор протестировал способы на Python 3.7 и 3.9, а чтобы вести отсчет времени в наносекундах, использовал функцию perf_counter_ns из пакета time.
Pythonic
Простыми словами, Pythonic — стиль кода. Поэтому, говоря, что какой-либо код — pythonic — имеется в виду, что он написан в соответствии с идиомами Python. Это использование таких функций как map, sum и range, а также понимание списков и генераторов.
Например, нужно сравнить два способа подсчета всех целых чисел в диапазоне от 1-100, которые кратны 3:
from time import perf_counter_ns
def main():
# non pythonic
start=perf_counter_ns()
total=0
for i in range(1,100):
if (i %3)== 0:
total += i
end=perf_counter_ns()
print (f"Non-Pythonic Total of divisible by 3= {total}")
print(f"Time took {end-start}")
# pythonic
start=perf_counter_ns()
total =sum(range(1, 100, 3))
end=perf_counter_ns()
print (f"Pythonic Total of divisible by 3= {total}")
print(f"Time took {end-start}")
if __name__ == "__main__":
main()
Это дает нам:
Non-Pythonic Total of divisible by 3= 1683 Time took 13300 Pythonic Total of divisible by 3= 1683 Time took 2900
Это время второго прогона. Первые запуски были 14,500 и 3,000, то есть от 3,5% до 9% дольше. В данном случае Pythonic-код почти в пять раз быстрее обычного.
Мемоизация
Метод ускорения медленных функций. Это способ оптимизации, при котором сохраняется результат выполнения функции, и этот результат используется при следующем вызове. Если повторные вызовы функций выполняются с одинаковыми параметрами, можно сохранить предыдущие значения вместо повторения ненужных вычислений.
В пакете functools есть lru_cache, который можно использовать для оформления функции, которую нужно мемоизировать. В приведенном ниже примере:
- fib — это простая немемоизированная функция Фибоначчи;
- fib(35) делает много сложений;
- mfib — это мемоизированная версия.
from time import perf_counter_ns
from functools import lru_cache
def main():
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
@lru_cache(maxsize=None)
def mfib(n):
return n if n < 2 else mfib(n-1) + mfib(n-2)
start=perf_counter_ns()
print(f"Non-memoized fib()={fib(35)}")
end=perf_counter_ns()
print(f"Time took {end-start}")
start=perf_counter_ns()
print(f"Memoized fib()={mfib(35)}")
end=perf_counter_ns()
print(f"Time took {end-start}")
if __name__ == "__main__":
main()
Результат говорит сам за себя:
Non-memoized fib()=9227465 Time took 2905175700 Memoized fib()=9227465 Time took 148700
Код выполнился почти в 20 тысяч раз быстрее.
Кстати, разработчик Орен Тош считает, что код можно еще ускорить, используя подкласс Dictionary с методом __missing__ dunder.
Перевод кода на С
Это не всегда легко, так как для этого нужно знать язык C и то, как он взаимодействует с Python. Кроме того, может быть всего несколько случаев, когда кодинг на C поможет. Помогает то, что CPython написан на C.
В библиотеке ctypes есть библиотека типов C и их отображений в Python. Она также позволяет обращаться к библиотекам операционной системы, но нужно быть готовым работать на достаточно низком уровне и знать язык С, включая массивы, структуры и указатели.
Компиляция Python
Машинный код, который создается при компиляции кода, всегда будет работать быстрее, чем интерпретируемый байт-код. Есть несколько компиляторов Python, включая Numpa, Nuitka, pypi и Cython. Автор советует оптимизировать код Python, прежде чем пытаться компилировать его. Компилятор Numpa — JIT (Just-In-Time), который также обеспечивает ускорение на GPU.
Использование from
Можно постоянно использовать пакет import, но более разумно использовать from, когда можно импортировать только нужную функцию (или функции). Зачем импортировать 20 функций, если нужна только одна? Для таких коротких программ, как ниже, вероятно, разница не будет заметна, но с увеличением размера программы она станет очевидна:
from time import perf_counter_ns
def main():
start=perf_counter_ns()
n = 10
fact = 1
for i in range(1,n+1):
fact = fact * i
end=perf_counter_ns()
print (f"The factorial of {n} is {fact}")
print(f"Time took {end-start}")
if __name__ == "__main__":
main()
При первом запуске было получено 4200 наносекунд, а при последующих — около 3900. Не забывайте, что можно помещать импорты внутрь функций, чтобы они вызывались только тогда, когда это необходимо.
Заключение
Если нужно выделить какой-то один способ, автор склоняется к использованию мемоизации, которая дает максимальные показатели по скорости, но чтобы стать лучшим программистом на Python, желательно освоить Pythonic-подход.


Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: