Теперь мы хотим наполнить магазин товарами (а именно заполнить поле listGoods). Для этого создадим класс, отвечающий за информацию об одном товаре (я использую dataclass’ы для таких примеров).
Далее необходимо заполнить наш магазин товарами. Для чистоты эксперимента я создам по 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 asizeof print(«Размер магазина:», asizeof.asizeof(shop)) >>> Размер магазина: 106648
Получается, что наш магазин в оперативке занял почти 106Кб. Да, это не так много, но если учесть, что я сохранил лишь 600 товаров, заполнив в них только информацию о наименовании, цене и валюте, в реальной задаче придется хранить в несколько раз больше полей. Например, можно хранить артикул, производителя, количество товара на складе, страну производителя, цвет модели, вес и много других параметров. Все эти данные могут раздуть ваш магазин с нескольких килобайт до нескольких сотен мегабайт (и это при условии, что данные еще даже не начинали обрабатываться).
Замеряем время правильно в Python. В чем разница между time, timeit, repeat, %%time, %time и т.д.
Теперь перейдем к решению данной проблемы. Python создает новый объект таким образом, что под него выделяется очень много информации, о которой мы даже не догадываемся. Надо понимать, что python создает объект __dict__ внутри класса для того, чтобы можно было добавлять новые атрибуты и удалять уже имеющиеся без особых усилий и последствий. Посмотрим, как можно динамически добавлять новые атрибуты в класс.
shop = ShopClass(«MyShop») print(shop.__dict__) >>> shop.city = «Москва» print(shop.__dict__) >>>
Однако в нашем примере это абсолютно не играет никакой роли. Мы уже заранее знаем, какие атрибуты должны быть у нас. В python’e есть магический атрибут __slots__, который позволяет отказаться от __dict__. Отказ от __dict__ приведет к тому, что для новых классов не будет создаваться словарь со всеми атрибутами и хранимым в них данными, по итогу объем занимаемой памяти должен будет уменьшиться. Изменим немного наши классы:
И протестируем по памяти наш магазин.
from pympler import asizeof print(«Размер магазина:», asizeof.asizeof(shop)) >>> Размер магазина: 43904
Как видно, объем, занимаемый магазином, уменьшился почти в 2.4 раза (значение может варьироваться в зависимости от операционной системы, версии Python, значений и других факторов). У нас получилось оптимизировать занимаемый объем памяти, добавив всего пару строчек кода. Но у такого подхода есть и минусы, например, если вы захотите добавить новый атрибут, вы получите ошибку:
shop = ShopClass(«MyShop») shop.city = «Москва» >>> AttributeError: ‘ShopClass’ object has no attribute ‘city’
На этом преимущества использования слотов не заканчиваются, из-за того, что мы избавились от атрибута __dict__ теперь ptyhon’у нет необходимости заполнять словарь каждого класса, что влияет и на скорость работы алгоритма. Протестируем наш код при помощи модуля timeit, первый раз протестируем наш код на отключенных __slots__ (включенном__dict__):
Python Быстрее чем Си?! Ускоряем Python До Максимума!
Теперь включим __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 товаров):
Как мы видим, время обработки весьма неутешительно. Теперь приступим к использованию 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 = unit cdef 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 0 def my_def(): data = c_testFunc() return data
Теперь в main.py нашего проекта сделаем вызов cython кода. Для этого делаем сначала импорт всех установленных библиотек:
from cython_npm.cythoncompile import export from cython_npm.cythoncompile import install import 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: .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
В сегодняшней статье мы рассмотрим как оптимизировать время выполнения программы в Python c помощью операции кэширования. Мы рассмотрим данную операцию на примере рекурсивной функции в Python. Но сначала — пара слов о рекурсии. Рекурсия — это понятие в программировании, при котором функция вызывает сама себя один или несколько раз.
Данные типы функций часто сталкиваются с проблемами скорости, из-за того, что функция постоянно вызывает сама себя. Операция рекурсии занимает достаточно много памяти из за постоянного повторения одних и тех же шагов. Кэшировнаие или Мемоизация (англ.
memoization от англ. memory и англ. optimization) помогает этому процессу, сохраняя значения, которые уже были рассчитаны для последующего использования. Давайте сначала вспомним что такое рекурсивные функции. Давайте посмотрим несколько примеров!
Написание факториальной функции.
Факториалы — один из самых простых примеров рекурсии, и является результатом умножения всех чисел меньших на единицу чем данное:
def factorial(n):
# установите базовый случай!
if n return 1
else:
return factorial( n – 1 ) * n
print( factorial(5) ) # результат от умножения 5 * 4 * 3 * 2 * 1
Последовательность Фибоначчи. Последовательность Фибоначчи — одна из самых известных формул в математике. Это также одна из самых известных рекурсивных функций в программировании. Каждое число в последовательность — это сумма двух предыдущих чисел, таких, что fib(5) = fib(4) + fib(3). Вычислим ее для 20.
from datetime import datetime
import time
def fib(n):
if n return n
else:
return fib( n — 1 ) + fib( n — 2 )
9227465
0:00:17.647056 # время вычисления
Как видно на вычисление результата для числа 35, ушло целых 17 секунд.
По мере того, как растет количество передаваемых данных, растет структура и количество рекурсивных вызовов . Он экспоненциальный, что может значительно замедлить работу программы. Даже попытка выполнить fib(40) может занять пару минут, а fib(100) обычно не работает из-за проблем с максимальной глубиной рекурсии. Что приводит нас к нашей следующей теме о том, как решить эту проблему… кэширование .
Понимание мемоизации.
Когда вы заходите на веб-страницу в первый раз, вашему браузеру требуется некоторое время, чтобы загрузить изображения и файлы, необходимые странице. Когда вы во второй раз зайдете на ту же самую страницу, она обычно загружается намного быстрее. Это связано с тем, что ваш браузер использует технику, известную как «кэширование». В вычислительной технике мемоизация — это метод оптимизации, используемый в первую очередь для ускорения программы, сохраняя результаты ранее вызванных функций и возвращая сохраненный результат при попытке вычислить ту же последовательность. Это просто известно как кэширование.
Использование мемоизации.
Чтобы применить ее к последовательности Фибоначчи, мы должны понять, какой наилучший метод кэширования значений. В Python словари дают нам возможность для хранения значений на основе заданного ключа. Благодаря скорости и уникальной ключевой структуре словарей мы можем использовать их для хранения значение каждой последовательности Фибоначчи. Таким образом, как только одна последовательность, такая как fib(3), рассчитывается, его не нужно вычислять снова. Он просто сохраняется в кэше и извлекаются по мере необходимости. Давайте попробуем:
cache = < >
def fib(n):
if n in cache:
return cache[ n ]
result = 0
if n < = 1:
result = n
else:
result = fib( n – 1 ) + fib( n -2 )
cache[ n ] = result
return result
Теперь, когда мы знаем, как самостоятельно создать систему кэширования, давайте воспользуемся встроенными средствами Python. способ запоминания. Он известен как lru_cache.
from functools import lru_cache
Лайфхаки Python: сэкономить память и ускорить выполнение программы
Python часто ругают за то, что он медленный. Однако в нем существует несколько подходов, которые позволяют писать достаточно быстрый код. Сегодня поговорим про обработку списков.
TL;DR Используйте списковые включения (list comprehensions), генераторные выражения (generator expressions) и генераторы везде, где только можно. Это поможет сэкономить память и время выполнения программы.
Антон Сычугов
Ведущий разработчик CoMagic.dev (IT-платформа UIS и CoMagic)
Списковые включения (List comprehensions)
Например у нас есть большой список словарей (объявления контекстной рекламы):
import timeit import time import itertools import random now = time.time() ads_list = list( for i in range(1000000))
Зададим начальное время выборки и конечное
date_start = now — 60*60*24*7 date_end = now — 60*60*24*3
И попробуем выбрать все объявления, ставка которых выше 600 и дата попадает в выбранный интервал. Затем возьмем первые 1000 элементов полученного списка. Сначала решим задачу «в лоб»:
def many_lists(): expensive_ads = [a for a in ads_list if a[’bid’] > 600] expensive_in_interval = [a for a in expensive_ads if a[’date’] > date_start and a[’date’] < date_end] return expensive_in_interval[:1000] timeit.timeit(many_lists, number=100) 9.126969000000031
Ок, попробуем немного оптимизировать и сделаем проверку даты там же, где и ставки:
def many_lists_improved(): expensive_ads = [a for a in ads_list if a[’bid’] > 600 and a[’date’] > date_start and a[’date’] < date_end] return expensive_ads[:1000] timeit.timeit(many_lists_improved, number=100) 8.304767416000004
Уже лучше, но не сильно лучше.
Генераторные выражения (generator expressions)
Попробуем использовать генераторные выражения (для получения среза будем использовать функцию islice из itertools, которая возвращает итератор по срезу):
def generators(): expensive_ads = (a for a in ads_list if a[’bid’] > 600) expensive_in_interval = (a for a in expensive_ads if a[’date’] > date_start and a[’date’] < date_end) return list(itertools.islice(expensive_in_interval, 0, 1000)) timeit.timeit(generators, number=100) 2.422867124999925
Итог: увеличение производительности более чем в 3 раза.
Генераторные фунции (generator functions)
Если предикатов фильтрации или обработчиков элементов списка много, то удобнее использовать генераторы. Они могут не дать прироста скорости, но помогут сэкономить память.
Генераторной фунцией в python называется функция, которая ведет себя как итератор. Для определения генераторной функции нужно использовать ключевое слово yield:
def count_five(): for i in range(5): yield i
Например у нас есть список кортежей (чтобы не было соблазна менять словарь на месте) с данными объявлений, и мы хотим выбрать все объявления из списка, попадающие в наш интервал, а также протэгировать по условиям ставки.
Стоит поиграть: EVE Online — космический симулятор для прокачки интеллекта и навыка командной работы
Попробуем для начала решить задачу в лоб и в каждой функции-обработчике возвращать новый список, а затем решим задачу с помощью генераторов:
import sys import time import random import psutil from timeit import timeit now = time.time() ads_list = list((1000000 — i, now — i, random.randint(1, 1000)) for i in range(1000000)) date_end = now — 60*60*24*3 date_start = now — 60*60*24*7 def below_3days(ad_list): return [a for a in ad_list if a[1] < date_end] def above_7days(ad_list): return [a for a in ad_list if a[1] >date_start] def tag_bid(ad_list): return [(a[0], a[1], a[2] > 500 and ’expensive’ or ’cheap’) for a in ad_list] def above_7days_gen(ad_list): for a in ad_list: if a[1] > date_start: yield a def below_3days_gen(ad_list): for a in ad_list: if a[1] < date_end: yield a def tag_bid_gen(ad_list): for a in ad_list: tag = a[2] >500 and ’expensive’ or ’cheap’ yield (a[0], a[1], tag) list_pipeline = lambda ads: below_3days(above_7days(tag_bid(ads))) gen_pipeline = lambda ads: below_3days_gen(above_7days_gen(tag_bid_gen(ads))) pipelines = < ’list’: list_pipeline, ’gen’: gen_pipeline >def run_pipeline(ad_list, pipeline): processed = pipeline(ad_list) return sorted(processed, key=lambda item: item[0]) if __name__ == ’__main__’: try: pipeline = pipelines[sys.argv[1]] except (IndexError, KeyError): print(’invalid arguments, use `list` or `gen` to choose pipeline’) sys.exit(1) process = psutil.Process() mem_before = process.memory_info().rss tm = timeit(lambda: run_pipeline(ads_list, pipeline), number=1) consumption = process.memory_info().rss — mem_before print(’consumption:’, consumption/1024, ’KB, in’, round(tm, 2), ’s’)
Запустим наш скрипт сначала с ключом list:
python run_pipeline.py list consumption: 15568.0 KB, in 0.25 s
А потом с ключом gen:
python run_pipeline.py gen consumption: 6112.0 KB, in 0.25 s
Как можно увидеть, скорость выполнения не изменилась, но мы сэкономили память (почти трехкратная разница), потому что генераторы не создают новых списков, а обрабатывают один элемент за итерацию.
И напоследок
Не всегда операторы в python ведут себя так, как мы привыкли. Например сложение списков:
def plus(): list1 = list(range(1000000)) list2 = list(range(1000000)) list1 = list1 + list2 return list1 def plus_eq(): list1 = list(range(1000000)) list2 = list(range(1000000)) list1 += list2 return list1 timeit.timeit(plus, number=10) 0.4108163330001844 timeit.timeit(plus_eq, number=10) 0.3518642500000624
Посмотрим дизассемблером, что происходит внутри этих функций:
import dis dis.dis(plus) 2 0 LOAD_GLOBAL 0 (list) 2 LOAD_GLOBAL 1 (range) 4 LOAD_CONST 1 (1000000) 6 CALL_FUNCTION 1 8 CALL_FUNCTION 1 10 STORE_FAST 0 (list1) 12 LOAD_GLOBAL 0 (list) 14 LOAD_GLOBAL 1 (range) 16 LOAD_CONST 1 (1000000) 18 CALL_FUNCTION 1 20 CALL_FUNCTION 1 22 STORE_FAST 1 (list2) 24 LOAD_FAST 0 (list1) 26 LOAD_FAST 1 (list2) 28 BINARY_ADD 30 STORE_FAST 0 (list1) 32 LOAD_FAST 0 (list1) 34 RETURN_VALUE dis.dis(plus_eq) 2 0 LOAD_GLOBAL 0 (list) 2 LOAD_GLOBAL 1 (range) 4 LOAD_CONST 1 (1000000) 6 CALL_FUNCTION 1 8 CALL_FUNCTION 1 10 STORE_FAST 0 (list1) 12 LOAD_GLOBAL 0 (list) 14 LOAD_GLOBAL 1 (range) 16 LOAD_CONST 1 (1000000) 18 CALL_FUNCTION 1 20 CALL_FUNCTION 1 22 STORE_FAST 1 (list2) 24 LOAD_FAST 0 (list1) 26 LOAD_FAST 1 (list2) 28 INPLACE_ADD 30 STORE_FAST 0 (list1) 32 LOAD_FAST 0 (list1) 34 RETURN_VALUE
Как видно, инструкция 28 в случае `+` простое сложение, а в случае `+=` — сложение на месте, которое не приводит к созданию нового списка. += в данном случае сопоставим по производительности с list.extend:
def extend(): list1 = list(range(1000000)) list2 = list(range(1000000)) list1.extend(list2) return list1 timeit.timeit(extend, number=10) 0.3511457080001037
Заключение
Генераторы и итераторы помогают сэкономить память или время выполнения, но у них есть и особенности, из-за которых они могут не подойти. Например, по генераторам можно итерироваться только один раз, в отличие от списков.
Выводы
На примерах выше мы увидели, что python предоставляет нам некоторую возможность для маневра при обработке списков, главное знать об этих возможностях и использовать их там, где они подходят.
Источник: tproger.ru