在为新创建的类实例分配资源时,python(我想是cpython)的效率如何?我遇到一种情况,我将需要实例化节点类数百万次才能构建树结构。每个节点对象应该是轻量级的,只包含一些数字以及对父节点和子节点的引用。
例如,python是否需要为每个实例化对象的所有“双下划线”属性(例如docstring,__dict__
,__repr__
,__class__
等分配内存) ),或者单独创建这些属性,还是将指针存储到类定义的位置?还是效率很高,除了我定义的需要存储在每个对象中的自定义内容外,不需要存储任何内容?
答案 0 :(得分:12)
从表面上看,这很简单:方法,类变量和类docstring存储在类中(函数docstring存储在函数中)。实例变量存储在实例中。该实例还引用该类,因此您可以查找方法。通常,它们全部存储在字典(__dict__
)中。
是的,简短的答案是:Python不在实例中存储方法,但是所有实例都需要引用该类。
例如,如果您有一个像这样的简单类:
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()
然后在内存中看起来(非常简化)是这样的:
但是,当深入CPython时,有一些重要的事情:
__weakref__
字段(另外8个字节)。在这一点上,还需要指出CPython针对其中一些“问题”进行了优化:
__slots__
来避免__dict__
和__weakref__
。这样可以大大减少每个实例的内存占用。鉴于所有这些以及其中的一些要点(尤其是有关优化的要点)都是实现细节,因此很难给出关于Python类有效内存需求的规范答案。
但是,如果您想减少实例的内存占用,则可以尝试__slots__
。它们确实有缺点,但万一它们不适用于您,这是减少内存的一种很好的方法。
class Slotted:
__slots__ = ('a', 'b')
def __init__(self):
self.a = 1
self.b = 1
如果这还不够,并且您要使用许多“值类型”,那么您还可以更进一步,创建扩展类。这些是用C定义但包装的类,以便您可以在Python中使用它们。
为方便起见,我在这里使用Cython的IPython绑定来模拟扩展类:
%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 Dicted:
def __init__(self):
self.a = 1
self.b = 1
我通常使用psutil
(即使是代理方法)来衡量内存影响,并简单地衡量其前后使用了多少内存。由于我需要以某种方式将实例保留在内存中,因此测量值有些偏移,否则将(立即)回收内存。而且这只是一个近似值,因为Python实际上会做大量的内存整理,尤其是在有大量创建/删除操作时。
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重用了一些内存,有时还会为其他目的保留内存,但是它至少应该给出合理的提示:
>>> 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的普通类比带有插槽的类需要更多的内存,但是扩展类(如果适用和可用)可以具有更低的内存占用量。
memory_profiler
是另一个可以非常方便地测量内存使用情况的工具,尽管我已经有一段时间没有使用它了。
答案 1 :(得分:9)
[edit]通过python进程获得内存使用情况的准确度量并不容易; 我认为我的答案不能完全回答问题,但这是一种在某些情况下可能有用的方法。
大多数方法都使用代理方法(创建n个对象并估计对系统内存的影响),而外部库则尝试包装这些方法。例如,可以在here,here和there [/ edit]
中找到线程在cPython 3.7
上,常规类实例的最小大小为56个字节; __slots__
(无字典),为16个字节。
import sys
class A:
pass
class B:
__slots__ = ()
pass
a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)
56, 16
在实例级别找不到文档字符串,类变量和类型注释:
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
[edit]此外,请参见@LiuXiMin answer,以获取类定义大小的度量。 [/ edit]
答案 2 :(得分:7)
CPython中最基本的对象只是一个type reference and reference count。两者都是字大小的(即在64位计算机上为8字节),因此实例的最小大小为2个字(即在64位计算机上为16字节)。
>>> import sys
>>>
>>> class Minimal:
... __slots__ = () # do not allow dynamic fields
...
>>> minimal = Minimal()
>>> sys.getsizeof(minimal)
16
每个实例都需要__class__
的空间和隐藏的引用计数。
类型引用(大约为object.__class__
)表示实例从其类中获取内容。您在类上定义的所有内容(而不是实例)都不会占用每个实例的空间。
>>> 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'
请注意,方法也是类上的函数。通过实例获取一个实例将部分绑定到函数,从而调用function's data descriptor protocol来创建一个临时方法对象。结果,方法不会增加实例大小。
实例不需要空间来容纳类属性,包括__doc__
和 any 方法。
唯一增加实例大小的是存储在实例上的内容。有三种方法可以实现此目的:__dict__
,__slots__
和container types。所有这些存储内容都以某种方式分配给实例。
默认情况下,实例具有__dict__
field-对存储属性的映射的引用。此类 也有其他一些默认字段,例如__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__
的实例都使用dict
的空间,属性名称和值。
添加__slots__
field to the class会生成具有固定数据布局的实例。这将允许的属性限制为声明的属性,但是在实例上只占用很小的空间。 __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__
的实例仅将空格用于属性值。
从list
,dict
或tuple
之类的容器类型继承,可以存储项目(self[0]
)而不是属性({{1 }})。除了self.a
或__dict__
之外,这还使用了紧凑的内部存储 。此类类很少手动构建-经常使用诸如__slots__
之类的助手。
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__
开销的一部分通常是由Python解释器优化的。 CPython具有sharing keys between instances,而considerably reduce the size per instance可以。 PyPy在__dict__
和__dict__
之间使用completely eliminates the difference的优化键共享表示。
除了最琐碎的情况外,不可能精确地测量对象的内存消耗。测量孤立对象的大小会错过相关的结构,例如__slots__
使用内存都是实例上的指针和外部__dict__
。衡量对象组会误计数共享对象(中间字符串,小整数等)和惰性对象(例如,dict
的{{1}}仅在访问时存在)。请注意,PyPy does not implement sys.getsizeof
to avoid its misuse。
为了测量内存消耗,应使用完整的程序测量值。例如,可以使用resource
或psutils
to get the own memory consumption while spawning objects。
我已经创建了一个这样的measurement script for number of fields, number of instances and implementation variant。在CPython 3.7.0和PyPy3 3.6.1 / 7.1.1-beta0上,实例计数为1000000,显示的值为 bytes / field 。
dict
对于CPython,与__dict__
相比, # 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 |
可以节省大约30%-50%的内存。对于PyPy,消耗量是可比的。有趣的是,PyPy比使用__slots__
的CPython差,并且对于极端字段计数保持稳定。
答案 3 :(得分:5)
效率高吗?除了我定义的需要存储在每个对象中的自定义内容之外,不需要存储任何内容吗?
几乎是,除了某些空间。 Python中的类已经是type
的实例,称为元类。当新创建类对象的实例时,custom stuff
就是__init__
中的那些东西。类中定义的属性和方法不会
花更多的空间。
关于某些特定空间,只需参考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