为什么namedtuple使用的内存少于字典?

时间:2019-05-17 20:42:05

标签: python python-2.7

我之所以这样问,是因为我感到惊讶-我认为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://stackoverflow.com/questions/43921240/pythonic-way-to-convert-dictionary-to-namedtuple-or-another-hashable-dict-like

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不够准确吗?

3 个答案:

答案 0 :(得分:4)

基本答案就是“是”:一个普通对象有一个内部字典来存储实例的属性:

class Foo:
    pass

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

这必须是一个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构造函数generates a descriptor并将其存储在类中;这是存储命名属性和元组索引之间的转换的位置。当您访问实例a上的属性f时,属性访问将通过以下描述符进行路由:

type(Foo.a)
#<class 'property'>

答案 1 :(得分:3)

  

但是namedtuple也不需要属性的哈希表吗?

不。 namedtuple的实例布局与常规元组完全相同。元组条目到属性的映射由生成的descriptors提供,这是Python提供的用于控制属性解析的机制。描述符存储在生成的namedtuple类型中,因此它们是每个类型的开销,而不是每个实例的开销。当前,描述符是property objects,正如您在current implementation中看到的那样,但这可能会发生变化(特别是其中的任何一个都用C重写)。

namedtuple的存储效率要比dict高,因为就内存布局而言,它只是一个tuple。

答案 2 :(得分:0)

一个有理由的理由是,一个命名的元组对于所有实例(名称->索引)仅需要一次其映射。哈希表可能位于某些位于中心的元数据(名称空间)中,而不是在对象本身中,因此不计入每个实例的内存分配中。