Frontend-разработчик в Foquz.
https://www.cat-in-web.ru/
Сравниваем скорость выполнения распространенных (но не очень эффективных) решений и по-настоящему производительного кода на чистом Python без привлечения сторонних библиотек.
«Питон – медленный». Наверняка вы не раз сталкивались с этим утверждением, особенно от людей, пришедших в Python из C, C++ или Java. Во многих случаях это верно. Циклы или сортировка массивов, списков или словарей иногда действительно работают медленно. В конце концов, главная миссия Python – сделать программирование приятным и легким. Ради лаконичности и удобочитаемости пришлось отчасти принести в жертву производительность.
Тем не менее, в последние годы предпринято немало усилий для решения проблемы. Теперь мы можем эффективно обрабатывать большие наборы данных с помощью NumPy, SciPy, Pandas и numba, поскольку все эти библиотеки написаны на C/C++. Еще один интересный проект – PyPy ускоряет код Python в 4.4 раза по сравнению с CPython (оригинальная реализация Python).
Недостаток PyPy – нет поддержки некоторых популярных модулей, например, Matplotlib, SciPy.
Но ускорить Python можно и без внешних библиотек. В наших силах разогнать его с помощью полезных трюков, используемых в повседневной практике кодинга.
1. Стандартные функции
В Python много работающих очень быстро реализованных на C встроенных функций. Они покрывают большинство тривиальных вычислительных операций (abs, min, max, len, sum). Хороший разработчик должен их знать, чтобы в подходящем месте не изобретать неуклюжие велосипеды, а брать надёжное стандартное решение. Возьмём в качестве примеров встроенные функции set() и sum(). Сравним их работу с кастомными реализациями того же функционала.
Пример для set():
Пример для sum():
Стандартные варианты в 36 (set) и 20 (sum) раз быстрее, чем функции, написанные самим разработчиком.
2. sort() vs sorted()
Если нам просто нужен отсортированный список, при этом неважно, что будет с оригиналом, sort() будет работать немного быстрее, чем sorted(). Это справедливо для базовой сортировки:
Справедливо и для сортировки с использованием ключа – параметра key, который определяет сортировочную функцию:
Так происходит потому, что метод sort() изменяет список прямо на месте, в то время как sorted() создает новый отсортированный список, сохраняя исходный нетронутым. Другими словами, порядок значений внутри a_long_list фактически уже изменился.
Однако функция sorted() более универсальна. Она может работать с любой итерируемой структурой. Поэтому, если нужно отсортировать, например, словарь (по ключам или по значениям), придется использовать sorted():
3. Литералы вместо функций
Когда нужен пустой словарь или список, вместо dict() или list(), можно напрямую вызвать {} и [] (для пустого множества все еще нужна функция set()). Этот прием не обязательно ускорит ваш код, но сделает его более "pythonic".
4. Генераторы списков
Обычно, когда требуется создать новый список из старого на основе определенных условий, мы используем цикл for – итерируем все значения и сохраняем нужные в новом списке.
Например, отберём все чётные числа из списка another_long_list:
Но есть более лаконичный и элегантный способ сделать то же самое. Код цикла for можно сократить до одной-единственной строки с помощью генератора списка, выиграв при этом в скорости почти в два раза:
Сочетая это правило с Правилом #3 (использование литералов), мы легко можем превратить список в словарь или множество, просто изменив скобки:
Разберёмся в коде:
Выражение sorted(a_dict.items(), key=lambda item: item[1]) возвращает список кортежей [('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)].
Далее мы распаковываем кортежи и присваиваем первый элемент каждого кортежа в переменную key, а второй – в переменную value.
Наконец, сохраняем каждую пару key-value в словаре.
5. enumerate() для значения и индекса
Иногда при переборе списка нужны и значения, и их индексы. Чтобы вдвое ускорить код используйте enumerate() для превращения списка в пары индекс-значение:
6. zip() для перебора нескольких списков
В некоторых случаях приходится перебирать более одного списка. Для ускорения операции рекомендуется использовать функцию zip(), которая преобразует их в общий итератор кортежей:
Обратите внимание, списки должны быть одинаковой длины, так как функция zip() останавливается, когда заканчивается более короткий список.
И наоборот, чтобы получить доступ к элементам каждого кортежа, мы можем распаковать список кортежей, добавив звездочку (*) и используя множественное присваивание:
7. Комбинация set() и in
Если нужно проверить, содержит ли список некоторое значение, можно написать такую неуклюжую функцию:
Однако есть более характерный для Python способ сделать это – использовать оператор in:
Повысить эффективность можно предварительным удалением из списка дубликатов с помощью set. Таким образом, мы сократим количество элементов для проверки. Кроме того, оператор in очень быстро работает с множествами.
Преобразование списка в множество заняло 20 мс. Но это одноразовые затраты. Зато сама проверка заняла 5 мкс – то есть в 2 тыс. раз меньше, что становится важным при частых обращениях.
8. Проверка на True
Практически в любой программе необходимо проверять, являются ли переменные/списки/словари/... пустыми. На этих проверках тоже можно немножко сэкономить.
Не следует явно указывать == True или is True в условии if, достаточно указать имя проверяемой переменной. Это экономит ресурсы, которые использует «магическая» функция __eq__ для сравнения значений.
Аналогично можно проверять обратное условие, добавив оператор not:
9. Подсчет уникальных значений с Counter()
Если нам необходимо подсчитать количество уникальных значений в списке, можно, например, создать словарь, в котором ключи – это значения списка, а значения – счетчик встречаемости.
Однако более эффективный способ для решения этой задачи – использование Counter() из модуля collections. Весь код при этом уместится в одной строчке:
Этот фрагмент будет работать примерно в 10 раз быстрее, чем предыдущий.
У Counter также есть удобный метод most_common, позволяющий получить самые часто встречающиеся значения:
Одним словом, collections – это замечательный модуль, который должен быть в базовом наборе инструментов любого Python-разработчика. Не поленитесь прочитать наше руководство по применению модуля.
10. Цикл for внутри функции
Представьте, что вы создаете функцию, которую нужно повторить некоторое количество раз. Очевидный способ решения этой задачи – помещение функции внутрь цикла for.
Однако правильнее будет перевернуть конструкцию – и поместить цикл внутрь функции.
В данном примере для миллиона итераций (длина a_long_list) мы сэкономили около 22% времени.
***
Будем рады, если вы поделитесь в комментариях своими подходами к ускорению кода в Python. Вот ещё несколько статей, которые могут вас заинтересовать:
Комментарии