“namedtuple”在内存使用上是否真的和元组一样高效?我的测试结果是否。

19
据Python文档中所述,`namedtuple`的优点之一是它和元组一样节省内存。为了验证这一点,我使用了带有ipython_memory_usage的iPython。测试结果如下图所示: 测试结果显示:
  • 10000000 个 `namedtuple` 实例占用了约 850 MiB 的 RAM
  • 10000000 个 `tuple` 实例占用了约 73 MiB 的 RAM
  • 10000000 个 `dict` 实例占用了约 570 MiB 的 RAM
因此,`namedtuple`比`tuple`使用更多的内存!甚至比`dict`使用更多的内存!!
您认为呢? 我哪里做错了吗?

4
我无法确定你问题的确切答案,但可能是Peephole Optimizer注意到你的元组是由不可变成员定义为文字值,因此它给你返回了对同一元组的引用列表。 - mgilson
@Chinny84 -- 其实,我非常惊讶字典所占用的内存比命名元组还少。我知道如果你在Python3.6中工作,字典已经通过新的实现进行了升级,应该更加内存高效,但我仍然认为这不应该比元组更优秀... - mgilson
@mgilson 这很可能是因为namedtuple()返回的类具有一些Python级别的属性,而dict仍然是纯C的。 - Ashwini Chaudhary
1
就像mgilson所提到的那样,尝试动态创建元组。CPython可以缓存不可变对象的文字,不幸的是,命名元组没有文字,因此无法被缓存。 - Ashwini Chaudhary
@AshwiniChaudhary -- 它有哪些实例级别属性?通过将 verbose=True 传递给构造函数,您可以查看用于生成命名元组的代码...所有属性都在类级别上定义--而且无论创建多少个实例,都只有一个类... - mgilson
1
@mgilson:快速检查显示您的假设是正确的。 (1, 2, 3) 的构造被常量折叠,循环中的所有append调用都将附加相同的元组。 - user2357112
2个回答

26

一个更简单的度量方法是检查等价的tuplenamedtuple对象的大小。 给定两个大致类似的对象:

一个更简单的度量方法是检查等价的tuplenamedtuple对象的大小。给定两个大致类似的对象:

from collections import namedtuple
import sys

point = namedtuple('point', 'x y z')
point1 = point(1, 2, 3)

point2 = (1, 2, 3)

获取它们在内存中的大小:

>>> sys.getsizeof(point1)
72

>>> sys.getsizeof(point2)
72

在我看来它们看起来一模一样...


进一步地,为了重现您的结果,请注意,如果按照您所做的方式创建一个相同元组的列表,则每个元组都是完全相同的对象:

>>> test_list = [(1,2,3) for _ in range(10000000)]
>>> test_list[0] is test_list[-1]
True

所以在你的元组列表中,每个索引都包含对相同对象的引用。这里没有10000000个元组,而是10000000个对同一元组的引用。

另一方面,你的namedtuple对象列表实际上创建了10000000个独立的对象。

一个更好的可比较的方式是查看内存使用情况

>>> test_list = [(i, i+1, i+2) for i in range(10000000)]

并且:

>>> test_list_n = [point(x=i, y=i+1, z=i+2) for i in range(10000000)]

它们大小相同:

>>> sys.getsizeof(test_list)
81528056

>>> sys.getsizeof(test_list_n)
81528056

1
有趣的是,这个大小与字典相同:>>> test_list_d = [{"x":i, "y":i+1, "z":i+2} for i in range(10000000)] >>> sys.getsizeof(test_list_d) 81528056 - tekumara
2
这是因为你总是只计算生成器对象的大小,而不是结果数据结构的大小。 - powo

10

我用Python 3.6.6做了一些调查,得出以下结论:

  1. 在这三种情况下(元组列表、命名元组列表、字典列表),sys.getsizeof返回存储引用的列表的大小。所以在所有三种情况下,您都会得到大小:81528056。

  2. 基本类型的大小如下:

    sys.getsizeof((1,2,3)) 72

    sys.getsizeof(point(x=1, y=2, z=3)) 72

    sys.getsizeof(dict(x=1, y=2, z=3)) 240

  3. 命名元组的 timing 非常糟糕:
    元组列表:1.8秒
    命名元组列表:10秒
    字典列表:4.6秒

  4. 查看系统负载后,我对 getsizeof 的结果产生了怀疑。

    在测量 Python3 进程的占用内存时,我得出了以下结论:

    test_list = [(i, i+1, i+2) for i in range(10000000)]
    增加了:1 745 564K
    每个元素约为 175B。

    test_list_n = [point(x=i, y=i+1, z=i+2) for i in range(10000000)]
    增加了:1 830 740K
    每个元素约为 183B。

    test_list_n = [point(x=i, y=i+1, z=i+2) for i in range(10000000)]
    增加了:2 717 492 K
    每个元素约为 272B。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接