我在元组/列表中存储了大量复杂数据,但更喜欢使用小包装类来使数据结构更易于理解,例如。
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
p = Person('foo', 'bar')
print(p.last)
...
优于
p = ['foo', 'bar']
print(p[1])
...
然而似乎有一个可怕的内存开销:
l = [Person('foo', 'bar') for i in range(10000000)]
# ipython now taks 1.7 GB RAM
和
del l
l = [('foo', 'bar') for i in range(10000000)]
# now just 118 MB RAM
为什么呢?是否有任何明显的替代解决方案,我没有想到?
谢谢!
(我知道,在这个例子中,'包装器'类看起来很傻。但是当数据变得更复杂和嵌套时,它会更有用)
答案 0 :(得分:8)
正如其他人在答案中所说,你必须为比较生成不同的对象才有意义。
所以,让我们比较一些方法。
tuple
l = [(i, i) for i in range(10000000)]
# memory taken by Python3: 1.0 GB
class Person
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
l = [Person(i, i) for i in range(10000000)]
# memory: 2.0 GB
namedtuple
(tuple
+ __slots__
)from collections import namedtuple
Person = namedtuple('Person', 'first last')
l = [Person(i, i) for i in range(10000000)]
# memory: 1.1 GB
namedtuple
基本上是一个扩展tuple
并对所有命名字段使用__slots__
的类,但它添加了字段getter和其他一些辅助方法(如果生成的话,您可以看到确切的代码)用verbose=True
调用。
class Person
+ __slots__
class Person:
__slots__ = ['first', 'last']
def __init__(self, first, last):
self.first = first
self.last = last
l = [Person(i, i) for i in range(10000000)]
# memory: 0.9 GB
这是上面namedtuple
的精简版。一个明显的赢家,甚至比纯元组更好。
答案 1 :(得分:6)
中的元组文字
[('foo', 'bar') for i in range(10000000)]
是一个常量表达式。 CPython窥孔优化器将对其进行评估并在代码块中重用结果对象。因此[('foo', 'bar') for i in range(10000000)]
创建一个包含10000个对相同元组对象的引用的列表:
>>> {*map(id, tuple_l)}
{140673197930568} # One unique memory address
Person('foo', 'bar')
未被识别为常量表达式,因此会在每次迭代时进行评估,从而导致创建10000个不同的对象:
>>> len({*map(id, class_l)})
10000000
这是内存占用量巨大差异的主要原因。
纯Python类的内存效率并不高,但您可以添加__slots__
属性来减少每个实例的大小:
class Person:
__slots__ = ('first', 'last')
...
添加__slots__
会将内存占用减少约60%。
答案 2 :(得分:5)
使用__slots__
可以减少内存占用(在我的测试中从1.7 GB到625 MB),因为每个实例不再需要保存dict
来存储属性。
class Person:
__slots__ = ['first', 'last']
def __init__(self, first, last):
self.first = first
self.last = last
缺点是您不能再在创建实例后向其添加属性;该类仅为__slots__
属性中列出的属性提供内存。
答案 3 :(得分:1)
除关闭__dict__
和__weakref__
外,还有一种方法是通过关闭对循环垃圾收集的支持来减少对象占用的内存量。它在库recordclass中实现:
$ pip install recordclass
>>> import sys
>>> from recordclass import dataobject, make_dataclass
创建课程:
class Person(dataobject):
first:str
last:str
或
>>> Person = make_dataclass('Person', 'first last')
结果:
>>> print(sys.getsizeof(Person(100,100)))
32
对于基于__slot__
的课程,我们有:
class Person:
__slots__ = ['first', 'last']
def __init__(self, first, last):
self.first = first
self.last = last
>>> print(sys.getsizeof(Person(100,100)))
56
因此,可以节省更多的内存。
对于基于dataobject
的:
l = [Person(i, i) for i in range(10000000)]
memory size: 681 Mb
对于基于__slots__
的:
l = [Person(i, i) for i in range(10000000)]
memory size: 921 Mb
答案 4 :(得分:-1)
在第二个示例中,您只创建一个对象,因为元组是常量。
#include <functional>
struct GameObjectRef {};
struct cpArbiter {};
struct cpCollisionType {};
typedef std::function<bool(const GameObjectRef &a, const GameObjectRef &b, cpArbiter *arbiter)> EarlyCollisionCallback;
typedef std::function<void(const GameObjectRef &a, const GameObjectRef &b, cpArbiter *arbiter)> LateCollisionCallback;
struct MyThing
{
EarlyCollisionCallback a_, b_;
LateCollisionCallback c_, d_;
virtual void addCollisionMonitor(cpCollisionType a, cpCollisionType b,
EarlyCollisionCallback onCollisionBegin = nullptr,
EarlyCollisionCallback onCollisionPreSolve = nullptr,
LateCollisionCallback onCollisionPostSolve = nullptr,
LateCollisionCallback onCollisionSeparate = nullptr)
{
a_ = std::move(onCollisionBegin);
b_ = std::move(onCollisionPreSolve);
c_ = std::move(onCollisionPostSolve);
d_ = std::move(onCollisionSeparate);
}
void call_callbacks(const GameObjectRef &a, const GameObjectRef &b, cpArbiter *arbiter)
{
if (a_) a_(a, b, arbiter);
if (b_) b_(a, b, arbiter);
if (c_) c_(a, b, arbiter);
if (d_) d_(a, b, arbiter);
}
};
int main()
{
MyThing m;
}
类具有开销,属性保存在字典中。因此,命名元组只需要一半的内存。