Улучшите свои навыки работы на Python в 2023 году
Python является наиболее широко используемым языком программирования в области Data Science, и его популярность продолжает расти. За последние годы вся область Data Science значительно расширилась.
В этой статье мы дадим вам семь советов о том, как улучшить свои навыки работы на Python. Часто именно мелочи имеют большое значение. Эти советы обогатят вашу жизнь специалиста по работе с данными.
Как Data Science-специалисту, вам часто приходится иметь дело с большими объёмами данных. По этой причине вы должны эффективно программировать с точки зрения времени выполнения и памяти. Ваш код на Python также должен быть хорошо структурированным и лёгким для чтения. Эти советы помогут вам писать эффективный и читаемый код на Python.
Совет 1: Ускорьте NumPy
NumPy – это библиотека Python для эффективной работы с массивами. Она также предлагает быстрые и оптимизированные векторизованные операции. Но! Она не поддерживает параллельную обработку. В качестве альтернативы NumPy вы можете использовать NumExpr.
NumExpr обеспечивает значительно лучшую производительность, чем NumPy, поскольку она поддерживает многопоточность. Кроме того, она позволяет избежать выделения памяти для промежуточных результатов.
Во-первых, вы должны установить пакеты 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 s
%timeit ne.evaluate("sin(var1) / cos(var2)")
# 566 ms
Из примера мы можем увидеть, что процесс выполняется в 5 раз быстрее с помощью библиотеки NumExpr.
NumExpr лучше всего работает, когда у вас большие массивы. Эта библиотека также развивает максимальную производительность, если у вас мощный компьютер с большим количеством ядер. По этой причине мы рекомендуем использовать NumExpr при наличии этих двух условий. Для операций с небольшими массивами вы можете использовать NumPy, так как разница в производительности будет минимальна. Причина в том, что NumExpr разбивает операнды массива на небольшие фрагменты. Эти фрагменты легко помещаются в кэш процессора. Блоки распределяются между доступными ядрами центрального процессора, обеспечивая параллельное выполнение.
Если вы хотите узнать больше о NumExpr, ознакомьтесь с репозиторием NumExpr на GitHub.
Совет 2: Быстрая альтернатива функции 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 ms
# swifter.apply()
%timeit df.swifter.apply(lambda x: x**7)
# 37.5 ms
Этот простой пример показывает, что функция swifter.apply()
имеет более быстрое время выполнения. Разница особенно заметна на мощных компьютерах с несколькими ядрами. Если вам нужно повысить производительность в вашем следующем проекте, рассмотрите пакет swifter
.
Совет 3: Использование встроенных функций 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 s
start = perf_counter()
result_list = map(str.lower, company_list_sample)
print(perf_counter()-start)
# 0.97 s
В приведённом выше коде мы реплицируем список из четырех записей 10 миллионов раз, таким образом, мы получаем список из 40 миллионов записей. Затем мы преобразуем строки в списке в нижний регистр. Вы можете видеть, что встроенная функция работает примерно в 17 раз быстрее. Особенно при работе с большими объёмами данных, этот совет обеспечивает огромное повышение производительности. Так что используйте встроенные функции!
Есть еще много встроенных функций, таких как min(), max(), all()
и т.д. Проведите своё собственное исследование, если вам нужна конкретная функция Python. Это того стоит!
Совет 4: Используйте абстракцию списков вместо циклов
Программисты часто используют списки в сочетании с циклами для хранения вычисленных результатов. Однако этот подход неэффективен с точки зрения времени выполнения. По этой причине лучше использовать абстракцию списка, которая обладает лучшей производительностью. Следующий пример показывает разницу в производительности:
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 s
start = perf_counter()
result_list_com = [i*i for i in range(number_round)]
print(perf_counter()-start)
# 0.69 s
print(result_list_com[10])
# 100
Чему мы учимся на этом примере? Используйте абстракцию списков, когда это возможно. Этот способ реализуется несколько противоречиво в программировании. Некоторым программистам трудно читать синтаксис, поскольку одна строка кода выражает все операторы. На наш взгляд, синтаксис ясен и лаконичен. Это дело вкуса, но производительность лучше при абстракции списков.
Абстракция списков начинается с открывающей скобки [. Затем выполняется вычисление из цикла for. Далее следует заголовок цикла с тремя элементами (ключевое слово for, переменная run, длина цикла). Абстракция списков заканчивается закрывающей скобкой ]. Как только вы поймёте синтаксис, вы сможете писать циклы for гораздо более компактно.
Но как насчет использования памяти? Как мы можем уменьшить объём памяти? Это особенно желательно при работе с большими списками, если мы хотим выполнить с ними дальнейшие операции. В нашем примере мы храним 10 000 000 значений в списке. Но должны ли мы сохранять все записи напрямую, или они нужны нам только при необходимости?
В этих случаях мы можем использовать генераторы. Генератор создает элемент списка, когда это необходимо. В результате генератор требует меньше памяти и имеет лучшее время выполнения. Взгляните на следующий пример:
import sys
from time import perf_counter
print(sys.getsizeof(result_list_com), 'bytes')
# 89095160 bytes
start = perf_counter()
result_gen = (i*i for i in range(number_round))
print(perf_counter()-start)
# 0.22 ms
print(sys.getsizeof(result_gen), 'bytes')
# 112 bytes
print(list(result_gen)[10])
# 100
Мы можем выполнить все операции, как в предыдущем примере. Единственная разница в том, что теперь мы используем () вместо []. Вместо списка мы храним генератор. Такой подход более эффективен с точки зрения памяти. Проверьте, можете ли вы использовать абстракцию списков или генераторы в своих проектах. Они могут повысить производительность и уменьшить объём используемой памяти.
Совет 5: Объединяйте словари с синтаксисом двойной звездочки **
Как вы объединяете словари? Вы можете сделать это с помощью однострочника. Мы используем синтаксис asterisk **
. В следующем примере вы можете увидеть, как это работает:
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 является более современным. Если оба словаря содержат один и тот же ключ и разные значения, то используется значение последнего словаря. После слияния новый словарь содержит все три пары ключ-значений. Синтаксис лаконичен и компактен, поэтому объединение происходит очень легко. Этот трюк может сэкономить много времени.
Другим методом является метод обновления. Этот метод обновляет первый словарь и не создаёт копию. Взгляните на следующий пример:
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'}
Недостатком метода обновления является то, что вы можете использовать только один словарь для обновления. Если вам понадобится объединять словари в будущем, запомните этот совет.
Совет 6: Не импортируйте ненужные модули
Возможно, вы слышали этот совет много раз, но он может значительно повысить производительность вашего кода. Нет необходимости импортировать целые библиотеки. Обычно вам нужны только определённые их функции. Кроме того, запуск вашего кода занимает много времени, потому что сначала необходимо импортировать всю библиотеку. Этого не должно быть. Кроме того, вы можете получить доступ к отдельным функциям с помощью точечной нотации. Это очень неэффективно, и вам следует избегать точечных обозначений. Следующие примеры демонстрируют это:
import math
from time import perf_counter
start = perf_counter()
variable = math.exp(7)
print(perf_counter()-start)
# 8.47-05 s
В этом примере мы используем функцию math.exp()
с точечной нотацией. Это приводит к низкой производительности вашего кода. Кроме того, мы импортировали всю математическую библиотеку, хотя нам нужна только функция exp()
.
from math import exp
from time import perf_counter
start = perf_counter()
variable = exp(7)
print(perf_counter()-start)
# 4.51-05 s
В этом примере мы импортируем функцию exp()
без точечной нотации. Используя этот трюк, мы можем вдвое сократить время выполнения нашего кода!
Совет 7: Используйте компилятор just-in-time
Numba– это just-in-time (JIT) – компилятор, который хорошо работает с циклами NumPy, массивами и функциями. Декораторы используются для указания Numba скомпилировать определённые функции с помощью 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 ms
@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 ms
Вы можете видеть, что декоратор над функцией foo
ускоряет код. Декоратор nopython=True
указывает, что компиляция будет выполняться без участия интерпретатора Python. Numba ускоряет выполнение цикла и тригонометрических функций NumPy. Однако он не может использоваться со всеми функциями Python. Ниже приведены преимущества и недостатки Numba:
Недостатки:
- Numba не поддерживает функции Pandas.
- Неподдерживаемый код выполняется через интерпретатор и имеет дополнительные накладные расходы.
- Только неофициальная поддержка на M1 / Arm64.
Преимущества:
- Очень хорошая поддержка массивов NumPy, функций и циклов.
- Поддержка Nvidia CUDA. Его можно хорошо использовать для разработки нейронных сетей на основе NumPy.
Минусы и плюсы показывают, что Numba следует использовать в первую очередь для операций NumPy. Кроме того, вы всегда должны вначале проверять, подходит ли Numba для взаимодействия с необходимыми функциями.
Заключение
В этой статье мы узнали, как повысить эффективность вашего кода с точки зрения времени выполнения и памяти. Полученные знания:
- NumPy не поддерживает параллельную обработку. Для этого вы можете использовать NumExpr.
- Функция Pandas apply() может быть ускорена с помощью swifter.
- Проверьте, существуют ли нужные вам встроенные функции.
- Используйте абстракцию списка вместо циклов. Проверьте, подходят ли генераторы для вашего проекта.
- Объединяйте словари с синтаксисом двойной звездочки **.
- Не импортируйте ненужные модули.
- Если у вас возникли проблемы во время выполнения, вы можете использовать компиляторы just-in-time. Они ускоряют ваш код.
Большое спасибо за чтение данной статьи! Если вам понравилась эта статья, не стесняйтесь поделиться ею. Хорошего дня!