python:类与元组巨大的内存开销(?)

Posted

技术标签:

【中文标题】python:类与元组巨大的内存开销(?)【英文标题】:python: class vs tuple huge memory overhead (?) 【发布时间】:2017-12-20 18:02:02 【问题描述】:

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

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

为什么?有没有我没有想到的明显替代解决方案?

谢谢!

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

【问题讨论】:

collections.namedtuple 似乎是为此目的而设计的,但它们以 1.1GB 为例。也好不了多少。 查看__slots__ 或将key-sharing dictionary 移至Python 3。 在元组的情况下,我相信它只是引用同一个元组 1000 万次。当您创建一个对象时,无论是类还是新元组,它都会使用更多的内存 如答案所示,您的元组示例仅创建一个元组对象。您应该创建一个测试用例,在其中创建许多 不同 元组与自定义对象,看看性能如何。 尝试随机化这些值,你应该会得到不同的结果。 【参考方案1】:

使用__slots__ 可以大大减少内存占用(在我的测试中从1.7 GB 减少到625 MB),因为每个实例不再需要保存dict 来存储属性。

class Person:
    __slots__ = ['first', 'last']
    def __init__(self, first, last):
        self.first = first
        self.last = last

缺点是实例创建后不能再添加属性;该类只为__slots__ 属性中列出的属性提供内存。

【讨论】:

我已经更正了我认为你回答中的“错字”,如果不是,请回滚并道歉。 不,更正有效。这是Person 的实例,您不能再向其添加新属性。您可能也无法向firstlast 添加属性,但出于完全不同的原因:)【参考方案2】:

在第二个示例中,您只创建一个对象,因为元组是常量。

>>> l = [('foo', 'bar') for i in range(10000000)]
>>> id(l[0])
4330463176
>>> id(l[1])
4330463176

类有开销,即属性保存在字典中。因此,namedtuples 只需要一半的内存。

【讨论】:

虽然元组确实是常量,但这并不能解释这里的区别。 [tuple(['foo', 'bar']) for i in range(N)] 创建 N 个常量(但不同)元组对象。 我没有投反对票,但原因不仅仅是因为“元组是不变的”。它基本上是一种适用于某种元组文字的 CPython 优化,例如 (1, 2 , 3/1) 在 CPython 2 中不会产生相同的 ID,因为 3/1 在 CPython 2 中不能恒定折叠。【参考方案3】:

正如其他人在他们的回答中所说,您必须生成不同的对象才能进行比较。

那么,让我们比较一些方法。

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 的精简版。一个明显的赢家,甚至比纯元组更好。

【讨论】:

感谢您的精彩概述!我想有人想知道 2*10M 整数如何占用 1000M 内存,这似乎是由于包含列表 + 引用:import numpy as npl = np.array([(i, i) for i in range(10000000)]) 只需要 189MB(在构建过程中短时间占用 1GB 之后)。但是,这不适用于类实例(参考?)。 实际上,np.array([(i, i) for i in range(10000000)]) 将创建一个dtype('int64') 的齐次二维数组10000000x2,这意味着该数组的大小为~ 8 x N_elem 字节,或者在本例中为~160 MB .【参考方案4】:

除了关闭__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')

结果(python 3.9,64 位):

>>> print(sys.getsizeof(Person(100,100)))
32

对于基于__slot__ 的类,我们有(python 3.9,64 位):

class PersonSlots:
    __slots__ = ['first', 'last']
    def __init__(self, first, last):
        self.first = first
        self.last = last

>>> print(sys.getsizeof(Person(100,100)))
48

因此可以节省更多内存。

对于基于dataobject

l = [Person(i, i) for i in range(10000000)]
memory size: 409 Mb

对于__slots__-based:

  l = [PersonSlots(i, i) for i in range(10000000)]
  memory size: 569 Mb

【讨论】:

以上是关于python:类与元组巨大的内存开销(?)的主要内容,如果未能解决你的问题,请参考以下文章

3python自学之路-数据类型之序列与元组

python学习笔记之列表与元组

python列表与元组的用法

零基础学python-2.7 列表与元组

python:列表与元组

python-列表与元组