【问题标题】:Subclassing namedtuple vs implementing __slots__?子类化 namedtuple 与实现 __slots__?
【发布时间】:2019-11-08 19:28:41
【问题描述】:

我目前正在尝试优化一个可以从更少的内存占用中受益的程序。在该程序中,有一些对象存储数据并且从不改变其状态,但它们确实具有功能,例如:

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,因为它们占用的内存非常小,缺点是我无法添加方法,除非我将它们子类化:

Boundary = namedtuple('Boundary', ['xmin', 'ymin', 'xmax', 'ymax', 'center'])
class Extent(Boundary):
    def contains(self, extent: 'Extent'):
        ...
    def intersects(self, extent: 'Extent'):
        ...

但这仍然允许创建__dict__,所以我们必须明确声明__slots__

Boundary = namedtuple('Boundary', ['xmin', 'ymin', 'xmax', 'ymax', 'center'])
class Extent(Boundary):
    __slots__ = ()
    ...

所以我的问题是最后一个例子(namedtuple 子类和__slots__)相对于实现__slots__ 并且不继承任何东西的类有什么好处?

【问题讨论】:

  • 嗯,namedtupletuple。所以如果你想要一个序列类型,这是一个很好的方法。否则,您可能只想要一个有插槽的普通课程。
  • @juanpa.arrivillaga 在namedtuple 子类和带插槽的常规类的内存配置文件中有多少对比?
  • 如果您真的想优化空间,请将您的数据打包成一个字节字符串并完全放弃单独的 Python 对象。
  • @chepner 我还没有看到这个建议,也许你可以在一个例子中详细说明或指向参考?
  • @pstatix 是的,那又怎样?这正是您感兴趣的。如果列表的大小是命名元组子类或自定义类的属性,列表的大小会改变吗?当然,它会考虑类的真实大小。它只是不会递归地为您提供该对象图中所有内容的大小,这可能与不考虑它一样具有误导性。

标签: python class oop python-internals


【解决方案1】:

我将把您关于 namedtuple x 插槽的具体问题分开,并专注于您的内存节省问题。因为元组 x 带槽的属性可能并且会在几个字节的大小上有所不同 - 但使用其中任何一个,您都必须将完整的 Python 对象作为数字本身。

可以有一个类将数据保存为数组中的打包字节,并根据需要将属性懒惰地生成为 Python 数字。在内存方面这将是赢家,因为 Python 中的数字是一个最小大小为 24 字节的完整对象(在 64 位平台上)。

如果懒惰地获取数字成为性能问题,您可以将您的类移动到 cython,并让操作使用每个对象中的本机打包数字。

总而言之,一个有效的方法可能是一个专门的 Sequence 类,它将用序列中的对象的数据包装一个 NumyArray,以及从该序列中作为元素检索的临时对象。

好消息是 numpy 已经提供了这个功能 - 它允许数组由具有自定义 dtype 的对象组成,甚至还有 np.void一个由其 dtype 定义的结构类型基类 - 它可以有额外的功能。

下面的代码可以在一个 numpy 数组中保存一个“Extents”类,每个实例都准确地获取数据所需的字节数。您可以使用扩展的 numpy 数据类型的优势 - 例如,如果内存是一个问题并且 32 位 FP 值对您来说就足够了,请使用“float32”(“f4”)。

您实际上可以只使用带有自定义 dtype 的原始 np 数组 - 此代码显示了如何添加一些花里胡哨来拥有“中心”属性,并能够访问 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 数组的大小 - 因此,我为结尾保留了一个内部索引,您必须设置最大大小。如果 max-size 变化太大,有一些关于如何调整 numpy-arrays 大小的方法,你必须将逻辑添加到容器类中才能做到这一点,这受到 Python 本身用于分配大小的逻辑的启发列表。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-20
    • 1970-01-01
    • 1970-01-01
    • 2017-11-01
    • 2019-10-27
    • 1970-01-01
    • 2011-06-30
    • 2010-12-21
    相关资源
    最近更新 更多