了解 Python 3.7 中类、namedtuple 和 __slots__ 的大小

Posted

技术标签:

【中文标题】了解 Python 3.7 中类、namedtuple 和 __slots__ 的大小【英文标题】:Understanding size of class, namedtuple and __slots__ in Python 3.7 【发布时间】:2019-03-18 12:32:15 【问题描述】:

在观看了 Nina Zahkarenko 在 Pycon2016 (link) 上的 Python 内存管理演讲后,似乎 dunder 方法 __slots__ 是一种减少对象大小和加速属性查找的工具。

我的期望是普通类会是最大的,而__slots__/namedtuple 方法会节省空间。但是,对sys.getsizeof() 的快速实验似乎表明并非如此:

from collections import namedtuple
from sys import getsizeof

class Rectangle:
   '''A class based Rectangle, with a full __dict__'''
   def __init__(self, x, y, width, height):
      self.x = x
      self.y = y
      self.width = width
      self.height = height

class SlotsRectangle:
   '''A class based Rectangle with __slots__ defined for attributes'''
   __slots__ = ('x', 'y', 'width', 'height')

   def __init__(self, x, y, width, height):
      self.x = x
      self.y = y
      self.width = width
      self.height = height

NamedTupleRectangle = namedtuple('Rectangle', ('x', 'y', 'width', 'height'))
NamedTupleRectangle.__doc__ = 'A rectangle as an immutable namedtuple'

print(f'Class: getsizeof(Rectangle(1,2,3,4))')
print(f'Slots: getsizeof(SlotsRectangle(1,2,3,4))')
print(f'Named Tuple: getsizeof(NamedTupleRectangle(1,2,3,4))')

终端输出:

$ python3.7 example.py
Class: 56
Slots: 72
Named Tuple: 80

这里发生了什么?从 Python 的 Data Model 上的文档来看,__slots__ 似乎使用了描述符,这会给实现它的类增加函数开销。但是,为什么结果如此严重地偏向普通类?

引导我内心的 Raymond H.:必须有更艰难的方法!

【问题讨论】:

getsizeof 没有报告类对象引用的数据结构的大小;只有类对象本身。 【参考方案1】:

函数sys.getsizeof() 可能没有按照您的想法执行;它不适用于复杂的对象,例如自定义类。

查看this answer 计算对象内存大小的方法;也许它可以帮助你。 我在这里复制了该答案的代码,但完整的解释在我链接的答案中。

import sys
from numbers import Number
from collections import Set, Mapping, deque

try: # Python 2
    zero_depth_bases = (basestring, Number, xrange, bytearray)
    iteritems = 'iteritems'
except NameError: # Python 3
    zero_depth_bases = (str, bytes, Number, range, bytearray)
    iteritems = 'items'

def getsize(obj_0):
    """Recursively iterate to sum size of object & members."""
    _seen_ids = set()
    def inner(obj):
        obj_id = id(obj)
        if obj_id in _seen_ids:
            return 0
        _seen_ids.add(obj_id)
        size = sys.getsizeof(obj)
        if isinstance(obj, zero_depth_bases):
            pass # bypass remaining control flow and return
        elif isinstance(obj, (tuple, list, Set, deque)):
            size += sum(inner(i) for i in obj)
        elif isinstance(obj, Mapping) or hasattr(obj, iteritems):
            size += sum(inner(k) + inner(v) for k, v in getattr(obj, iteritems)())
        # Check for custom object instances - may subclass above too
        if hasattr(obj, '__dict__'):
            size += inner(vars(obj))
        if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
            size += sum(inner(getattr(obj, s)) for s in obj.__slots__ if hasattr(obj, s))
        return size
    return inner(obj_0)

【讨论】:

感谢@Ralf 的回答,我相信sys.getsizeof() 正在满足我的用例。我的矩形对象包含四个变量,它们可以引用 Python 托管堆上其他地方的对象。 x 的内存占用与我无关,它可能是具有数千个元素的 list[]。据我了解,x 属性只是一个参考。我期望看到的是使用 slots 与不使用的类相比减少了内存开销。【参考方案2】:

recordclass 库有更紧凑的变体:

from recordclass import dataobject

class Rectangle(dataobject):
   x:int
   y:int
   width:int
   height:int

>>> r = Rectangle(1,2,3,4)
>>> print(sys.getsizeof(r))
48

它比基于__slots__ 的内存占用更少,因为它不参与循环垃圾回收(Py_TPFLAGS_HAVE_GC 标志未设置,因此PyGC_Head(24 字节 [= 3.8]) 根本不需要)。

【讨论】:

【参考方案3】:

“引导我内心的 Raymond H”,+1

所以关于插槽的事情是,你必须阅读about slots。

另一件事是,它们确实会影响class 大小:

print(f'(Class) Class: getsizeof(Rectangle)') # 1056
print(f'(Class) Slots: getsizeof(SlotsRectangle)') # 888

酷。现在假设我们向 Rectangle 类添加一个字段:

rect = Rectangle(1,2,3,4)
rect.extra_field = dict() # wild right?
print(f'(Object) Class: getsizeof(rect)') # still 56

因此,您可以“计算”“您使用”的资源(以实例变量的形式),插槽矩形将是 112,非插槽矩形也将是 112...

但是,我们知道情况并非如此,因为我们希望常规矩形至少为 352,因为我们在其中添加了 dict

插槽阻止您执行此操作,因此提供了一种限制资源使用的方法。

看看这个answer here,它似乎对你的用例来说可能工作得很好。在槽矩形和常规矩形上运行它分别产生152352

另外,如果您真的想尝试优化您的代码并尽量减少资源使用,请转到 rust/c/c++ 方面。

【讨论】:

以上是关于了解 Python 3.7 中类、namedtuple 和 __slots__ 的大小的主要内容,如果未能解决你的问题,请参考以下文章

python高级用法之命名元组namedtuple

Window 10 安装python 3.7 + selenium (附最新安装包)

python python中类的一个例子。

学习Python中类及C++类的对比

Python中类的特殊方法详解

Python中类的定制