Как сократить время работы программы python

Время на прочтение
8 мин

Количество просмотров 19K

Введение

Зачастую скорость выполнения 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 
@dataclass
class 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 asizeof
print("Размер магазина:", 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': 'Москва'}

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

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

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

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__):

import timeit
code = """
class ShopClass:
    #__slots__ = ("name", "listGoods")
    def __init__(self, name=""):
        self.name = name
        self.listGoods = []
@dataclass
class DataGoods:
    #__slots__ = ("name", "price", "unit")
    name:str
    price:int
    unit:str
shop = 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 time
class ShopClass:
   __slots__ = ("name", "listGoods")
   def __init__(self, name=""):
      self.name = name
      self.listGoods = []
@dataclass
class DataGoods:
   __slots__ = ("name", "price", "unit")
   name: str
   price: int
   unit: str
shop = 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.49887752532959
telephoneSum, televizorSum, tosterSum = 0, 0, 0
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("ВРЕМЯ НА ПОДСЧЁТ СУММ 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 = 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: {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, по максимуму избавиться от циклов в циклах в циклах в цикле, очищать руками память и удалять ненужные элементы по ходу выполнения кода. Не стоит ожидать, что переписав ваш код на другой язык — это решит все ваши проблемы, учитесь искать узкие места в коде и оптимизировать их алгоритмически или при помощи фишек самого языка.

Для ускорения кода на Python программисты могут использовать много приемов. Мы собрали несколько самых простых и при этом самых эффективных из них.

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

Большинство из наших читателей, вероятно, уже начали писать на Python. Сперва всё кажется простым и очевидным. Но при решении задач со сложными алгоритмами начинается головная боль с Time Limit Exceeded. Однако, в этом нет вины Python – это вина программиста. Да, Python медленный, но если программист напишет эффективную программу, она точно выполнится без подобных загвоздок.

Представляем вам несколько приемов и подходов для ускорения кода и повышения его эффективности.

Используйте подходящие структуры данных

Применение правильных структур данных значительно ускоряет выполнение кода.

В Python встроены такие структуры данных, как список (list), кортеж (tuple), множество (set) и словарь (dictionary). Несмотря на это, большинство людей хорошо помнят только про списки. Это неправильный подход.

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

Избегайте циклов for

В случаях, когда цикл for обрабатывает диапазон непостоянного размера, его выполнение в Python происходит медленнее, чем выполнение цикла while. Поэтому в таких случаях лучше прибегайте к while.

Применяйте списковые включения (list comprehension)

Не обращайтесь ни к какой другой технике, если можно использовать списковые включения. Например, этот код заносит в список все числа между 1 и 1000, кратные 3:

L = []
for i in range (1, 1000):
    if i%3 == 0:
        L.append (i)

Со списковыми включениями код трансформируется в одну строку:

L = [i for i in range (1, 1000) if i%3 == 0]

Этот приём работает быстрее, чем просто метод append().

Не пренебрегайте множественным присваиванием

Не стоит инициализировать несколько переменных так:

a = 2
b = 3
c = 5
d = 7

Лучше придерживайтесь следующего синтаксиса:

a, b, c, d = 2, 3, 5, 7

[python_ad_block]

Не создавайте глобальные переменные

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

Применяйте библиотечные функции

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

Соединяйте строки методом join

В Python конкатенацию строк можно производить при помощи знака +.

concatenatedString = "Программирование " + "это " + "весело."

Но также для этого есть метод join().

concatenatedString = " ".join (["Программирование", "это", "весело."])

Всё дело в том, что оператор + каждый раз создаёт новую строку, а затем копирует в неё исходные. join() устроен иначе и обеспечивает выигрыш во времени.

Используйте генераторы

Если у вас в списке хранится много данных, которые требуется использовать все за раз, применяйте generator. Это сэкономит ваше время.

Будьте бдительны

Взгляните на следующий код:

L = []
for element in set(L):
    ...

Данный код может показаться эффективным, так как в нём для удаления дубликатов используется set. Но на самом деле он будет выполняться долго. Не забывайте, что приведение списка ко множеству – это время. Так что этот вариант будет лучше:

for element in L:
    ...

Избегайте точек

Старайтесь не пользоваться ими. Взгляните на пример:

import math
val = math.sqrt(60)

Вместо этого можно применить следующий синтаксис:

from math import sqrt
val = sqrt(60)

Всё потому, что когда вы вызываете функцию с помощью точки, она сперва обращается к методу __getattribute()__ или __getattr()__. Эти методы, в свою очередь, используют операции со словарями, отнимающие время. Поэтому старайтесь писать: from module import function.

Используйте 1 в бесконечных циклах

Пишите while 1 вместо while True. Это выиграет вам немного времени.

Попробуйте другие подходы

Не бойтесь применять новые практики для повышения эффективности кода.

Допустим, у вас есть код:

if a_condition:
    if another_condition:
        do_something
else:
    raise exception

Вместо этого стоит попробовать:

if (not a_condition) or (not another_condition):
    raise exception
do_something

Используйте ускорители

Медлительность Python послужила вдохновением для различных проектов, сокращающих его время работы. На большинстве соревнований по программированию вы встретитесь с pypy (там, где можно писать на Python).

Эти средства помогут уменьшить время выполнения Python-программ.

Для больших датасетов используйте специальные библиотеки

C/C++ быстрее Python. Поэтому многие пакеты и модули, которые можно использовать в программах на Python, пишутся на C/C++. Среди таких модулей – Numpy, Scipy и Pandas, столь необходимые при обработке больших массивов данных.

Python регулярно обновляется и совершенствуется и с каждым релизом становится всё быстрее и оптимизированнее. Поэтому для ускорения кода всегда пишите его на новейшей версии языка.

Заключение

Мы рассмотрели приёмы для ускорения кода на Python. Конечно, этот список не исчерпывающий: есть и другие способы, которые могут вам пригодиться. Обязательно ищите их и пишите код эффективно!

Перевод статьи Speed Up Python Code.

«Питон – медленный». Наверняка вы не раз сталкивались с этим утверждением, особенно от людей, пришедших в Python из C, C++ или Java. Во многих случаях это верно. Циклы или сортировка массивов, списков или словарей иногда действительно работают медленно. В конце концов, главная миссия Python – сделать программирование приятным и легким. Ради лаконичности и удобочитаемости пришлось отчасти принести в жертву производительность.

Тем не менее, в последние годы предпринято немало усилий для решения проблемы. Теперь мы можем эффективно обрабатывать большие наборы данных с помощью NumPy, SciPy, Pandas и numba, поскольку все эти библиотеки написаны на C/C++. Еще один интересный проект – PyPy ускоряет код Python в 4.4 раза по сравнению с CPython (оригинальная реализация Python).

Недостаток PyPy – нет поддержки некоторых популярных модулей, например, Matplotlib, SciPy.

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

1. Стандартные функции

<a href="https://docs.python.org/3/library/functions.html" target="_blank" rel="noopener noreferrer nofollow">Список встроенных функций Python 3</a>

Список встроенных функций Python 3

В Python много работающих очень быстро реализованных на C встроенных функций. Они покрывают большинство тривиальных вычислительных операций (abs, min, max, len, sum). Хороший разработчик должен их знать, чтобы в подходящем месте не изобретать неуклюжие велосипеды, а брать надёжное стандартное решение. Возьмём в качестве примеров встроенные функции set() и sum(). Сравним их работу с кастомными реализациями того же функционала.

Пример для set():

        import random
random.seed(666)
a_long_list = [random.randint(0, 50) for i in range(1000000)]

# 1. Кастомная реализация set
%%time
unique = []
for n in a_long_list:
  if n not in unique:
    unique.append(n)

# Вывод в консоли:
# CPU times: user 316 ms, sys: 1.36 ms, total: 317 ms
# Wall time: 317 ms

# 2. Встроенная функция set
%%time  
unique = list(set(a_long_list))

# Вывод в консоли:
# CPU times: user 8.74 ms, sys: 110 μs, total: 8.85 ms
# Wall time: 8.79 ms
    

Пример для sum():

        # 1. Кастомная реализация sum
%%time
sum_value = 0
for n in a_long_list:
  sum_value += n
print(sum_value)

# Вывод в консоли:
# 25023368
# CPU times: user 9.91 ms, sys: 2.2 ms, total: 101 ms
# Wall time: 100 ms

# 2. Встроенная функция sum
%%time
sum_value = sum(a_long_list)
print(sum_value)

# Вывод в консоли:
# 25023368
# CPU times: user 4.74 ms, sys: 277 μs, total: 5.02 ms
# Wall time: 4.79 ms
    

Стандартные варианты в 36 (set) и 20 (sum) раз быстрее, чем функции, написанные самим разработчиком.

2. sort() vs sorted()

Если нам просто нужен отсортированный список, при этом неважно, что будет с оригиналом, sort() будет работать немного быстрее, чем sorted(). Это справедливо для базовой сортировки:

        # 1. Дефолтная сортировка с использованием sorted()
%%time
sorted(a_long_list)

# Вывод в консоли:
# CPU times: user 12 ms, sys: 2.51 ms, total: 14.5 ms
# Wall time: 14.2 ms

# 2. Дефолтная сортировка с использованием sort()
%%time
a_long_list.sort()

# Вывод в консоли:
# CPU times: user 8.52 ms, sys: 82 μs, total: 8.6 ms
# Wall time: 10 ms
    

Справедливо и для сортировки с использованием ключа – параметра key, который определяет сортировочную функцию:

        # 1. Сортировка с ключом с использованием sorted()
%%time
str_list1 = "Although both functions can sort list, there are small differences".split()
result = sorted(str_list1, key=str.lower)
print(result)

# Вывод в консоли:
# ['Although', 'are', 'both', 'can', 'differences', 'functions', 'list,', 'small', 
'sort', 'there']
# CPU times: user 29 μs, sys: 0 ns, total: 29 μs
# Wall time: 32.9 μs

# 2. Сортировка с ключом с использованием sort()
%%time
str_list2 = "Although both functions can sort list, there are small differences".split()
str_list2.sort(key=str.lower)
print(str_list2)

# Вывод в консоли:
# ['Although', 'are', 'both', 'can', 'differences', 'functions', 'list,', 'small', 
'sort', 'there']
# CPU times: user 26 μs, sys: 0 ns, total: 26 μs
# Wall time: 29.8 μs

# 3. Сортировка с ключом (лямбда) с использованием sorted()
%%time
str_list1 = "Although both functions can sort list, there are small differences".split()
result = sorted(str_list1, key=lambda str: len(str))
print(result)

# Вывод в консоли:
# ['can', 'are', 'both', 'sort', 'list,', 'there', 'small', 'Although', 'functions', 'differences']
# CPU times: user 61 μs, sys: 3 μs, total: 64 μs
# Wall time: 59.8 μs

# 4. Сортировка с ключом (лямбда) с использованием sort()
%%time
str_list2 = "Although both functions can sort list, there are small differences".split()
str_list2.sort(key=lambda str: len(str))
print(str_list2)

# Вывод в консоли:
# ['can', 'are', 'both', 'sort', 'list,', 'there', 'small', 'Although', 'functions', 'differences']
# CPU times: user 36 μs, sys: 0 ns, total: 36 μs
# Wall time: 38.9 μs
    

Так происходит потому, что метод sort() изменяет список прямо на месте, в то время как sorted() создает новый отсортированный список, сохраняя исходный нетронутым. Другими словами, порядок значений внутри a_long_list фактически уже изменился.

Однако функция sorted() более универсальна. Она может работать с любой итерируемой структурой. Поэтому, если нужно отсортировать, например, словарь (по ключам или по значениям), придется использовать sorted():

        a_dict = {'A': 1, 'B': 3, 'C': 2, 'D': 4, 'E': 5}

# 1. Дефолтная сортировка по ключам
%%time
result = sorted(a_dict) 
print(result)

# Вывод в консоли:
# ['A', 'B', 'C', 'D', 'E']
# CPU times: user 4 μs, sys: 0 ns, total: 4 μs
# Wall time: 6.91 μs

# 2. Cортировка по значениям, результат в виде списка кортежей
%%time
result = sorted(a_dict.items(), key=lambda item: item[1]) 
print(result)

# Вывод в консоли:
# [('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)]
# CPU times: user 7 μs, sys: 0 ns, total: 7 μs
# Wall time: 8.82 μs

# 3. Сортировка по значениям, результат в виде словаря
%%time
result = {key: value for key, value in sorted(a_dict.items(), key=lambda item: item[1])}
print(result)

# Вывод в консоли:
# {'A': 1, 'C': 2, 'B': 3, 'D': 4, 'E': 5}
# CPU times: user 8 μs, sys: 0 ns, total: 8 μs
# Wall time: 11.2 μs
    

3. Литералы вместо функций

Когда нужен пустой словарь или список, вместо dict() или list(), можно напрямую вызвать {} и [] (для пустого множества все еще нужна функция set()). Этот прием не обязательно ускорит ваш код, но сделает его более «pythonic».

        # 1. Создание пустого словаря с помощью dict()
%%time
sorted_dict1 = dict()
for key, value in sorted(a_dict.items(), key=lambda item:item[1]):
  sorted_dict1[key] = value

# Вывод в консоли:
# CPU times: user 10 μs, sys: 0 ns, total: 10 μs
# Wall time: 12.2 μs

# 2. Создание пустого словаря с помощью литерала словаря
%%time
sorted_dict2 = {}
for key, value in sorted(a_dict.items(), key=lambda item:item[1]):
  sorted_dict2[key] = value

# Вывод в консоли:
# CPU times: user 9 μs, sys: 0 ns, total: 9 μs
# Wall time: 11 μs

# 3. Создание пустого списка с помощью list()
%%time
list()

# Вывод в консоли:
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 3.81 μs

# 4. Создание пустого списка с помощью литерала списка
%%time
[]

# Вывод в консоли:
# CPU times: user 2 μs, sys: 0 ns, total: 2 μs
# Wall time: 3.1 μs
    

4. Генераторы списков

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

Например, отберём все чётные числа из списка another_long_list:

        even_num = []
for number in another_long_list:
    if number % 2 == 0:
        even_num.append(number)

    

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

        import random
random.seed(666)
another_long_list = [random.randint(0,500) for i in range(1000000)]

# 1. Создание нового списка с помощью цикла for
%%time
even_num = []
for number in another_long_list:
  if number % 2 == 0:
    even_num.append(number)

# Вывод в консоли:
# CPU times: user 113 ms, sys: 3.55 ms, total: 117 ms
# Wall time: 117 ms

# 2. Создание нового списка с помощью генератора списка
%%time
even_num = [number for number in another_long_list if number % 2 == 0]

# Вывод в консоли:
# CPU times: user 56.6 ms, sys: 3.73 ms, total: 60.3 ms
# Wall time: 64.8 ms
    

Сочетая это правило с Правилом #3 (использование литералов), мы легко можем превратить список в словарь или множество, просто изменив скобки:

        a_dict = {'A': 1, 'B': 3, 'C': 2, 'D': 4, 'E': 5}

sorted_dict3 = {key: value for key, value 
  in sorted(a_dict.items(), key=lambda item: item[1])}
print(sorted_dict3)

# Вывод в консоли:
# {'A': 1, 'C': 2, 'B': 3, 'D': 4, 'E': 5}
    

Разберёмся в коде:

  • Выражение sorted(a_dict.items(), key=lambda item: item[1]) возвращает список кортежей [('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)].
  • Далее мы распаковываем кортежи и присваиваем первый элемент каждого кортежа в переменную key, а второй – в переменную value.
  • Наконец, сохраняем каждую пару keyvalue в словаре.

5. enumerate() для значения и индекса

Иногда при переборе списка нужны и значения, и их индексы. Чтобы вдвое ускорить код используйте enumerate() для превращения списка в пары индекс-значение:

        import random
random.seed(666)
a_short_list = [random.randint(0,500) for i in range(5)]

# 1. Получение индексов с помощью использования длины списка
%%time
for i in range(len(a_short_list)):
  print(f'number {i} is {a_short_list[i]}')

# Вывод в консоли:
# number 0 is 233
# number 1 is 462
# number 2 is 193
# number 3 is 222
# number 4 is 145
# CPU times: user 189 μs, sys: 123 μs, total: 312 μs
# Wall time: 214 μs

# 2. Получение индексов с помощью enumerate()
for i, number in enumerate(a_short_list):
  print(f'number {i} is {number}')

# Вывод в консоли:
# number 0 is 233
# number 1 is 462
# number 2 is 193
# number 3 is 222
# number 4 is 145
# CPU times: user 72 μs, sys: 15 μs, total: 87 μs
# Wall time: 90.1 μs


    

6. zip() для перебора нескольких списков

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

        list1 = ['a', 'b', 'c', 'd', 'e']
list2 = ['1', '2', '3', '4', '5']

pairs_list = [pair for pair in zip(list1, list2)]
print(pairs_list)

# Вывод в консоли:
[('a', '1'), ('b', '2'), ('c', '3'), ('d', '4'), ('e', '5')]
    

Обратите внимание, списки должны быть одинаковой длины, так как функция zip() останавливается, когда заканчивается более короткий список.

И наоборот, чтобы получить доступ к элементам каждого кортежа, мы можем распаковать список кортежей, добавив звездочку (*) и используя множественное присваивание:

        # 1. Распаковка списка кортежей с помощью zip()
%%time
letters1, numbers1 = zip(*pairs_list)
print(letters1, numbers1)

# Вывод в консоли:
('a', 'b', 'c', 'd', 'e') ('1', '2', '3', '4', '5')
# CPU times: user 5 μs, sys: 1e+03 ns, total: 6 μs
# Wall time: 6.91 μs

# 2. Распаковка списка кортежей простым перебором
letters2 = [pair[0] for pair in pairs_list]
numbers2 = [pair[1] for pair in pairs_list]
print(letters2, numbers2)

# Вывод в консоли:
['a', 'b', 'c', 'd', 'e'] ['1', '2', '3', '4', '5']
# CPU times: user 5 μs, sys: 1e+03 ns, total: 6 μs
# Wall time: 7.87 μs
    

7. Комбинация set() и in

Если нужно проверить, содержит ли список некоторое значение, можно написать такую неуклюжую функцию:

        import random
random.seed(666)
another_long_list = [random.randint(0,500) for i in range(1000000)]

def check_membership(n):
    for element in another_long_list:
        if element == n:
            return True
    return False
    

Однако есть более характерный для Python способ сделать это – использовать оператор in:

        # 1. Проверка наличия значения в списке перебором элементов
%%time
check_membership(900)

# Вывод в консоль
# CPU times: user 29.7 ms, sys: 847 μs, total: 30.5 ms
# Wall time: 30.2 ms

# 2. Проверка наличия значения в списке с помощью in
900 in another_long_list

# Вывод в консоль
# CPU times: user 10.2 ms, sys: 79 μs, total: 10.3 ms
# Wall time: 10.3 ms
    

Повысить эффективность можно предварительным удалением из списка дубликатов с помощью set. Таким образом, мы сократим количество элементов для проверки. Кроме того, оператор in очень быстро работает с множествами.

        # Убираем дубликаты
check_list = set(another_long_list)

# Вывод в консоль
# CPU times: user 19.8 ms, sys: 204 μs, total: 20 ms
# Wall time: 20 ms

# Проверяем наличие значения в списке
900 in check_list

# Вывод в консоль
# CPU times: user 2 μs, sys: 0 ns, total: 2 μs
# Wall time: 5.25 μs
    

Преобразование списка в множество заняло 20 мс. Но это одноразовые затраты. Зато сама проверка заняла 5 мкс – то есть в 2 тыс. раз меньше, что становится важным при частых обращениях.

8. Проверка на True

Практически в любой программе необходимо проверять, являются ли переменные/списки/словари/… пустыми. На этих проверках тоже можно немножко сэкономить.

Не следует явно указывать == True или is True в условии if, достаточно указать имя проверяемой переменной. Это экономит ресурсы, которые использует «магическая» функция __eq__ для сравнения значений.

        string_returned_from_function = 'Hello World'

# 1. Явная проверка на равенство
%%time
if string_returned_from_function == True:
  pass

# Вывод в консоль
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 5.01 μs

# 2. Явная проверка с использованием оператора is
%%time
if string_returned_from_function is True:
  pass

# Вывод в консоль
# CPU times: user 2 μs, sys: 1 ns, total: 3 μs
# Wall time: 4.05 μs

# 3. Неявное равенство
%%time
if string_returned_from_function:
  pass

# Вывод в консоль
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 4.05 μs
    

Аналогично можно проверять обратное условие, добавив оператор not:

        if not string_returned_from_function:
  pass
    

9. Подсчет уникальных значений с Counter()

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

        %%time
num_counts = {}
for num in a_long_list:
    if num in num_counts:
        num_counts[num] += 1
    else:
        num_counts[num] = 1

# Вывод в консоль
# CPU times: user 448 ms, sys: 1.77 ms, total: 450 ms
# Wall time: 450 ms
    

Однако более эффективный способ для решения этой задачи – использование Counter() из модуля collections. Весь код при этом уместится в одной строчке:

        %%time
num_counts2 = Counter(a_long_list)

# Вывод в консоль
# CPU times: user 40.7 ms, sys: 329 μs, total: 41 ms
# Wall time: 41.2 ms
    

Этот фрагмент будет работать примерно в 10 раз быстрее, чем предыдущий.

У Counter также есть удобный метод most_common, позволяющий получить самые часто встречающиеся значения:

        for number, count in num_counts2.most_common(10):
  print(number, count)

# Вывод в консоль
29 19831
47 19811
7 19800
36 19794
14 19761
39 19748
32 19747
16 19737
34 19729
33 19729

    

Одним словом, collections – это замечательный модуль, который должен быть в базовом наборе инструментов любого Python-разработчика. Не поленитесь прочитать наше руководство по применению модуля.

10. Цикл for внутри функции

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

        def compute_cubic1(number):
  return number**3

%%time
new_list_cubic1 = [compute_cubic1(number) for number in a_long_list]

# Вывод в консоль
# CPU times: user 335 ms, sys: 14.3 ms, total: 349 ms
# Wall time: 354 ms
    

Однако правильнее будет перевернуть конструкцию – и поместить цикл внутрь функции.

        def compute_cubic2():
  return [number**3 for number in a_long_list]

%%time
new_list_cubic2 = compute_cubic2()

# Вывод в консоль
# CPU times: user 261 ms, sys: 15.7 ms, total: 277 ms
# Wall time: 277 ms
    

В данном примере для миллиона итераций (длина a_long_list) мы сэкономили около 22% времени.

***

Будем рады, если вы поделитесь в комментариях своими подходами к ускорению кода в Python. Вот ещё несколько статей, которые могут вас заинтересовать:

  • Не изобретать велосипед, или Обзор модуля collections в Python
  • Назад в будущее: практическое руководство по путешествию во времени с Python
  • Как написать код, который полюбят все

На Python пишут как десктопные программы, так и высокопрофессиональные web-приложения. Он является интерпретируемым языком и благодаря этому можно использовать продвинутые инструменты. Например, интроспекцию и метапрограммирование.

Но Python накладывает и некоторые ограничения, одно из них — снижение скорости работы по сравнению с программами, написанными на компилируемых языках программирования (C++ и др).

Андрей Смирнов

Андрей Смирнов


Python-разработчик, преподаватель по финансовой грамотности в Московской школе программистов (МШП)

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

Исходные данные (демо-приложение)

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

А в качестве стартового кода мы возьмём задачу: имеется магазин, продающий определенные товары. Товар характеризуется тремя величинами: название, цена, валюта. Необходимо реализовать хранилище товаров, заполнить его некими товарами.

На языке Python такая задача решается быстро:

Как ускорить приложения на Python

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

Профилирование

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

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

Профилирование по времени

Сначала добавим в нашу программу измерение скорости её работы. Для этого в Python есть специальная функция time, находящаяся в одноименном модуле. Идея использования этой функции очень проста: мы изменяем текущее время в начале работы программы и в конце. Далее считаем дельту, которая будет являться длительностью работы программы.

Как ускорить приложения на Python

И ещё несколько пунктов, которые обязательно нужно сказать про этот код:

  1. В ходе профилирования нет смысла измерять время работы кода, ответственного за ввод данных с клавиатуры, чтение из файла, получение данных из сетевого хранилища и т.д. Эти операции априори будут медленными из-за низкой скорости передачи данных по сравнению с аналогичной скоростью в передачи данных в ОЗУ компьютера. Если вы понимаете, что проблема низкой скорости кроется в коде ввода данных, тогда его нужно профилировать отдельно от основной программы.
  2. Одна и та же программа, запущенная два раза, практически никогда не выдаст идентичное время выполнения. Это происходит из-за того, что программа выполняется в операционной системе, в которой постоянно работают фоновые процессы. И чаще всего отключить все лишние процессы невозможно. В таком случае, чтобы минимизировать их влияние, достаточно всего лишь запустить программу многократно и посчитать среднее время выполнения (что и сделано в коде).

Этот код при запуске показал следующие тайминги:

Как ускорить приложения на Python

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

  • Intel Core i7-7700HQ
  • 16Gb RAM
  • KUbuntu 22.04

Ещё немного про профилирование по времени и сразу же первая оптимизация

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

Но эта проблема решаема с помощью встроенного в Python средства профилирования, идеально подходящего для такой ситуации — утилиты cProfile. Она способна не просто запустить код и рассчитать время его работы, но и рассчитать время работы каждого отдельного метода (включая даже низкоуровневые методы создания списков, выделения памяти, добавления объектов и т.д.).

Для того, чтобы запустить cProfile, не требуется менять код. Достаточно просто запустить программу на исполнение с подключением дополнительного модуля:

python3 -m cProfile main.py

В таком случае вся программа выполнится и после неё будет выведена детальная информация о времени выполнения каждой функции:

Как ускорить приложения на Python

Сразу же есть две мысли:

  1. Наличие любого дополнительного профилировщика замедляет программу. Это происходит, потому что любой профилировщик добавляет свой исполняемый код, благодаря которому и собирается статистика выполнения. В результате этого среднее время выполнения нашей программы увеличилось с 2.46 до 3.26 секунд.
  2. Сразу же можно заметить, что больше всего раз вызывается метод list.append, который добавляет новый объект в список. И именно на этом месте появляется идея для оптимизации: если мы заранее знаем, что объектов будет добавляться именно три миллиона, что мешает нам создать заранее список такого размера?

Попробуем изменить код так, чтобы список создавался сразу:

Как ускорить приложения на Python

Запустим его также с использованием cProfile. И что же мы видим?

Как ускорить приложения на Python

Среднее время уменьшилось до 2.06 секунд, и это со включённым профилировщиком. А без него будет так вообще 1.66! И всё путём простейшей оптимизации.

Профилирование по памяти

Также сразу же добавим в наш код профилирование по памяти, так как очень интересно узнать “сколько же занимает в памяти три миллиона товаров”. Для подсчёта памяти будем использовать библиотеку pympler.

Как ускорить приложения на Python

И такой код при размере хранилища в три миллиона товаров показал следующие результаты:

Размер хранилища: 176000864 байт или 167.85 мегабайт

Вы можете заметить, что я убрал из кода подсчёт по времени. Причина проста: pympler для подсчёта количества занимаемой памяти проходит по всем имеющимся структурам данных, и во время подсчёта скорость выполнения увеличивается раз в пять, делая профилирование по времени неоправданным.

А теперь, когда мы достаточно знаем о поведении нашего приложения (и во времени, и в памяти) — приступим к его последовательной оптимизации.

Способы оптимизации

Оптимизация структур данных

Начнём мы с несколько нестандартной оптимизации, а именно — залезем внутрь нашего объекта товара и основательно там покопаемся.

Сейчас объект представлен в виде обыкновенного класса. Давайте подумаем, а возможно ли здесь использовать какую-нибудь иную структуру данных, которая построена на основе класса, но имеет дополнительный функционал? И такая структура есть, она называется датакласс. Правда, сразу стоит оговориться, что обычный датакласс является небольшой надстройкой над обычным классом, в которой разработчики языка чётко указали, какие будут поля и какие они будут иметь типы данных. А нам будет интересен датакласс с фиксированными полями, в который невозможно добавить новые поля.

Почему это важно? Для того, чтобы иметь возможность добавлять и удалять поля в рантайме, в классах питона реализована структура словаря __dict__. А это, в свою очередь, далеко не всегда является необходимым функционалом.

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

Реализуем эту идею (для этого определим кортеж __slots__).

Как ускорить приложения на Python

Если этот код запустить и проверить время выполнения, то мы получим ускорение в среднем на 25 процентов

Как ускорить приложения на Python

А если директиву __slots__ указать в коде, который мы профилировали по памяти, то результаты получатся ещё более сногсшибательными:

Размер хранилища: 80000504 байт или 76.29 мегабайт

То есть, путём отказа от динамического добавления элементов мы сразу уменьшили расходы памяти нашего приложения вдвое!

И на этом мы не остановимся.

Оптимизация интерпретатора

Следующая оптимизация, которая может помочь нам в достижении нашей цели — замена интерпретатора Python на интерпретатор PyPy.

Согласно определению из Википедии, PyPy — это интерпретатор языка Python, написанный на языке Python. Однако в него встроен трассирующий JIT-компилятор, способный преобразовывать код на Python в машинный код прямо во время выполнения программы. Эта особенность позволяет ему существенно ускорить процесс исполнения программы без каких либо изменений кода.

Установим pypy следующей командой:

sudo apt install pypy3

А после этого запустим код с его помощью:

pypy3 main.py

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

Время исполнения уменьшилось ещё на 68%. И для такого запуска абсолютно не потребовалось менять исходный код.

Справедливости ради нужно заметить, что за счёт глубинной оптимизации некоторые сторонние библиотеки, которыми вы можете пользоваться, не смогут запуститься в pypy. И для них придётся искать аналоги. Но самые популярные библиотеки (такие как twisted, django, numpy, scikit-learn и другие) им полностью поддерживаются и работоспособны.

А как ещё можно оптимизировать?

В мире существуют и другие способы оптимизации, но они уже относятся к категории радикальных, подразумевающих кардинальное изменение структуры кода и (или) даже языка программирования. Среди них:

  • изменение структуры хранимых данных со списка объектов на pandas.DataFrame.
  • добавление строгой типизации и адаптация кода под компилятор cython
  • распараллеливание программы на потоки при помощи Nvidia CUDA.
  • И, наконец, если затраты от потерь производительности существенно превышают затраты от кардинальной переработки кода, можно попробовать переписать критичные части кода на языке C++ и оформить их в виде библиотеки, функции из которой можно запустить из Python-кода.

Итоги

Итак, в ходе нашего увлекательного путешествия мы

  • написали код
  • измерили его производительность (по памяти и по времени)
  • оптимизировали его несколько раз
  • результаты в виде графиков приведены ниже.

Как ускорить приложения на Python

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

Благодарю за внимание.

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

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

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

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

В этой статье мы оптимизируем общие шаблоны и процедуры в программировании на Python, чтобы повысить производительность и улучшить использование доступных вычислительных ресурсов.

Проблема с производительностью

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

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

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

Несогласованность и ошибочный вывод – еще один результат плохо оптимизированных программ. Эти моменты подчеркивают необходимость оптимизации программ.

Зачем и когда нужна оптимизация?

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

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

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

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

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

Профилирование

Прежде чем мы сможем оптимизировать наш код, он должен работать. Таким образом, мы сможем узнать, как он работает и использует ресурсы. И это подводит нас к первому правилу оптимизации – не надо.

Как сказал Дональд Кнут – математик, компьютерный ученый и профессор Стэнфордского университета:

«Преждевременная оптимизация – корень всех зол».

Решение должно работать, чтобы его можно было оптимизировать.

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

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

  • PyCallGraph – который создает визуализации графа вызовов, которые представляют отношения вызовов между подпрограммами для кода Python.
  • cProfile – который описывает, как часто и как долго выполняются различные части кода.
  • gProf2dot – это библиотека, которая визуализирует вывод профилировщика в виде точечного графика.

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

Выбор структур данных и потока управления

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

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

На время и использование памяти сильно повлияет наш выбор структуры данных. Также важно отметить, что некоторые структуры данных по-разному реализованы на разных языках программирования.

Для циклов и списков

Циклы являются обычным явлением при разработке на Python, и довольно скоро вы столкнетесь с пониманием списков, которые являются кратким способом создания новых списков, которые также поддерживают условия.

Например, если мы хотим получить список квадратов всех четных чисел в определенном диапазоне, используя цикл for:

new_list = []
for n in range(0, 10):
    if n % 2 == 0:
        new_list.append(n**2)

Версия цикла со списком понимания будет простой:

new_list = [ n**2 for n in range(0,10) if n%2 == 0]

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

Давайте сопоставим понимание списка с эквивалентом цикла for и посмотрим, сколько времени требуется каждому для достижения одного и того же результата:

import timeit

def for_square(n):
    new_list = []
    for i in range(0, n):
        if i % 2 == 0:
            new_list.append(n**2)
    return new_list

def list_comp_square(n):
    return [i**2 for i in range(0, n) if i % 2 == 0]

print("Time taken by For Loop: {}".format(timeit.timeit('for_square(10)', 'from __main__ import for_square')))

print("Time taken by List Comprehension: {}".format(timeit.timeit('list_comp_square(10)', 'from __main__ import list_comp_square')))

После запуска скрипта 5 раз с использованием Python 2:

$ python for-vs-lc.py 
Time taken by For Loop: 2.56907987595
Time taken by List Comprehension: 2.01556396484
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 2.37083697319
Time taken by List Comprehension: 1.94110512733
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 2.52163410187
Time taken by List Comprehension: 1.96427607536
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 2.44279003143
Time taken by List Comprehension: 2.16282701492
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 2.63641500473
Time taken by List Comprehension: 1.90950393677

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

Если увеличить диапазон квадратов с 10 до 100, разница станет более очевидной:

$ python for-vs-lc.py 
Time taken by For Loop: 16.0991549492
Time taken by List Comprehension: 13.9700510502
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 16.6425571442
Time taken by List Comprehension: 13.4352738857
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 16.2476081848
Time taken by List Comprehension: 13.2488780022
$ 
$ python for-vs-lc.py 
Time taken by For Loop: 15.9152050018
Time taken by List Comprehension: 13.3579590321

cProfile – это профилировщик, который поставляется с Python, и если мы используем его для профилирования нашего кода:

Анализ cprofile

При дальнейшем рассмотрении мы все еще можем видеть, что инструмент cProfile сообщает, что наше понимание списка требует меньше времени на выполнение, чем наша реализация цикла For Loop, как мы установили ранее. cProfile отображает все вызванные функции, количество их вызовов и время, затраченное каждой из них.

Если наше намерение состоит в том, чтобы сократить время, затрачиваемое на выполнение нашего кода, то лучше использовать List Component, чем цикл For Loop. Эффект от такого решения по оптимизации нашего кода будет намного яснее в большем масштабе и покажет, насколько важной, но в то же время простой может быть оптимизация кода.

Но что, если нас беспокоит использование памяти? Понимание списка потребует больше памяти для удаления элементов в списке, чем обычный цикл. Понимание списка всегда создает новый список в памяти по завершении, поэтому для удаления элементов из списка будет создан новый список. В то время как для обычного цикла for мы можем использовать list.remove() или list.pop() для изменения исходного списка вместо создания нового в памяти.

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

Связанные списки

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

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

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

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

Диапазон против XRange

При работе с циклами в Python иногда нам нужно сгенерировать список целых чисел, чтобы помочь нам в выполнении циклов for. Для этого используются функции range и xrange.

Их функциональность такая же, но они отличаются тем, что диапазон возвращает объект списка, а xrange возвращает объект xrange.

Что это значит? Объект xrange является генератором в том смысле, что это не окончательный список. Это дает нам возможность генерировать значения в ожидаемом окончательном списке по мере необходимости во время выполнения с помощью метода, известного как «уступка».

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

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

Давайте исследуем разницу в потреблении памяти между двумя функциями:

$ python
Python 2.7.10 (default, Oct 23 2015, 19:19:21) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> 
>>> r = range(1000000)
>>> x = xrange(1000000)
>>> 
>>> print(sys.getsizeof(r))
8000072
>>> 
>>> print(sys.getsizeof(x))
40
>>> 
>>> print(type(r))
<type 'list'>
>>> print(type(x))
<type 'xrange'>

Мы создаем диапазон из 1000000 целых чисел, используя range и xrange. Тип объекта, созданного функцией диапазона, – это список, который потребляет 8000072 байта памяти, в то время как объект xrange потребляет только 40 байтов памяти.

Функция xrange экономит много памяти, но как насчет времени поиска элемента? Давайте рассчитаем время поиска целого числа в сгенерированном списке целых чисел с помощью Timeit:

import timeit

r = range(1000000)
x = xrange(1000000)

def lookup_range():
    return r[999999]

def lookup_xrange():
    return x[999999]

print("Look up time in Range: {}".format(timeit.timeit('lookup_range()', 'from __main__ import lookup_range')))

print("Look up time in Xrange: {}".format(timeit.timeit('lookup_xrange()', 'from __main__ import lookup_xrange')))

Результат:

$ python range-vs-xrange.py 
Look up time in Range: 0.0959858894348
Look up time in Xrange: 0.140854120255
$ 
$ python range-vs-xrange.py 
Look up time in Range: 0.111716985703
Look up time in Xrange: 0.130584001541
$ 
$ python range-vs-xrange.py 
Look up time in Range: 0.110965013504
Look up time in Xrange: 0.133008003235
$ 
$ python range-vs-xrange.py 
Look up time in Range: 0.102388143539
Look up time in Xrange: 0.133061170578

xrange может потреблять меньше памяти, но для поиска в нем элемента требуется больше времени. Учитывая ситуацию и доступные ресурсы, мы можем выбрать диапазон или xrange в зависимости от того, к чему мы стремимся. Это подтверждает важность профилирования для оптимизации нашего кода Python.

Примечание. Xrange устарела в Python 3, и функция диапазона теперь может выполнять те же функции. Генераторы по-прежнему доступны в Python 3 и могут помочь нам сэкономить память другими способами, такими как генераторы понимания или выражений.

Наборы

При работе со списками в Python нужно помнить, что они допускают повторяющиеся записи. Что, если имеет значение, содержат ли наши данные дубликаты?

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

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

Сравним две операции:

import timeit

# here we create a new list and add the elements one by one
# while checking for duplicates
def manual_remove_duplicates(list_of_duplicates):
    new_list = []
    [new_list.append(n) for n in list_of_duplicates if n not in new_list]
    return new_list

# using a set is as simple as
def set_remove_duplicates(list_of_duplicates):
    return list(set(list_of_duplicates))

list_of_duplicates = [10, 54, 76, 10, 54, 100, 1991, 6782, 1991, 1991, 64, 10]

print("Manually removing duplicates takes {}s".format(timeit.timeit('manual_remove_duplicates(list_of_duplicates)', 'from __main__ import manual_remove_duplicates, list_of_duplicates')))

print("Using Set to remove duplicates takes {}s".format(timeit.timeit('set_remove_duplicates(list_of_duplicates)', 'from __main__ import set_remove_duplicates, list_of_duplicates')))

После пяти запусков скрипта:

$ python sets-vs-lists.py 
Manually removing duplicates takes 2.64614701271s
Using Set to remove duplicates takes 2.23225092888s
$ 
$ python sets-vs-lists.py 
Manually removing duplicates takes 2.65356898308s
Using Set to remove duplicates takes 1.1165189743s
$ 
$ python sets-vs-lists.py 
Manually removing duplicates takes 2.53129696846s
Using Set to remove duplicates takes 1.15646100044s
$ 
$ python sets-vs-lists.py 
Manually removing duplicates takes 2.57102680206s
Using Set to remove duplicates takes 1.13189387321s
$ 
$ python sets-vs-lists.py 
Manually removing duplicates takes 2.48338890076s
Using Set to remove duplicates takes 1.20611810684s

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

Это может быть полезно при фильтрации записей для розыгрыша призов, когда мы должны отфильтровывать повторяющиеся записи. Если для фильтрации 120 записей требуется 2 секунды, представьте себе фильтрацию 10 000 записей. В таком масштабе значительно возросшая производительность, предоставляемая с помощью Sets.

Это может происходить нечасто, но при необходимости может иметь огромное значение. Правильное профилирование может помочь нам определить такие ситуации и существенно повлиять на производительность нашего кода.

Конкатенация строк

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

Мы можем использовать + (плюс) для соединения строк. Это идеально подходит для нескольких объектов String и не масштабируется. Если вы используете оператор + для объединения нескольких строк, каждое объединение создаст новый объект, поскольку строки неизменяемы. Это приведет к созданию множества новых объектов String в памяти, следовательно, к неправильному использованию памяти.

Мы также можем использовать оператор конкатенации + = для объединения строк, но это работает только для двух строк одновременно, в отличие от оператора +, который может объединять более двух строк.

Если у нас есть итератор, такой как List, который имеет несколько строк, идеальный способ объединить их – использовать метод .join().

Давайте создадим список из тысячи слов и сравним, как сравниваются .join() и оператор + =:

import timeit

# create a list of 1000 words
list_of_words = ["foo "] * 1000

def using_join(list_of_words):
    return "".join(list_of_words)

def using_concat_operator(list_of_words):
    final_string = ""
    for i in list_of_words:
        final_string += i
    return final_string

print("Using join() takes {} s".format(timeit.timeit('using_join(list_of_words)', 'from __main__ import using_join, list_of_words')))

print("Using += takes {} s".format(timeit.timeit('using_concat_operator(list_of_words)', 'from __main__ import using_concat_operator, list_of_words')))

После двух попыток:

$ python join-vs-concat.py 
Using join() takes 14.0949640274 s
Using += takes 79.5631570816 s
$ 
$ python join-vs-concat.py 
Using join() takes 13.3542580605 s
Using += takes 76.3233859539 s

Очевидно, что метод .join() не только более аккуратный и читаемый, но и значительно быстрый, чем оператор конкатенации, при соединении строк в итераторе.

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

Заключение

Мы установили, что оптимизация кода имеет решающее значение для Python, а также увидели разницу, когда она масштабируется. С помощью модуля Timeit и профилировщика cProfile мы смогли определить, какая реализация требует меньше времени для выполнения, и подкрепили ее цифрами. Структуры данных и структуры потока управления, которые мы используем, могут сильно повлиять на производительность нашего кода, и нам следует быть более осторожными.

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

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


Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/

Введение

Зачастую скорость выполнения 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 
@dataclass
class 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 asizeof
print("Размер магазина:", 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': 'Москва'}

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

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

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

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__):

import timeit
code = """
class ShopClass:
    #__slots__ = ("name", "listGoods")
    def __init__(self, name=""):
        self.name = name
        self.listGoods = []
@dataclass
class DataGoods:
    #__slots__ = ("name", "price", "unit")
    name:str
    price:int
    unit:str
shop = 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 time
class ShopClass:
   __slots__ = ("name", "listGoods")
   def __init__(self, name=""):
      self.name = name
      self.listGoods = []
@dataclass
class DataGoods:
   __slots__ = ("name", "price", "unit")
   name: str
   price: int
   unit: str
shop = 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.49887752532959
telephoneSum, televizorSum, tosterSum = 0, 0, 0
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("ВРЕМЯ НА ПОДСЧЁТ СУММ 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 = 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: {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, по максимуму избавиться от циклов в циклах в циклах в цикле, очищать руками память и удалять ненужные элементы по ходу выполнения кода. Не стоит ожидать, что переписав ваш код на другой язык — это решит все ваши проблемы, учитесь искать узкие места в коде и оптимизировать их алгоритмически или при помощи фишек самого языка.

As we know, Python programming language is a bit slow and the target is to speed it up without the assistance of more extreme solutions, such as C extensions or a just-in-time (JIT) compiler.
While the first rule of optimization might be to “not do it”, the second rule is almost certainly “don’t optimize the unimportant.” To that end, if the program is running slow, one might start by profiling the code. More often than not, one finds that the program spends its time in a few hotspots, such as inner data processing loops. Once those locations are identified, the no-nonsense techniques can be used to make the program run faster. 
A lot of programmers start using Python as a language for writing simple scripts. When writing scripts, it is easy to fall into a practice of simply writing code with very little structure.
Code #1: Taking this code into consideration. 
 

Python3

import sys

import csv

with open(sys.argv[1]) as f:

    for row in csv.reader(f):

A little-known fact is that code defined in the global scope like this runs slower than code defined in a function. The speed difference has to do with the implementation of local versus global variables (operations involving locals are faster). So, simply put the scripting statements in a function to make the program run faster. 

  Code #2 : 

Python3

import sys

import csv

def main(filename):

    with open(filename) as f:

        for row in csv.reader(f):

main(sys.argv[1])

The speed difference depends heavily on the processing being performed, but the speedups of 15-30% are not uncommon.
 

Selectively eliminate attribute access –

Every use of the dot (.) operator to access attributes comes with a cost. Under the covers, this triggers special methods, such as __getattribute__() and __getattr__(), which often lead to dictionary lookups.
One can often avoid attribute lookups by using the from module import name form of import as well as making selected use of bound methods as shown in the code fragment given below – 
Code #3 : 
 

Python3

import math

def compute_roots(nums):

    result = []

    for n in nums:

        result.append(math.sqrt(n))

    return result

nums = range(1000000)

for n in range(100):

    r = compute_roots(nums)

Output : 
 

This program runs in about 40 seconds when running on the machine.


Code #4 : Change the compute_roots() function 
 

Python3

from math import sqrt

def compute_roots(nums):

    result = []

    result_append = result.append

    for n in nums:

        result_append(sqrt(n))

    return result

Output : 
 

This program runs in about 29 seconds when running on the machine.



The only difference between the two versions of code is the elimination of attribute access. Instead of using math.sqrt(), the code uses sqrt(). The result.append() method is additionally placed into a local variable re 
sult_append and reused in the inner loop. 
However, it must be emphasized that these changes only make sense in frequently executed code, such as loops. So, this optimization really only makes sense in carefully selected places.
 

Understand locality of variables –

As previously noted, local variables are faster than global variables. For frequently accessed names, speedups can be obtained by making those names as local as possible.
Code #5 : Modified version of the compute_roots() function 
 

Python3

import math

def compute_roots(nums):

    sqrt = math.sqrt

    result = []

    result_append = result.append

    for n in nums:

        result_append(sqrt(n))

    return result

In this version, sqrt has been lifted from the math module and placed into a local variable. This code will run about 25 seconds (an improvement over the previous version, which took 29 seconds). That additional speedup is due to a local lookup of sqrt being a bit faster than a global lookup of sqrt. 
Locality arguments also apply when working in classes. In general, looking up a value such as self.name will be considerably slower than accessing a local variable. In inner loops, it might pay to lift commonly accessed attributes into a local variable as shown in the code given below. 
Code #6 : 
 

Python3

class SomeClass:

    ...

    def method(self):

        for x in s:

            op(self.value)

class SomeClass:

    ...

    def method(self):

        value = self.value

        for x in s:

            op(value)

Dynamic Typing:

The reason Python is slow is because it’s dynamically typed now we’re going to talk about this more in detail but I want to give a comparison to a language like Java. Now in Java, everything is statically typed and this language is actually compiled before it runs, unlike Python that’s compiled at runtime through an interpreter. Now what happens in Java is when you write code, you need to define what type each of your variables is going to be, what type your methods and functions are going to be returning and you pretty much have to define exactly what everything’s going to be throughout your code. Now although this leads to much longer development times and takes a much longer time to write your code but what it does is increase efficiency when you are compiling, now the reason this actually works and the reason it works so much faster than Python code is because if you know the type that a specific variable or object is going to be, you can perform a ton of different optimizations and avoid performing a ton of different checks while you’re actually running the code because these checks are performed at compile time in Java essentially you can’t compile any Java code that hasn’t actual or even just like typed errors while you’re writing that code you are going to try to compile it and it would say like this type isn’t accurate, you can’t do this, you can’t compile it because it knows that when it comes to runtime that’s not going to work so essentially all of these checks that actually needs to be performed in Python when the code is running are performed beforehand and there’s just a ton of optimization done because of this statically typed length. Now one may ask a question like, Why doesn’t Python do this? Answer to this would be Python is dynamically typed which simply means that any variable can change its type and can change it’s value at any point in the program while it’s running which means that we can’t actually compile the entire program beforehand because we can’t do all of these checks at once because we don’t know what type these variables are going to be, they are going to change at runtime, different things are going to happen and because of that we can’t get all these optimization that we might have in a lower level language like Java, C or C++ and that is kind of the fundamental reason the language is slow, this dynamic typing and any fast language is going to have a compiler that’s going to run through, it’s going to make sure that everything is good, it’s going to do all these checks before it actually ends up running the code at runtime where what happens in Python is all of your code is actually compiled and checked at runtime so rather than compiling it before and taking all that time beforehand while you’re running the code , many different checks are happening to make sure that say this object is correct, these types are proper, everything is working the same. 

Concurrency:

Now the next thing to talk about is obviously the lack of concurrency in Python. This is going to be the major kind of factor on speed, if you’re writing an application in Java, C, you can spread everything out throughout multiple threads which allows you to utilize all the cores of your CPU so to break this down in modern-day computing most of us have four core CPUs or higher and that allows us to actually run four tasks at the exact same time concurrently now with Python this isn’t possible. Python says, well for each interpreter we can have at most one thread running at a time and a thread is just some kind of operation that’s happening on the CPU core so that means even if we create many threads in our Python program we can only be using one CPU core while in a Java program or a C program could be using all eight or be using all four which will obviously lead to 4X or 8X increase in speed, now we can get around this in Python by using multiprocessing, but there are some issues with that.

Понравилась статья? Поделить с друзьями:

Другие крутые статьи на нашем сайте:

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии