Python

Как я сократил время выполнения приложения на 1/10

Данные советы просты в реализации и могут пригодиться вам в обозримом будущем.

Считается, что первоочередной задачей программиста является написание чистого и эффективного кода. Как только вы создали чистый код, можете переходить к следующим 10 подсказкам. Я подробно объясню их ниже.

Как я измеряю время и сложность кода?

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

python -m cProfile [-o output_file] my_python_file.py

Используйте структуры данных из хеш-таблиц

  • Если ваше приложение будет выполнять огромное количество операций поиска на большой коллекции неповторяющихся элементов, то воспользуйтесь словарем.
  • Это высокопроизводительная коллекция данных.
  • Сложность поиска элемента — O(1).
  • Здесь стоит упомянуть, что словари не эффективны для наборов данных с малым количеством элементов.

Вместо:

items = [‘a’, ‘b’,..,’100m’] #1000s of items

found = False
for i in items:
  if (i == ‘100m’):
    found = True

Пишите:

items = {‘a’:’a’, ‘b’:’b:,..,’100m’:’100m’} #each item is key/value
found = False
if ‘100m’ in items:
found = True

Если есть такая возможность, то вместо перебора данных коллекций пользуйтесь поиском.

Векторизация вместо циклов

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

import numpy as np

array = np.array([[1., 2., 3.], [4., 5., 6.]])

m_array = array*array

Сократите количество строк в коде

Пользуйтесь встроенными функциями Python. Например, map()

Вместо:

newlist = []

def my_fun(a):
 return a + ‘t’

for w in some_list:
 newlist.append(my_fun(w))

Пишите:

def my_fun(a):
 return a + ‘t’

newlist = map(my_fun, some_list)

Каждое обновление строковой переменной создает новый экземпляр

Вместо:

my_var = ‘Malik’
myname_blog = ‘Farhad ‘ + my_var + ‘ FinTechExplained’

Пишите:

my_var = ‘Malik’
myname_blog = ‘Farhad {0} FinTechExplained’.format(my_var)

Пример выше уменьшает объем памяти.

Для сокращения строк пользуйтесь циклами и генераторами for

Вместо:

for x in big_x:
for y in x.Y:
items.append(x.A + y.A)

Пишите:

items = [x.A+y.A for x in big_x for y in x.Y]

Пользуйтесь многопроцессорной обработкой

Если ваш компьютер выполняет более одного процесса, тогда присмотритесь к многопроцессорной обработке в Python.

Она разрешает распараллеливание в коде. Многопроцессорная обработка весьма затратна, поскольку вам придется инициировать новые процессы, обращаться к общей памяти и т.д., поэтому пользуйтесь ей только для большого количества разделяемых данных. Для небольших объемов данных многопроцессорная обработка не всегда оправдана.

Вместо:

def some_func(d): 
 #computations

data = [1,2,..,10000] #large data

for d in data: 
 some_func(d)

Пишите:

import multiprocessing

def some_func(d): 
 #computations

data = [1,2,..,10000] #large data

pool = multiprocessing.Pool(processes=number_of_processors)

r = pool.map(some_func, data)
 pool.close()

Многопроцессорная обработка очень важна для меня, поскольку я обрабатываю по несколько путей выполнения одновременно.

Пользуйтесь Cython

Cython — это статический компилятор, который будет оптимизировать код за вас.

Загрузите расширения Cythonmagic и пользуйтесь тегом Cython для компиляции кода через Cython.

Воспользуйтесь Pip для установки Cython:

pip install Cython

Для работы с Cython:

%load_ext cythonmagic
 %%cython
 def do_work():
 … #работа с большим объемом вычислений

Пользуйтесь Excel только при необходимости

Не так давно мне нужно было реализовать одно приложение. И мне бы пришлось потратить много времени на загрузку и сохранение файлов из/в Excel. Вместо этого я пошел другим путем: создал несколько CSV-файлов и сгруппировал их в отдельной папке.

Примечание: все зависит от задачи. Если создание файлов в Excel сильно тормозит работу, то можно ограничиться несколькими CSV-файлами и утилитой на нативном языке, которая объединит эти CSV в один Excel-файл.

Вместо:

df = pd.DataFrame([[‘a’, ‘b’], [‘c’, ‘d’]],index=[‘row 1’, ‘row 2’],columns=[‘col 1’, ‘col 2’])
df.to_excel(“my.xlsx”)

df2 = df.copy()
with pd.ExcelWriter(‘my.xlsx’) as writer: 
df.to_excel(writer, sheet_name=’Sheet_name_1')
df2.to_excel(writer, sheet_name=’Sheet_name_2')

Пишите:

df = pd.DataFrame([[‘a’, ‘b’], [‘c’, ‘d’]],index=[‘row 1’, ‘row 2’],columns=[‘col 1’, ‘col 2’])
df2 = df.copy()

df.to_csv(“my.csv”)
df2.to_csv(“my.csv”)

Пользуйтесь Numba

Это — JIT-компилятор (компилятор «на лету»). С помощью декоратора Numba компилирует аннотированный Python- и NumPy-код в LLVM.

Разделите функцию на две части:
 1. Функция, которая выполняет вычисления. Ее декорируйте с @autojit.

2. Функция, которая выполняет операции ввода-вывода.

from numba import jit, autojit

@autojit
 def calculation(a):
 ….
 
 def main():
 calc_result = calculation(some_object)
 
 d = np.array(calc_result)
 #save to file
 return d

Пользуйтесь Dask для распараллеливания операций Pandas DataFrame

Dask очень классный! Он помог мне с параллельной обработкой множества функций в DataFrame и NumPy. Я даже попытался масштабировать их в кластере, и все оказалось предельно просто!

import pandas as pd
import dask.dataframe as dd
from dask.multiprocessing import get

data = pd.DataFrame(…) #large data set

def my_time_consuming_function(d):
 …. #долго выполняемая функция

ddata = dd.from_pandas(data, npartitions=30)

def apply_my_func(df): 
 return df.apply(
 (lambda row: my_time_consuming_function(*row)), axis=1)

def dask_apply(): 
 return ddata.map_partitions(apply_my_func).compute(get=get)

Пользуйтесь пакетом swifter

Swifter использует Dask в фоновом режиме. Он автоматически рассчитывает наиболее эффективный способ для распараллеливания функции в пакете данных.

Это плагин для Pandas.

import swifter
import pandas as pd

a_large_data_frame = pd.DataFrame(…) #large data set
def my_time_consuming_function(data):
…
 
result = a_large_data_frame.swifter.apply(some_function)

Пользуйтесь пакетом Pandarallel

Pandarallel может распараллеливать операции на несколько процессов.

Опять же, подходит только для больших наборов данных.

from pandarallel import pandarallel
from math import sin
 
 pandarallel.initialize()
 
 
 # ALLOWED
 def my_time_consuming_function(x):
 ….
 
 df.parallel_apply(my_time_consuming_function, axis=1)

Общие советы

  • Первым делом нужно писать чистый и эффективный код. Мы должны проследить, чтобы код внутри цикла не выполнял одни и те же вычисления.
  • Также важно не открывать/закрывать подключения ввода-вывода для каждой записи в коллекции.
  • Подумайте, можно ли кэшировать объекты.
  • Проверьте, что не создаете новые экземпляры объектов там, где они не нужны.
  • И, наконец, убедитесь, что код написан лаконично и не выполняет одни и те же повторяющиеся задачи со сложными вычислениями.

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

Заключение

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

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

Перевод статьи Farhad Malik: How To Make Python Faster