В 20 тысяч раз быстрее: 5 приемов для ускорения кода на Python
Несмотря на то, что Python — один из самых популярных языков программирования в мире, он не лишен недостатков. Самый большой из них, о котором, вероятно, известно всем — это скорость. О пяти способах улучшить Python-код в блоге на Dice рассказал разработчик программного обеспечения Дэвид Болтон.
Примечание: автор протестировал способы на Python 3.7 и 3.9, а чтобы вести отсчет времени в наносекундах, использовал функцию perf_counter_ns из пакета time
.
Простыми словами, 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-код почти в пять раз быстрее обычного.
Метод ускорения медленных функций. Это способ оптимизации, при котором сохраняется результат выполнения функции, и этот результат используется при следующем вызове. Если повторные вызовы функций выполняются с одинаковыми параметрами, можно сохранить предыдущие значения вместо повторения ненужных вычислений.
В пакете functool
s есть lru_cache
, который можно использовать для оформления функции, которую нужно мемоизировать. В приведенном ниже примере:
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, включая 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-подход.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…