Как улучшить навыки работы с Python в 2023 году

Python  —  самый распространенный язык программирования в области науки о данных, и его популярность продолжает расти. За последние годы сфера влияния науки о данных существенно возросла.

Дата-сайентист часто работает с большими объемами данных. По этой причине ему нужно писать эффективный код с точки зрения времени выполнения и памяти. Код Python также должен быть хорошо структурирован и легко читаем. Эти семь советов помогут писать эффективный и читабельный код Python.


Ускорьте NumPy пакетом NumExpr

NumPy  —  это библиотека Python для эффективной работы с массивами, которая также предлагает быстрые и оптимизированные векторные операции. Однако она не поддерживает параллельную обработку. В качестве альтернативы NumPy вы можете использовать NumExpr.

NumExpr достигает значительно большей производительности, чем NumPy, поскольку поддерживает многопоточность. Кроме того, NumExpr позволяет не выделять память для промежуточных результатов.

Сначала установите пакеты NumPy и NumExpr:

$ pip install numpy numexpr

Посмотрите этот пример и попробуйте его выполнить.

import numpy as np
import numexpr as ne
import timeit

var1 = np.random.random(2**27)
var2 = np.random.random(2**27)

%timeit np.sin(var1) / np.cos(var2)
# 2.73 с

%timeit ne.evaluate("sin(var1) / cos(var2)")
# 566 мс

Оператор выполняется примерно в 5 раз быстрее с помощью NumExpr. Если хотите ускорить выполнение операторов NumPy, NumExpr предоставит такую возможность.

NumExpr эффективнее всего работает с большими массивами, а максимальную производительность развивает на мощном компьютере с большим количеством ядер. Рекомендуем использовать NumExpr при наличии этих двух условий. Для операций с небольшими массивами можно применять NumPy, так как разница в производительности будет минимальной. Дело в том, что NumExpr разбивает операнды массива на небольшие фрагменты. Эти фрагменты легко помещаются в кэш процессора и распределяются между доступными ядрами процессора, что позволяет выполнять их параллельно.

Чтобы узнать больше о NumExpr, ознакомьтесь с его репозиторием на GitHub.


Освойте быструю альтернативу метода pandas apply()

Метод pandas apply() может выполнять функции вдоль оси датафрейма. Многие программисты используют метод apply() в сочетании с лямбда-функциями. Но как увеличить производительность этого метода?

Используйте пакет swifter. Он очень быстро применяет функции к датафреймам и сериям. Если pandas apply() работает на одном ядре, то swifter обеспечивает поддержку нескольких ядер.

Для начала установите пакет swifter:

$ pip install swifter

После установки пакета можете опробовать его.

import pandas as pd
import numpy as np
import swifter
import timeit

df = pd.DataFrame({'a': np.random.randint(7, 10, 10**7)})

# pandas apply()
%timeit df.apply(lambda x: x**7)
# 54 мс

# swifter.apply()
%timeit df.swifter.apply(lambda x: x**7)
# 37.5 мс

Этот простой пример показывает, что метод swifter.apply() отличается более коротким временем выполнения. Разница особенно заметна на мощных компьютерах с несколькими ядрами. Если вам нужно повысить производительность проекта, обратите внимание на пакет swifter.

Используйте встроенные функции Python

Бывает так, что вы реализуете функцию, не зная, что она уже встроена в Python. Особенно часто это случается с теми, кто пришел из других языков программирования, таких как C и C++. 

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

import numpy as np
from time import perf_counter

result_list = []
company_list = ["Tesla", "Block", "Palantir", "Apple"]
company_list_sample = np.repeat(company_list, 10**7)

start = perf_counter()
for company in company_list_sample:
result_list.append(company.lower())
print(perf_counter()-start)
# 17.13 с

start = perf_counter()
result_list = map(str.lower, company_list_sample)
print(perf_counter()-start)
# 0.97 с

В приведенном выше коде мы воспроизводим список из четырех записей 10 миллионов раз, поэтому получаем список из 40 миллионов записей. Затем мы преобразуем строки в списке в нижний регистр. Как видите, встроенная функция работает примерно в 17 раз быстрее. Так что используйте встроенные функции! Это увеличивает производительность, особенно при работе с большими объемами данных.

Существует и множество других встроенных функций, таких как min(), max(), all() и т. д.

Используйте списки вместо циклов

Программисты часто используют списки в сочетании с циклами для хранения результатов вычислений. Однако такой подход неэффективен с точки зрения времени выполнения. По этой причине эффективнее использовать списковое выражение. Следующий пример демонстрирует разницу в производительности.

import numpy as np
from time import perf_counter

result_list_loop = []
result_list_com = []

number_round = 10000000

start = perf_counter()
for i in range(number_round):
result_list_loop.append(i*i)
print(perf_counter()-start)
# 1.47 с

start = perf_counter()
result_list_com = [i*i for i in range(number_round)]
print(perf_counter()-start)
# 0.69 с

print(result_list_com[10])
# 100

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

Списковое выражение начинается с открывающей скобки [. Затем идет вычисление из цикла for. После  —  заголовок цикла, состоящий из трех элементов (ключевое слово for, переменная run, длина цикла). Список заканчивается закрывающей скобкой ]. Освоив этот синтаксис, вы сможете писать for-циклы гораздо компактнее.

Но как насчет использования памяти? Как уменьшить объем расходуемой памяти? Это особенно целесообразно при работе с большими списками, с которыми придется выполнять дальнейшие операции. В нашем примере в списке сохраняется 10 миллионов значений. Но нужно ли хранить непосредственно все записи или они будут нужны только по мере необходимости?

В таких случаях можно использовать генераторы списков. Генератор создает элемент списка по мере необходимости. В результате генератор требует меньше памяти и показывает лучшее время работы. Взгляните на следующий пример.

import sys 
from time import perf_counter

print(sys.getsizeof(result_list_com), 'bytes')
# 89095160 байт

start = perf_counter()
result_gen = (i*i for i in range(number_round))
print(perf_counter()-start)
# 0.22 мс

print(sys.getsizeof(result_gen), 'bytes')
# 112 байт

print(list(result_gen)[10])
# 100

Мы можем выполнить все операции, как в предыдущем примере. Единственное отличие заключается в том, что вместо [] используется (). Вместо списка  —  храним генератор. Такой подход более эффективен для памяти. Подумайте, можно ли использовать в ваших проектах списковое выражение или генераторы. Они могут повысить производительность и уменьшить объем расходуемой памяти.

Объединяйте словари с помощью синтаксиса двойной звездочки **

Как объединить словари? Это можно сделать с помощью одной строки, используя синтаксис двойной звездочки **. Следующий пример покажет, как это работает.

dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}

dict_merged = {**dict_1, **dict_2}
print(dict_merged)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}

Прежде всего, определим два словаря с одинаковыми и разными парами “ключ-значение”. Компания Tesla была основана в 2003 году, поэтому второй словарь (dict_2) является более актуальным. Если оба словаря содержат одинаковые ключи и разные значения, то используется значение последнего словаря. После слияния новый словарь содержит все три пары “ключ-значение”. Синтаксис лаконичен и компактен, поэтому объединять словари очень просто. А самое приятное в том, что можно объединить три и более словарей. Этот прием может сэкономить много времени.

Другим подходом является метод update. Этот метод обновляет первый словарь и не создает его копию. Взгляните на следующий пример.

dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}

dict_1.update(dict_2)
print(dict_1)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}

Недостатком метода update является то, что можно использовать только один словарь для обновления. Если в будущем захотите объединить словари, помните об этом.

Не импортируйте ненужные модули

Возможно, вы слышали этот совет много раз, но он может значительно повысить производительность кода. Нет необходимости импортировать целые библиотеки  —  обычно достаточно лишь нескольких функций. Код также долго запускается, потому что сначала нужно импортировать всю библиотеку. Этого не должно происходить. Кроме того, затем приходится обращаться к отдельным функциям через точечную нотацию. Это очень неэффективно: необходимо избегать точечной нотации. Следующий пример демонстрирует это.

import math
from time import perf_counter

start = perf_counter()
variable = math.exp(7)
print(perf_counter()-start)
# 8.47-05 с

В этом примере используем функцию math.exp() с точечной нотацией. Это приводит к низкой производительности кода. Кроме того, мы импортировали всю библиотеку math, хотя нам нужна только функция exp().

from math import exp
from time import perf_counter

start = perf_counter()
variable = exp(7)
print(perf_counter()-start)
# 4.51-05 с

В данном примере импортируем функцию exp() без точечной нотации. Используя этот трюк, можно вдвое сократить время выполнения кода. 

Используйте компилятор just-in-time

Numba  —  это компилятор just-in-time (jit), который хорошо работает с циклами, массивами и функциями NumPy. Декораторы используются для указания Numba компилировать определенные функции. Numba компилирует декорированные функции в машинный код точно в срок, так что весь код или его часть выполняется со скоростью нативного машинного кода.

Сначала установите Numba с помощью pip.

pip install numba

После успешной установки можно использовать Numba. Взгляните на следующий пример:

import numpy as np
from numba import jit
import timeit

var = np.random.random(10**7)
num_loop = 10000

def foo(var):
result = 0
for i in range(num_loop):
result += 1
result = np.sin(var) + np.cos(var)
return result
%timeit foo(var)
# 154 мс

@jit(nopython=True)
def foo_numba(var):
result = 0
for i in range(num_loop):
result += 1
result = np.sin(var) + np.cos(var)
return result

%timeit foo_numba(var)
# 76.3 мс

Как видите, вышеупомянутый декоратор функции foo ускоряет код. Декоратор nopython=True указывает, что компиляция будет выполняться без участия интерпретатора Python. Numba ускоряет выполнение цикла и тригонометрических функций NumPy. Однако она не может использоваться со всеми функциями Python.

Ниже перечислены преимущества и недостатки Numba.

Минусы:

  • Numba не поддерживает pandas.
  • Неподдерживаемый код выполняется посредством интерпретатора и влечет дополнительные накладные расходы Numba.
  • Только неофициальная поддержка на M1/Arm64.

Плюсы:

  • Отличная поддержка массивов и функций NumPy, а также циклов.
  • Обладает поддержкой Nvidia CUDA и может применяться для разработки нейронных сетей на основе NumPy.

Минусы и плюсы показывают, что Numba следует использовать в первую очередь для операций с NumPy. Однако в начале всегда следует проверить, подходит ли Numba для соответствующей реализации.


Заключение

Вы узнали о том, как повысить эффективность кода с точки зрения времени выполнения и сохранения памяти. Выделим главные моменты.

  • NumPy не поддерживает параллельную обработку. Для этого можно использовать NumExpr.
  • Функция Pandas apply() может быть ускорена с помощью swifter.
  • Проверьте, есть ли встроенные функции.
  • Используйте списковое выражение вместо циклов. Посмотрите, подходят ли генераторы списков для вашего проекта.
  • Объединяйте массивы с помощью синтаксиса двойной звездочки **.
  • Не импортируйте ненужные модули.
  • Если возникают проблемы со временем выполнения, можно использовать компиляторы just-in-time. Компиляторы just-in-time ускоряют работу кода.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Patrick Tinz: Speed Up your Python Skills in 2023

Предыдущая статья4 ключевых аспекта проектирования распределенных систем
Следующая статьяУправление состоянием в React: обзор