class 的实例使用哪些资源?

What resources does an instance of a class use?

在为新创建的 class 实例分配资源时,python(我猜是 cpython)的效率如何?我有一种情况,我需要实例化一个节点 class 数百万次来构建树结构。每个节点对象应该是轻量级的,只包含一些数字和对父节点和子节点的引用。

例如,python 是否需要为每个实例化对象的所有 "double underscore" 属性分配内存(例如文档字符串、__dict____repr____class__, etc, etc),要么单独创建这些属性,要么存储指向它们由 class?或者它是否高效并且不需要存储任何东西,除了我定义的需要存储在每个对象中的自定义内容?

[edit] 要准确测量 python 进程的内存使用情况并不容易; 我认为我的回答没有完全回答问题,但这是一种在某些情况下可能有用的方法。

大多数方法使用代理方法(创建 n 个对象并估计对系统内存的影响),以及试图包装这些方法的外部库。例如,可以找到线程 here, here, and there [/edit]

cPython 3.7 上,常规 class 实例的最小大小为 56 字节;使用 __slots__(无字典),16 个字节。

import sys

class A:
    pass

class B:
    __slots__ = ()
    pass

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

输出:

56, 16

在实例级别找不到文档字符串、class 变量和类型注释:

import sys

class A:
    """regular class"""
    a: int = 12

class B:
    """slotted class"""
    b: int = 12
    __slots__ = ()

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

输出:

56, 16

[编辑]此外,参见 class 定义 的大小度量。 [/edit]

Is it efficient and does not need to store anything except the custom stuff I defined that needs to be stored in each object?

几乎是,除了某些 space。 Python 中的 Class 已经是 type 的实例,称为 metaclass。当新建一个 class 对象的实例时,custom stuff 就是 __init__ 中的那些东西。 class中定义的属性和方法不会 花费更多 space.

至于某些space,参考一下Reblochon Masque的回答,很好,印象深刻

也许我可以举一个简单但说明性的例子:

class T(object):
    def a(self):
        print(self)
t = T()
t.a()
# output: <__main__.T object at 0x1060712e8>
T.a(t)
# output: <__main__.T object at 0x1060712e8>
# as you see, t.a() equals T.a(t)

import sys
sys.getsizeof(T)
# output: 1056
sys.getsizeof(T())
# output: 56

从表面上看很简单:方法、class 变量和 class 文档字符串存储在 class 中(函数文档字符串存储在函数中)。实例变量存储在实例中。该实例还引用了 class,因此您可以查找这些方法。通常所有这些都存储在字典中(__dict__)。

所以是的,简短的回答是:Python 不在实例中存储方法,但所有实例都需要引用 class.

例如,如果您有一个像这样的简单 class:

class MyClass:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __repr__(self):
        return f"{self.__class__.__name__}({self.a}, {self.b})"

instance_1 = MyClass()
instance_2 = MyClass()

然后 in-memory 它看起来(非常简单)如下所示:

更深入

然而,在深入学习 CPython 时,有几件事很重要:

  • 将字典作为抽象会导致相当多的开销:您需要对实例字典(字节)的引用,并且字典中的每个条目都存储哈希(8 字节)、指向键的指针(8 字节)和一个指向存储属性的指针(另外 8 个字节)。字典通常也是 over-allocate 这样添加另一个属性不会触发 dictionary-resize.
  • Python没有"value-types",即使是整数也是一个实例。这意味着您不需要 4 个字节来存储整数 - Python 需要(在我的计算机上)24 个字节来存储整数 0 和至少 28 个字节来存储不为零的整数。然而,对其他对象的引用只需要 8 个字节(指针)。
  • CPython 使用引用计数,因此每个实例都需要一个引用计数(8 字节)。此外,大多数 CPythons classes 都参与循环垃圾收集器,这会导致每个实例另外 24 字节的开销。除了这些可以是weak-referenced的classes(大部分)还有一个__weakref__字段(另外8个字节)。

此时也有必要指出 CPython 优化了其中的一些 "problems":

  • Python 使用 Key-Sharing Dictionaries 来避免实例字典的一些内存开销(散列和键)。
  • 您可以在 class 中使用 __slots__ 来避免 __dict____weakref__。这可以显着减少每个实例的 memory-footprint。
  • Python 实习一些值,例如,如果您创建一个小整数,它不会创建一个新的整数实例,而是 return 对已存在实例的引用。

考虑到所有这些以及其中的几个要点(尤其是关于优化的要点)是 implementation-details 很难给出关于 Python 的有效 memory-requirements classes.

减少实例的内存占用

但是,如果您想减少实例的 memory-footprint,一定要尝试 __slots__。它们确实有 draw-backs,但如果它们不适用于您,它们是减少内存的好方法。

class Slotted:
    __slots__ = ('a', 'b')
    def __init__(self):
        self.a = 1
        self.b = 1

如果这还不够,并且您操作了很多 "value types",您还可以更进一步,创建扩展 classes。这些是在 C 中定义的 classes,但被包装以便您可以在 Python.

中使用它们

为方便起见,我在这里使用 Cython 的 IPython 绑定来模拟扩展 class:

%load_ext cython
%%cython

cdef class Extensioned:
    cdef long long a
    cdef long long b

    def __init__(self):
        self.a = 1
        self.b = 1

正在测量内存使用情况

在所有这些理论之后剩下的有趣问题是:我们如何测量记忆?

我也用普通的class:

class Dicted:
    def __init__(self):
        self.a = 1
        self.b = 1

我通常使用 psutil(即使它是一种代理方法)来测量内存影响,并简单地测量前后使用了多少内存。测量值有点偏移,因为我需要以某种方式将实例保留在内存中,否则内存将被回收(立即)。此外,这只是一个近似值,因为 Python 实际上做了相当多的内存管理,尤其是当有很多 create/deletes.


import os
import psutil
process = psutil.Process(os.getpid())

runs = 10
instances = 100_000

memory_dicted = [0] * runs
memory_slotted = [0] * runs
memory_extensioned = [0] * runs

for run_index in range(runs):
    for store, cls in [(memory_dicted, Dicted), (memory_slotted, Slotted), (memory_extensioned, Extensioned)]:
        before = process.memory_info().rss
        l = [cls() for _ in range(instances)]
        store[run_index] = process.memory_info().rss - before
        l.clear()  # reclaim memory for instances immediately

每个 运行 的内存不会完全相同,因为 Python re-uses 一些内存,有时还保留内存用于其他目的,但它至少应该给出一个合理的提示:

>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2
(15.625, 5.3359375, 2.7265625)

我在这里使用 min 主要是因为我对最小值感兴趣,我除以 1024**2 将字节转换为兆字节。

总结:正如预期的那样,带 dict 的普通 class 比带插槽的 classes 需要更多的内存,但扩展 classes(如果适用且可用)可以有更低的内存内存占用。

另一个可以非常方便地测量内存使用情况的工具是 memory_profiler,尽管我已经有一段时间没有使用它了。

CPython中最基本的对象就是一个type reference and reference count。两者都是 word-sized(即在 64 位机器上为 8 字节),因此实例的最小大小为 2 个字(即在 64 位机器上为 16 字节)。

>>> import sys
>>>
>>> class Minimal:
...      __slots__ = ()  # do not allow dynamic fields
...
>>> minimal = Minimal()
>>> sys.getsizeof(minimal)
16

每个实例需要 space 用于 __class__ 和一个隐藏的引用计数。


类型引用(大约 object.__class__)意味着 实例从它们的 class 中获取内容。您在 class 上定义的所有内容,而不是实例,不会占用每个实例 space。

>>> class EmptyInstance:
...      __slots__ = ()  # do not allow dynamic fields
...      foo = 'bar'
...      def hello(self):
...          return "Hello World"
...
>>> empty_instance = EmptyInstance()
>>> sys.getsizeof(empty_instance)  # instance size is unchanged
16
>>> empty_instance.foo             # instance has access to class attributes
'bar'
>>> empty_instance.hello()         # methods are class attributes!
'Hello World'

请注意,方法也是 class 上的函数。通过实例获取一个调用 function's data descriptor protocol 通过将实例部分绑定到函数来创建临时方法对象。因此,方法不会增加实例大小

实例不需要 space class 属性,包括 __doc__any 方法。


唯一增加实例大小的是存储在实例上的内容。可以通过三种方式实现:__dict____slots__container types。所有这些都以某种方式存储分配给实例的内容。

  • 默认情况下,实例有一个 __dict__ field - 对存储属性的映射的引用。这样的 classes also 还有一些其他的默认字段,比如 __weakref__.

    >>> class Dict:
    ...     # class scope
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.bar = 2                   # assign to instance
    ...
    >>> dict_instance = Dict()
    >>> dict_instance.foo = 1                  # assign to instance
    >>> sys.getsizeof(dict_instance)           # larger due to more references
    56
    >>> sys.getsizeof(dict_instance.__dict__)  # __dict__ takes up space as well!
    240
    >>> dict_instance.__dict__                 # __dict__ stores attribute names and values
    {'bar': 2, 'foo': 1}
    

    每个使用 __dict__ 的实例使用 space 作为 dict,属性名称和值。

  • 添加 __slots__ field to the class 生成具有固定数据布局的实例。这将允许的属性限制为声明的属性,但在实例上占用很少 space 。 __dict____weakref__ 插槽仅根据要求创建。

    >>> class Slots:
    ...     __slots__ = ('foo',)  # request accessors for instance data
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.foo = 2
    ...
    >>> slots_instance = Slots()
    >>> sys.getsizeof(slots_instance)           # 40 + 8 * fields
    48
    >>> slots_instance.bar = 1
    AttributeError: 'Slots' object has no attribute 'bar'
    >>> del slots_instance.foo
    >>> sys.getsizeof(slots_instance)           # size is fixed
    48
    >>> Slots.foo                               # attribute interface is descriptor on class
    <member 'foo' of 'Slots' objects>
    

    每个使用 __slots__ 的实例仅将 space 用于属性值。

  • 从容器类型继承,例如 listdicttuple,允许存储项 (self[0]) 而不是属性(self.a)。除了 __dict____slots__ 之外,这还使用了紧凑的内部存储 。这样的 classes 很少被手动构造 - 经常使用诸如 typing.NamedTuple 的助手。

    >>> from typing import NamedTuple
    >>>
    >>> class Named(NamedTuple):
    ...     foo: int
    ...
    >>> named_instance = Named(2)
    >>> sys.getsizeof(named_instance)
    56
    >>> named_instance.bar = 1
    AttributeError: 'Named' object has no attribute 'bar'
    >>> del named_instance.foo                  # behaviour inherited from container
    AttributeError: can't delete attribute
    >>> Named.foo                               # attribute interface is descriptor on class
    <property at 0x10bba3228>
    >>> Named.__len__                           # container interface/metadata such as length exists
    <slot wrapper '__len__' of 'tuple' objects>
    

    派生容器的每个实例的行为都像基本类型,加上潜在的 __slots____dict__.

最轻量级实例使用__slots__仅存储属性值。


请注意,__dict__ 开销的一部分通常由 Python 解释器优化。 CPython 能够在 __dict____slots__ 之间 sharing keys between instances, which can . PyPy uses an optimises key-shared representation that completely eliminates the difference

除了最微不足道的情况外,不可能准确测量对象的内存消耗。测量孤立对象的大小会遗漏相关结构,例如 __dict__ 使用内存 both 实例上的指针 外部 dict。测量对象组会错误地计算共享对象(驻留字符串、小整数...)和惰性对象(例如 __dict__dict 仅在访问时存在)。请注意 PyPy does not implement sys.getsizeof .

为了测量内存消耗,应该使用完整的程序测量。例如,可以使用 resource or psutils to get the own memory consumption while spawning objects.

我创造了一个这样的 measurement script for number of fields, number of instances and implementation variant。对于 1000000 的实例计数,在 CPython 3.7.0 和 PyPy3 3.6.1/7.1.1-beta0.[=62 上显示的值为 bytes/field =]

      # fields |     1 |     4 |     8 |    16 |    32 |    64 |
---------------+-------+-------+-------+-------+-------+-------+
python3: slots |  48.8 |  18.3 |  13.5 |  10.7 |   9.8 |   8.8 |
python3: dict  | 170.6 |  42.7 |  26.5 |  18.8 |  14.7 |  13.0 |
pypy3:   slots |  79.0 |  31.8 |  30.1 |  25.9 |  25.6 |  24.1 |
pypy3:   dict  |  79.2 |  31.9 |  29.9 |  27.2 |  24.9 |  25.0 |

对于 CPython,__slots____dict__ 节省大约 30%-50% 的内存。对于 PyPy,消耗是可比的。有趣的是,PyPy 比 CPython 差 __slots__,并且在极端字段计数下保持稳定。