Русский
Русский
English
Статистика
Реклама

Python amp оптимизация времени и памяти

Введение

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

Хотелось бы поделиться несколькими методами, которые помогают в реальных задачах. Я пользуюсь win10 x64.

Экономим память силами Python

В качестве примера рассмотрим вполне реальный пример. Пусть у нас имеется некоторый магазин в котором есть список товаров. Вот нам понадобилось поработать с этими товарами. Самый хороший вариант, когда все товары хранятся в БД, но вдруг что-то пошло не так, и мы решили выгрузить все товары в память, дабы обработать их. И тут встает резонный вопрос, а хватит ли нам памяти для работы с таким количеством товаров?

Давайте первым делом создадим некий класс, отвечающий за наш магазин. У него будет лишь 2 поля: name и listGoods, которые отвечают за название магазина и список товаров соответственно.

class ShopClass:    def __init__(self, name=""):        self.name = name        self.listGoods = []

Теперь мы хотим наполнить магазин товарами (а именно заполнить поле listGoods). Для этого создадим класс, отвечающий за информацию об одном товаре (я использую dataclassы для таких примеров).

# если ругается на dataclass, то делайте # pip install dataclasses# затем в коде вызывайте импорт# from dataclasses import dataclass @dataclassclass DataGoods:    name:str    price:int    unit:str

Далее необходимо заполнить наш магазин товарами. Для чистоты эксперимента я создам по 200 одинаковых товаров в 3х категориях:

shop = ShopClass("MyShop")for _ in range(200):    shop.listGoods.extend([        DataGoods("телефон", 20000, "RUB"),        DataGoods("телевизор", 45000, "RUB"),        DataGoods("тостер", 2000, "RUB")    ])

Теперь пришло время измерить память, которую занимает наш магазин в оперативке (для измерения памяти я использую модуль pympler):

from pympler import asizeofprint("Размер магазина:", asizeof.asizeof(shop))>>> Размер магазина: 106648

Получается, что наш магазин в оперативке занял почти 106Кб. Да, это не так много, но если учесть, что я сохранил лишь 600 товаров, заполнив в них только информацию о наименовании, цене и валюте, в реальной задаче придется хранить в несколько раз больше полей. Например, можно хранить артикуль, производителя, количество товара на складе, страну производителя, цвет модели, вес и много других параметров. Все эти данные могут раздуть ваш магазин с нескольких килобайт до нескольких сотен мегабайт (и это при условии, что данные еще даже не начинали обрабатываться).

Теперь перейдем к решению данной проблемы. Python создает новый объект таким образом, что под него выделяется очень много информации, о которой мы даже не догадываемся. Надо понимать, что python создает объект __dict__ внутри класса для того, чтобы можно было добавлять новые атрибуты и удалять уже имеющиеся без особых усилий и последствий. Посмотрим, как можно динамически добавлять новые атрибуты в класс.

shop = ShopClass("MyShop")print(shop.__dict__)  >>> {'name': 'MyShop', 'listGoods': []}shop.city = "Москва"print(shop.__dict__) >>> {'name': 'MyShop', 'listGoods': [], 'city': 'Москва'}

Однако в нашем примере это абсолютно не играет никакой роли. Мы уже заранее знаем, какие атрибуты должны быть у нас. В pythone есть магический атрибут __slots__, который позволяет отказаться от __dict__. Отказ от __dict__ приведет к тому, что для новых классов не будет создаваться словарь со всеми атрибутами и хранимым в них данными, по итогу объем занимаемой памяти должен будет уменьшиться. Изменим немного наши классы:

class ShopClass:    __slots__ = ("name", "listGoods")    def __init__(self, name=""):        self.name = name        self.listGoods = []@dataclassclass DataGoods:    __slots__ = ("name", "price", "unit")    name:str    price:int    unit:str

И протестируем по памяти наш магазин.

from pympler import asizeofprint("Размер магазина:", asizeof.asizeof(shop))>>> Размер магазина: 43904

Как видно, объем, занимаемый магазином, уменьшился почти в 2.4 раза (значение может варьироваться в зависимости от операционной системы,версии Python, значений и других факторов). У нас получилось оптимизировать занимаемый объем памяти, добавив всего пару строчек кода. Но у такого подхода есть и минусы, например, если вы захотите добавить новый атрибут, вы получите ошибку:

shop = ShopClass("MyShop")shop.city = "Москва">>> AttributeError: 'ShopClass' object has no attribute 'city'

На этом преимущества использования слотов не заканчиваются, из-за того, что мы избавились от атрибута __dict__ теперь ptyhon'у нет необходимости заполнять словарь каждого класса, что влияет и на скорость работы алгоритма. Протестируем наш код при помощи модуля timeit, первый раз протестируем наш код на отключенных __slots__ (включенном__dict__):

import timeitcode = """class ShopClass:    #__slots__ = ("name", "listGoods")    def __init__(self, name=""):        self.name = name        self.listGoods = []@dataclassclass DataGoods:    #__slots__ = ("name", "price", "unit")    name:str    price:int    unit:strshop = ShopClass("MyShop")for _ in range(200):    shop.listGoods.extend([        DataGoods("телефон", 20000, "RUB"),        DataGoods("телевизор", 45000, "RUB"),        DataGoods("тостер", 2000, "RUB")])"""print(timeit.timeit(code, number=60000))>>> 33.4812513

Теперь включим __slots__ (#__slots__ = ("name", "price", "unit") -> __slots__ = ("name", "price", "unit") и # __slots__ = ("name", "listGoods") -> __slots__ = ("name", "listGoods")):

# включили __slots__ в коде вышеprint(timeit.timeit(code, number=60000))>>> 28.535005599999998

Результат оказался более чем удовлетворительным, получилось ускорить код примерно на 15% (тестирование проводилось несколько раз, результат был всегда примерно одинаковый).

Таким образом, у нас получилось не только уменьшить объем памяти, занимаемой программой, но и ускорить наш код.

Пытаемся ускорить код

Способов ускорить python существует несколько, начиная от использования встроенных фишек язык (например, описанных в прошлой главе), заканчивая написанием расширений на C/C++ и других языках.

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

Cython

На мой взгляд Cython является отличным решением, если вы хотите писать код на Python, но при этом вам важна скорость выполнения кода. Реализуем код для подсчета сумм стоимости всех телевизоров, телефонов и тостеров на чистом Python и рассчитаем время, которое было затрачено (будем создавать 20.000.000 товаров):

import timeclass ShopClass:   __slots__ = ("name", "listGoods")   def __init__(self, name=""):      self.name = name      self.listGoods = []@dataclassclass DataGoods:   __slots__ = ("name", "price", "unit")   name: str   price: int   unit: strshop = ShopClass("MyShop")t = time.time()for _ in range(200*100000):   shop.listGoods.extend([      DataGoods("телефон", 20000, "RUB"),      DataGoods("телевизор", 45000, "RUB"),      DataGoods("тостер", 2000, "RUB")   ])print("СОЗДАЕМ ТОВАР НА PYTHON:", time.time()-t)>>> СОЗДАЕМ ТОВАР НА PYTHON: 44.49887752532959telephoneSum, televizorSum, tosterSum = 0, 0, 0t = time.time()for goods in shop.listGoods:   if goods.name == "телефон":      telephoneSum += goods.price   elif goods.name == "телевизор":      televizorSum += goods.price   elif goods.name == "тостер":      tosterSum += goods.priceprint("ВРЕМЯ НА ПОДСЧЁТ СУММ PYTHON:", time.time() - t)>>> ВРЕМЯ НА ПОДСЧЁТ СУММ PYTHON: 13.135360717773438

Как мы видим, время обработки весьма неутешительно. Теперь приступим к использованию cython. Для начала ставим библиотеку cython_npm (см. официальный гитхаб): pip install cython-npm. Теперь создадим новую папку в нашем проекте, назовем её cython_code и в ней создадим файл cython_data.pyx (программы cython пишутся с расширением .pyx).

Перепишем класс магазина под cython:

cdef class CythonShopClass:   cdef str name   cdef list listGoods   def __init__(self, str name):       self.name = name       self.listGoods = []

В cython необходимо строго типизировать каждую переменную, которую вы используете в коде (это не обязательно, но если этого не делать, то уменьшения по времени не будет). Для этого необходимо писать cdef <тип данных> <название переменной> в каждом классе или методе. Реализуем остальной код на cython. Функцию my_def() реализуем без cdef, а с привычным нам def, так как её мы будем вызывать из основного python файла. Также в начале нашего файла .pyx необходимо прописать версию языка (# cython: language_level=3).

# cython: language_level=3# на забывает вставить код класса магазинаcdef class CythonDataGoods:   cdef str name   cdef int price   cdef str unit   def __init__(self, str name, int price, str unit):       self.name = name       self.price = price       self.unit = unitcdef int c_testFunc():    cdef CythonShopClass shop    cdef CythonDataGoods goods    cdef int i, t, telephoneSum, televizorSum, tosterSum    size, i, telephoneSum, televizorSum, tosterSum = 0, 0, 0, 0, 0    shop = CythonShopClass("MyShop")    t = time.time()    for i in range(200*100000):       shop.listGoods.extend([           CythonDataGoods("телефон", 20000, "RUB"),           CythonDataGoods("телевизор", 45000, "RUB"),           CythonDataGoods("тостер", 2000, "RUB")       ])    print("СОЗДАЕМ ТОВАР НА CYTHON:", time.time()-t)    t = time.time()    for goods in shop.listGoods:        if goods.name == "телефон":            telephoneSum += goods.price        elif goods.name == "телевизор":            televizorSum += goods.price        elif goods.name == "тостер":            tosterSum += goods.price    print("ВРЕМЯ НА ПОДСЧЁТ СУММ CYTHON:", time.time() - t)    return 0def my_def():    data = c_testFunc()    return data

Теперь в main.py нашего проекта сделаем вызов cython кода. Для этого делаем сначала импорт всех установленных библиотек:

from cython_npm.cythoncompile import exportfrom cython_npm.cythoncompile import installimport time

И делаем сразу же компиляцию нашего cython и его импорт в основной python код

export('cython_code/cython_data.pyx')import cython_code.cython_data as cython_data

Теперь необходимо вызвать код cython

if __name__ == "__main__":   a = cython_data.my_def()

Запускаем. Обратим внимание, что было выведено в консоли. В cython, где мы делали вывод времени на создание товаров, мы получили:

>>> СОЗДАЕМ ТОВАР НА CYTHON: 4.082242012023926

А там где был вывод после подсчета сумм получили:

>>> ВРЕМЯ НА ПОДСЧЁТ СУММ CYTHON: 1.0513946056365967

Как мы видим, скорость создания товаров сократилась с 44 до 4 секунд, то есть мы ускорили данную часть кода почти в 11 раз. При подсчете сумм время сократилось с 13 секунд до 1 секунды, примерно в 13 раз.

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

Магия Python

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

shop = ShopClass("MyShop")t = time.time()getGoods = lambda index: {0: ("телефон", 20000, "RUB"),                           1: ("телевизор", 45000, "RUB"),                           2:("тостер", 2000, "RUB")}.get(index) shop.listGoods = [DataGoods(*getGoods(i%3)) for i in range(200*100000)]print("СОЗДАЕМ ТОВАР НА PYTHON:", time.time()-t)>>>  СОЗДАЕМ ТОВАР НА PYTHON: 19.719463109970093

Скорость увеличилась примерно в 2 раза, при этом мы пользовались силами самого python. Генераторы в python - очень удобная вещь, они позволяют не только ускорить ваш код, но и оптимизировать его по используемой памяти.

PyPy

Бывает так, что нет возможности переписать код на cython или другой язык, потому что уже имеется достаточно большая кодовая база (или по другой причине), а скорость выполнения программы хочется увеличить. Рассмотрим код из прошлого примера, где мы использовали лямба функции и генератор списков. Тут на помощь может прийти PyPy, это интерпретатор языка python, использующий JIT компилятор. Однако PyPy поддерживает не все сторонние библиотеки, если вы используете в коде таковые, то изучите подробнее документацию. Выполнить python код при помощи PyPy очень легко.

Для начала качаем PyPy с официального сайта. Распаковываем в любую папку, открываем cmd и заходим в папку, где теперь лежит файл pypy3.exe, в эту же папку положим наш код с программой. Теперь в cmd пропишем следующую команду:

Таким образом, 19 секунд pythonовского кода из прошлого примера у нас получилось сократить до 4.5 секунд вообще без переписывания кода, то есть почти в 4 раза.

Вывод

Мы рассмотрели несколько вариантов оптимизации кода по времени и памяти. На зло всем хейтерам, которые говорят, что python медленный, мы смогли достичь ускорения кода в десятки раз.

Были рассмотрели не все возможные варианты ускорения кода. В некоторых случаях можно использовать Numba, NumPy, Nim или multiprocessing. Все зависит от того, какую задачу вы решаете. Некоторые задачи будет проще решать на других языках, так как python не способен решить всё на этом свете.

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

Источник: habr.com
К списку статей
Опубликовано: 07.04.2021 00:23:14
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Программирование

Проектирование и рефакторинг

Cython

Оптимизация

Pypy

Ускорение

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru