为什么命名元组比字典占用更少的内存?

4
我之所以这样问是因为我感到很惊讶——我认为一个namedtuple将会有更多的开销。

(背景是我在内存中缓存一个大型Django查询,并发现Django对象的大小是.values()的100倍。然后我想知道namedtuple版本的对象会有多少开销,使我仍然可以使用.访问属性。比较小是不是我预期的。)
#!/usr/bin/env python                                                           

from pympler.asizeof import asizeof                                             
from collections import namedtuple                                              

import random                                                                   
import string                                                                   

QTY = 100000                                                                    


class Foz(object):                                                              
    pass                                                                        

dicts = [{'foo': random.randint(0, 10000),                                      
          'bar': ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)]),
          'baz': random.randrange(10000),                                       
          'faz': random.choice([True, False]),                                  
          'foz': Foz()} for _ in range(QTY)]                                    

print "%d dicts: %d" % (len(dicts), asizeof(dicts))                             

# https://dev59.com/fFcP5IYBdhLWcg3wu8a1

MyTuple = namedtuple('MyTuple', sorted(dicts[0]))                               

tuples = [MyTuple(**d) for d in dicts]                                          

print "%d namedtuples: %d" % (len(tuples), asizeof(tuples))                     

print "Ratio: %.01f" % (float(asizeof(tuples)) / float(asizeof(dicts))) 

运行中,

$ ./foo.py    
100000 dicts: 75107672
100000 namedtuples: 56707472
Ratio: 0.8

单个元组甚至更小,可能是由于 list 的开销:

$ ./foo.py    
1 dicts: 1072
1 namedtuples: 688
Ratio: 0.6

是哈希表数组的开销吗?但是一个namedtuple也需要属性的哈希表吧?是pympler不准确吗?


它们各自支持哪些操作? - Jared Smith
@JaredSmith 我认为私有代码(即使由asizeof计算)应该在大量项目上平均分配。 - rrauenza
1
__slots__ - wim
1
不,namedtuple 不需要哈希表。它是一个元组。它的特点是为了节省内存。这就是它的用例,并且使得与元组一起工作更易读/自我说明。 - juanpa.arrivillaga
3个回答

9
基本答案很简单:“是的”:普通对象具有内部字典,用于存储实例的属性。
class Foo:
    pass

f = Foo()
print(f.__dict__)
# {}

需要使用字典,因为在Python中,你可以给实例分配类没有定义的新属性:

f.a = 1
print(f.__dict__)
# {'a': 1}

使用字典可以快速查找属性,但由于数据结构本身的内存开销,会产生一定的内存开销。另外,由于不同的Foo实例可能定义了不同的属性,每个实例可能都需要自己的字典。
g = Foo()
print(g.__dict__)
# {}
print(f.__dict_ == g.__dict__)
# False

一个namedtuple不允许在运行时添加属性。因此,特定的namedtuple实例可以将其所有属性存储在单个实例中,该实例由所有实例共享。

给定一个namedtuple和一个实例:

Foo = collections.namedtuple("Foo", 'a,b')
f = Foo(1,2)
namedtuple构造函数生成每个字段的描述符并将其存储在类中;这里存储了命名属性和元组索引之间的转换。当您在实例f上访问属性a时,属性访问会通过此描述符进行路由。
type(Foo.a)
#<class 'property'>

namedtuple 中存储属性名用于访问的键在哪里?即 .foo - rrauenza
3
在这个语境中,“class”指的是类别或类,而不是实例或对象。因此,“In the class, not the instance”的含义是“在类别或类层面上考虑问题,而不是在实例或对象层面上考虑问题”。 - Mad Physicist
更新了答案,以明确内存节省来自于在类中共享属性字典。 - user2722968
@user2722968 我认为展示一些内容会让你已经很棒的答案更好:https://dev59.com/SmMm5IYBdhLWcg3wDbmo - rrauenza
@user2722968 但是没错!名称映射是在类中作为属性完成的! - rrauenza

4

但是,一个命名元组是否也需要属性的哈希表呢?

不需要。一个命名元组的实例布局与常规元组完全相同。从元组条目到属性的映射由生成的描述符提供,这是Python提供的一种控制属性解析的机制。这些描述符存储在生成的命名元组类型中,因此它们是每个类型的成本,而不是每个实例的成本。当前,这些描述符是property对象,正如您可以在当前实现中看到的那样,但这可能会改变(特别是如果任何这些内容被重写为C)。

命名元组比字典更节省内存,因为就内存布局而言,它只是一个元组。


0

可以说,命名元组只需要其映射一次即可用于所有实例(名称 --> 索引)。哈希表可能位于某个集中的元数据(命名空间)中,而不是对象本身中,因此它不会计入每个实例的内存分配。


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