Десять уловок для ускорения написания кода Python

Небольшое улучшение на каждом шагу, большой скачок в целом

Python медленный.

Готов поспорить, вы можете много раз сталкиваться с этим контраргументом по поводу использования Python, особенно от людей из мира C, C++ или Java. Это верно во многих случаях, например, перебор или сортировка массивов, списков или словарей Python иногда может быть медленным. В конце концов, Python разработан для того, чтобы программировать было весело и легко. Таким образом, улучшения краткости и читабельности кода Python должны сопровождаться снижением производительности.

При этом в последние годы было приложено много усилий для улучшения производительности Python. Теперь мы можем эффективно обрабатывать большие наборы данных, используя numpy, scipy, pandas и numba, поскольку все эти библиотеки реализовали свои критические пути кода в C/C++. Есть еще один интересный проект, Pypy project, который ускоряет код Python в 4,4 раза по сравнению с Cpython (оригинальная реализация Python).

Обратной стороной Pypy является то, что охват некоторых научно-популярных модулей (например, Matplotlib, Scipy) ограничен или отсутствует, что означает, что вы не можете использовать эти модули в коде, предназначенном для Pypy.

Помимо этих внешних ресурсов, что мы можем сделать, чтобы ускорить код Python в нашей повседневной практике кодирования? Сегодня я поделюсь с вами 10 приемами, которые я часто использовал в процессе изучения Python.

Как обычно, если вы хотите перезапустить коды в этом посте самостоятельно, все необходимые данные и записную книжку можно получить из моего Github.

1. Знаком со встроенными функциями

Python имеет множество встроенных функций, реализованных в C, которые работают очень быстро и хорошо обслуживаются (рис. 1). Мы должны хотя бы ознакомиться с этими именами функций и знать, где их найти (некоторые часто используемые функции, связанные с вычислениями: abs(), len(), max(), min(), set(), sum()). Поэтому всякий раз, когда нам нужно выполнить простое вычисление, мы можем выбрать правильный ярлык вместо того, чтобы неуклюже писать нашу собственную версию.

Давайте воспользуемся встроенными функциями set() и sum() в качестве примеров. Как вы можете видеть на рисунке 2, это в 36,1 и 20,9 раз быстрее при использовании set() и sum(), чем функции, написанные нами, соответственно.

2. sort() vs. sorted()

Обе функции могут сортировать список.

Если мы просто хотим получить отсортированный список и не заботимся об исходном списке, sort() немного быстрее, чем sorted(), как для базовой сортировки, так и при использовании key параметров (параметр key указывает функцию, которая должна быть вызывается для каждого элемента списка перед сравнением), как показано на рисунке 3.

Это связано с тем, что метод sort() изменяет список на месте, в то время как sorted() создает новый отсортированный список и сохраняет исходный список нетронутым. Другими словами, порядок значений внутри самого a_long_list фактически уже изменился.

Однако sorted() более универсален по сравнению с sort(). Это связано с тем, что sorted() принимает любую итерацию, в то время как sort() определяется только для списков. Следовательно, если мы хотим отсортировать что-то, кроме списка, sorted() - это правильная функция. Например, мы можем быстро отсортировать словарь по keys или по values (рисунок 4).

3. Используйте символы вместо их имен

Как показано на рисунке 5, когда нам нужен пустой словарь или объект списка, вместо использования dict() или list() мы можем напрямую вызвать {} (что касается пустого набора, нам нужно использовать сам set()) и []. Этот трюк не обязательно может ускорить коды, но сделает их более питоническими.

4. Понимание списка

Обычно, когда нам нужно создать новый список из старого списка на основе определенных правил, мы используем цикл for для перебора старого списка и преобразования его значений на основе правила и сохранения в новом списке. Например, предположим, что мы хотим найти все четные числа из another_long_list, мы можем использовать следующие коды:

even_num = []
for number in another_long_list:
    if number % 2 == 0:
        even_num.append(number)

Однако есть более лаконичный и элегантный способ добиться этого. Как показано на рисунке 6, мы помещаем исходный цикл for всего в одну строку кода. Причем скорость улучшилась почти в 2 раза.

В сочетании с правилом 3 мы также можем превратить список в словари или наборы, просто изменив [] на {}. Давайте перепишем коды на рисунке 5, мы можем пропустить этап присваивания и завершить итерацию внутри символа, как это sorted_dict3 = {key: value for key, value in sorted(a_dict.items(), key=lambda item: item[1])}.

Чтобы разбить это, начните с конца. Функция «sorted(a_dict.items(), key=lambda item: item[1])» вернула нам список кортежей (рисунок 4). Здесь мы использовали несколько назначений для распаковки кортежа, так как для каждого кортежа внутри списка мы присвоили key его первому элементу и value его второму элементу (как мы знаем, в каждом кортеже есть два элемента в этом случае). Наконец, каждая пара key и value хранилась в словаре.

5. Используйте enumerate() для значения и индекса

Иногда, когда мы перебираем список, мы хотим использовать в выражениях как его значения, так и индексы. Как показано на рисунке 7, мы должны использовать enumerate(), который превращает значения списка в пары индекса и значения. Это также ускорит наш код примерно в 2 раза.

6. Используйте zip() для упаковки и распаковки нескольких итераций

В некоторых случаях нам потребуется перебрать два или более списков. Затем мы можем использовать функцию zip(), которая преобразует несколько списков в один список кортежей (рисунок 8). Обратите внимание, что списки лучше иметь одинаковую длину, в противном случае zip() останавливается, как только заканчивается более короткий список.

И наоборот, чтобы получить доступ к элементам в каждом кортеже в списке, мы также можем распаковать список кортежей, добавив звездочку (*) и используя несколько назначений, например, letters1, numbers1 = zip(*pairs_list).

7. Объедините set() и in

Когда мы хотим проверить, существует ли значение внутри списка или нет, неуклюжий способ - создать такую ​​функцию:

# Construct a function for membership test
def check_membership(n):
    for element in another_long_list:
        if element == n:
            return True
    return False

Затем позвоните check_membership(value), чтобы узнать, есть ли value внутри another_long_list. Однако питонический способ сделать это - просто использовать in, вызвав value in another_long_list, как показано на рисунке 9. Это точно так же, как вы буквально спрашиваете Python: «Эй, питон, не могли бы вы сказать мне, value внутри another_long_list".

Для большей эффективности мы должны сначала удалить дубликаты из списка с помощью set(), а затем проверить принадлежность к заданному объекту. Таким образом мы сократили количество элементов, которые необходимо проверить. Вдобавок in - это очень быстрая операция на наборах по замыслу.

Как видно из рисунка 9, даже несмотря на то, что для создания заданного объекта потребовалось 20 мс, это всего лишь одноразовое вложение, а сам шаг проверки использовал только 5,2 мкс. Это улучшение в 1962 раза.

8. Проверьте, верна ли переменная

Неизбежно мы будем использовать множество операторов if для проверки пустых переменных, пустых списков, пустых словарей и т. Д. Отсюда мы также можем сэкономить немного времени.

Как показано на рисунке 10, нам не нужно явно указывать == True или is True в операторе if, вместо этого мы просто используем имя переменной. Это экономит ресурсы, используемые волшебной функцией __eq__ для сравнения значений с обеих сторон.

Точно так же, если нам нужно проверить, пуста ли переменная, нам просто нужно сказать if not string_returned_from_function:.

9. Для подсчета уникальных значений используйте Counters()

Допустим, мы пытаемся подсчитать уникальные значения в списке, созданном в Правиле 1, a_long_list. Один из способов - создать словарь, в котором ключи являются числами, а значения - счетчиками. По мере того, как мы перебираем список, мы можем увеличивать его счетчик, если он уже есть в словаре, и добавлять его в словарь, если его нет.

num_counts = {}
for num in a_long_list:
    if num in num_counts:
        num_counts[num] += 1
    else:
        num_counts[num] = 1

Однако более эффективный способ сделать это - просто использовать Counter() из коллекций в одной строке кода, num_counts2 = Counter(a_long_list). Да, это так просто. Как показано на рисунке 11, это примерно в 10 раз быстрее, чем написанная нами функция.

Если мы хотим узнать 10 наиболее распространенных чисел, экземпляр Counter() также имеет очень удобный метод most_common.

Одним словом, коллекции - замечательный модуль, мы должны сохранить его в нашем ежедневном наборе инструментов и использовать при любой возможности.

10. Поместите for цикл внутри функции

Может быть, когда-то мы создали функцию, и нам нужно было повторить эту функцию заданное количество раз. Очевидный способ состоит в том, что мы создаем функцию, а затем помещаем эту функцию в цикл for.

Однако, как показано на рисунке 12, вместо того, чтобы многократно выполнять функцию 1 миллион раз (длина a_long_list составляет 1000000), мы интегрировали цикл for внутри функции. Это сэкономило нам около 22% рабочего времени. Это связано с тем, что вызовы функций дороги, избегайте этого, записывая функцию в понимание списка - лучший выбор.

Это все! Спасибо, что прочитали этот пост. Надеюсь, что некоторые хитрости могут быть вам полезны. Кроме того, какие еще подходы вы использовали для ускорения кода Python? Буду очень признателен, если вы поделитесь ими, оставив комментарий.

Вот ссылки, которые могут вас заинтересовать:

Как всегда, я приветствую отзывы, конструктивную критику и информацию о ваших проектах в области науки о данных. Со мной можно связаться в Linkedin, а также на моем сайте.