Хабрахабр

Python потребляет много памяти или как уменьшить размер объектов?

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

Для простоты будем рассматривать структуры в Python для представления точки с координатами x, y, z с доступом к значениям координат по имени.

Dict

В небольших программах, особенно в скриптах, довольно просто и удобно использовать встроенный dict для представления структурной информации:

>>> ob =
>>> x = ob['x']
>>> ob['y'] = y

6 с упорядоченным набором ключей dict стал еще более привлекательным. С появлением более "компактной" реализации в Python 3. Однако, посмотрим на размер его следа в оперативной памяти:

>>> print(sys.getsizeof(ob))
240

Он занимает много памяти, особенно, если вдруг понадобится создать большое число экземпляров:

Class instance

Для любителей все облекать в классы более предпочтительным является определение в виде класса с доступом по имени атрибута:

class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3)
>>> x = ob.x
>>> ob.y = y

Интересна структура экземпляра класса:

Начиная с Python 3. Здесь __weakref__ это ссылка на список, так называемых, слабых ссылок (weak reference) на данный объект, поле __dict__ это ссылка на словарь экземпляра класса, в котором содержатся значения атрибутов экземпляра (заметим, что ссылки на 64-битной платформе занимают 8 байт). Это сокращает размер следа экземпляра в памяти: 3, используется общее пространство для хранения ключей в словаре для всех экземпляров класса.

>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112

Как результате большое количество экземпляров класса оставляют меньший след в памяти, чем обычный словарь (dict):

Нетрудно заметить, что след экземпляра в памяти все еще велик из-за размера словаря экземпляра.

Instance of class with __slots__

Это возможно при помощи "трюка" со __slots__: Существенное уменьшение следа экземпляра в памяти достигается путем исключения __dict__ и __weakref__.

class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
64

След в памяти стал существенно компактнее:

Использование __slots__ в определении класса приводит к тому, что след большого числа экземпляров в памяти существенно уменьшается:

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

Достигается такое сокращение тем, что в памяти после заголовка объекта хранятся ссылки на объекты, а доступ к ним осуществляется при помощи специальных дескрипторов (descriptor), которые находятся в словаре класса:

>>> pprint(Point.__dict__)
mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>})

Функция namedlist.namedlist создает класс по структуре идентичный классу со __slots__: Для автоматизации процесса создания класса со __slots__ существует библиотека namedlist.

>>> Point = namedlist('Point', ('x', 'y', 'z'))

Другой пакет attrs позволяет автоматизировать процесс создания классов как со __slots__, так и без него.

Tuple

Tuple это фиксированная структура или запись, но без имен полей. Для представления наборов данных в Python также есть встроенный тип tuple. Поля tuple раз и навсегда связываются с объектами-значениями в момент создания экземпляра tuple: Для доступа к полю используется индекс поля.

>>> ob = (1,2,3)
>>> x = ob[0]
>>> ob[1] = y # НЕВОЗМОЖНО

Экземпляры tuple вполне компактны:

>>> print(sys.getsizeof(ob))
72

Они занимают в памяти на 8 байт больше, чем экземпляры классов со __slots__, так как след tuple в памяти также содержит количеством полей:

Namedtuple

Ответом на этот запрос стал модуль collections.namedtuple. Так как tuple используется очень широко, то однажды возник запрос на то, чтобы можно было все-таки иметь доступ к полям и по имени тоже.

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

>>> Point = namedtuple('Point', ('x', 'y', 'z'))

Для нашего примера это будет выглядеть примерно так: Она создает подкласс tuple, в котором определены дескрипторы для доступа в полям по имени.

class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z))

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

Recordclass: мутируемый namedtuple без GC

Так как в Python нет встроенного типа, идентичного tuple, поддерживающего присваивания, то было создано множество вариантов. Так как tuple и, соответственно, namedtuple-классы порождают немутируемые объекты в том смысле, что объект значение ob.x уже нельзя связать с другим объектом-значением, то возник запрос на мутируемый вариант namedtuple. Кроме того, с его помощью можно уменьшить размер следа объекта в памяти по сравнению с размером следа объектов типа tuple. Мы остановимся на recordclass, получившем оценку на stackoverflow.

На его основе создаются подклассы, которые практически во всем идентичны namedtuples, но также поддерживают присваивание новых значений полям (не создавая новых экземпляров). В пакете recordclass вводится в обиход тип recordclass.mutabletuple, который практически во всем идентичен tuple, но также поддерживает присваивания. Функция recordclass подобно функции namedtuple позволяет автоматизировать создание таких классов:

>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)

Экземпляры класса имеют аналогичную стуктуру, что и tuple, но только без PyGC_Head:

Обычно namedtuple и recordclass используют для порождения классов, представляющих записи или простые (нерекурсивные) структуры данных. По умолчанию функция recordclass порождает класс, который не участвует в механизме циклической сборки мусора. По этой причине в следе экземпляров классов, порожденных recordclass по умолчанию, исключен фрагмент PyGC_Head, который необходим для классов, поддерживающих механизм циклической сборки мусора (более точно: в структуре PyTypeObject, соответствующей создаваемому классу в поле flags по умолчанию не установлен флаг Py_TPFLAGS_HAVE_GC). Корректное их использование в Python не порождает циклических ссылок.

Размер следа большого количества экземпляров оказывается меньше, чем у экземпляров класса со __slots__:

Dataobject

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

>>> Point = make_dataclass('Point', ('x', 'y', 'z'))

Созданный таким образом класс по умолчанию, создает мутируемые экземпляры.

Другой способ – использовать объявление класса путем наследования от recordclass.dataobject:

class Point(dataobject): x:int y:int z:int

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

>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
40

Для доступа к полям также используются специальные дескрипторы для доступа к полю по его смещению относительно начала объекта, которые размещены в словаре класса:

mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>})

Размер следа большого количества экземпляров оказывается минимально возможным для CPython :

Cython

Его достоинство состоит в том, что поля могут принимать значения типов языка C. Есть один подход, основанный на использовании Cython. Например: Дескрипторы для доступа к полям из чистого Python создаются автоматически.

cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z

В этом случае экземпляры имеют еще меньший размер памяти:

>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
32

След экземпляра в памяти имеет следующую структуру:

Размер следа большого количества экземпляров получается меньше:

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

Numpy

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

>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])

Массив и N элементов, инициализированный нулями создается при помощи функции:

>>> points = numpy.zeros(N, dtype=Point)

Размер массива минимально возможный:

Извлечение одной строки приводит к созданию массива, содержащего единственный элемент. Обычный доступ к элементам массива и строкам потребует преобразования объекта Python
в значение C int и наоборот. След его уже не будет столь компактен:

>>> sys.getsizeof(points[0]) 68

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

Заключение

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть