python:class vs tuple巨大的内存开销(?)

时间:2017-07-15 22:23:14

标签: python list class data-structures tuples

我在元组/列表中存储了大量复杂数据,但更喜欢使用小包装类来使数据结构更易于理解,例如。

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

为什么呢?是否有任何明显的替代解决方案,我没有想到?

谢谢!

(我知道,在这个例子中,'包装器'类看起来很傻。但是当数据变得更复杂和嵌套时,它会更有用)

5 个答案:

答案 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

namedtupletuple + __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;
}

类具有开销,属性保存在字典中。因此,命名元组只需要一半的内存。

相关问题