Создание подклассов namedtuple против реализации __slots__? - PullRequest
0 голосов
/ 08 ноября 2019

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

class Extent:
    def __init__(self, xmin, ymin, xmax, ymax):
        self.xmin = xmin
        self.ymin = ymin
        self.xmax = xmax
        self.ymax = ymax
        self.center = ((xmin + xmax) / 2, (ymin + ymax) / 2)
    def contains(self, extent: 'Extent'):
        # checks if one contains the other
    def intersects(self, extent: 'Extent'):
        # checks if one intersects the other

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

class Extent:
    __slots__ =  ('xmin', 'ymin', 'xmax', 'ymax', 'center')
    def __init__(self, xmin, ymin, xmax, ymax):
        ...

Затем я начал исследовать namedtuple, поскольку они используют довольно маленький объем памяти,Недостатком является то, что я не могу добавлять методы, если не подкласс их:1017 * Таким образом, у меня есть вопрос: какая польза от последнего примера (namedtuple подкласс с __slots__) над классом, который реализует __slots__ и не наследует от чего-либо?

1 Ответ

0 голосов
/ 11 ноября 2019

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

Можно иметь класс, который будет сохранятьданные упакованы байтами в массиве и лениво выдают ваши атрибуты в виде чисел Python по мере необходимости. Что касается памяти, это было бы победителем, так как число в Python - это полный объект с минимальным размером 24 байта (на 64-битных платформах).

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

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

Хорошая новость заключается в том, что numpy уже предлагает эту функцию - она ​​позволяет составлять массивы из объектов с пользовательскими dtype, и дажеnp.void базовый класс структурного типа, который определяется его типом d - который может иметь дополнительные функциональные возможности.

Код ниже может содержать класс "Extents" внутри массива numpy, причем каждый экземпляр принимает ровно числобайтов, необходимых для данных. С тем преимуществом, что вы можете использовать расширенные numy-типы данных - например, использовать «float32» («f4»), если память представляет собой проблему, и вам достаточно 32-битного значения FP.

Вы могли бы на самом делепросто используйте необработанный массив np с пользовательским типом dtype - этот код показывает, как вы можете добавить некоторые навороты, чтобы иметь свойство "center" и иметь возможность доступа к xmin и т. д. в качестве атрибутов, а не только с использованием синтаксиса сопоставления(seq [0] ["xmin"]):

from collections.abc import MutableSequence

import numpy as np


class Extent(np.void):
    attrs = "xmin ymin xmax ymax".split()
    dtype = np.dtype([(attr, "f8") for attr in attrs])

    def __getattr__(self, attr):
        return self.__getitem__(attr)

    def __setattr__(self, attr, value):
        if value in self.attrs:
            return self.__setitem__(self, attr, value)
        return super().__setattr__(attr, value)

    @classmethod
    def store(self, storage, xmin, ymin, xmax, ymax):
        storage.append((xmin, ymin, xmax, ymax))
        return storage[storage.last_item - 1]

    def contains(self, extent: 'Extent'):
        pass
        # checks if one contains the other
    def intersects(self, extent: 'Extent'):
        pass
        # checks if one intersects the other

    @property
    def center(self):
        return ((self.xmin + self.xmax) / 2, (self.ymin + self.ymax) / 2)

    def __repr__(self):
        return f"Extent <{self.xmin}, {self.ymin}, {self.xmax}, {self.ymax}>"


class ExtentList:
    def __init__(self, max_size):
        self.last_item = 0
        self.data = np.zeros(max_size, dtype=Extent)

    def __getitem__(self, index):
        return  self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

    def append(self, extent):
        self.data[self.last_item] = extent
        self.last_item += 1

    def __iter__(self):
        for i in range(self.last_item):
            yield self.data[i]

    def __repr__(self):
        return f"ExtentList <{self.data[:self.max_size]!r}>, max={self.max_size}"


На интерактивном терминале:


In [63]: values = ExtentList(10)                                                                                                     

In [64]: v = Extent.store(values, 10, 10, 20, 20)                                                                                    

In [65]: v                                                                                                                           
Out[65]: Extent <10.0, 10.0, 20.0, 20.0>

In [66]: v.center                                                                                                                    
Out[66]: (15.0, 15.0)

Более того - этот подход позволяет изменять ваши атрибуты на месте:

In [73]: v.xmax = 40                                                                                                                 

In [74]: v.center                                                                                                                    
Out[74]: (25.0, 15.0)

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

...