Python 字典与对象-哪个更有效?为什么?

Python 字典与对象-哪个更有效?为什么?,python,performance,dictionary,object,Python,Performance,Dictionary,Object,在Python中,在内存使用和CPU消耗方面,字典还是对象更有效 背景: 我必须将大量数据加载到Python中。我创建了一个对象,它只是一个字段容器。创建400万个实例并将它们放入字典大约需要10分钟和~6GB内存。词典准备好后,一眨眼就可以查到它 示例: 为了检查性能,我编写了两个简单的程序,一个使用对象,另一个使用字典: 对象(执行时间~18秒): 字典(执行时间~12秒): 问题: 是我做错了什么,还是字典比对象快?如果dictionary的性能确实更好,有人能解释一下原因吗?对象中的属性

在Python中,在内存使用和CPU消耗方面,字典还是对象更有效

背景: 我必须将大量数据加载到Python中。我创建了一个对象,它只是一个字段容器。创建400万个实例并将它们放入字典大约需要10分钟和~6GB内存。词典准备好后,一眨眼就可以查到它

示例: 为了检查性能,我编写了两个简单的程序,一个使用对象,另一个使用字典:

对象(执行时间~18秒):

字典(执行时间~12秒):

问题:
是我做错了什么,还是字典比对象快?如果dictionary的性能确实更好,有人能解释一下原因吗?

对象中的属性访问在幕后使用dictionary访问,因此使用属性访问会增加额外的开销。另外,在对象情况下,由于额外的内存分配和代码执行(例如
\uuuuu init\uuu
方法),您会产生额外的开销


在您的代码中,如果
o
是一个
Obj
实例,
o.attr
相当于
o.\uuu dict\uuuu['attr']
,有少量额外开销。

您尝试过使用
\uu插槽吗

从:

默认情况下,新旧样式类的实例都有一个用于属性存储的字典。这会为实例变量很少的对象浪费空间。创建大量实例时,空间消耗可能会变得非常严重

可以通过在新样式的类定义中定义
\uuuuuuuuuuuuuuuuuuuuuuuuuuuu
来覆盖默认值。
\uuuuuuuuuuuuuuuuuuuuuuuu
声明获取一系列实例变量,并在每个实例中保留足够的空间来保存每个变量的值。节省空间是因为没有为每个实例创建
\uuu dict\uu

那么这能节省时间和内存吗

在我的计算机上比较三种方法:

test_slots.py:

class Obj(object):
  __slots__ = ('i', 'l')
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)
测试对象:

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)
测试目录:

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o
test_namedtuple.py(2.6中支持):

运行基准测试(使用CPython 2.5):

使用CPython 2.6.2,包括命名元组测试:

$ python --version
Python 2.6.2
$ time python test_obj.py && time python test_dict.py && time python test_slots.py && time python test_namedtuple.py 

real    0m27.197s (using 'normal' object)
real    0m17.657s (using __dict__)
real    0m12.249s (using __slots__)
real    0m12.262s (using namedtuple)
所以,是的(这并不奇怪),使用
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
是性能优化。使用命名元组的性能与
\uuu\uu\uu
类似

from datetime import datetime

ITER_COUNT = 1000 * 1000

def timeit(method):
    def timed(*args, **kw):
        s = datetime.now()
        result = method(*args, **kw)
        e = datetime.now()

        print method.__name__, '(%r, %r)' % (args, kw), e - s
        return result
    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = []

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = []

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': []}) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': []} for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_slotobj():
    return dict((i, SlotObj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_slotobj():
    return [SlotObj(i) for i in xrange(ITER_COUNT)]

if __name__ == '__main__':
    profile_dict_of_dict()
    profile_list_of_dict()
    profile_dict_of_obj()
    profile_list_of_obj()
    profile_dict_of_slotobj()
    profile_list_of_slotobj()
结果:

hbrown@hbrown-lpt:~$ python ~/Dropbox/src/StackOverflow/1336791.py 
profile_dict_of_dict ((), {}) 0:00:08.228094
profile_list_of_dict ((), {}) 0:00:06.040870
profile_dict_of_obj ((), {}) 0:00:11.481681
profile_list_of_obj ((), {}) 0:00:10.893125
profile_dict_of_slotobj ((), {}) 0:00:06.381897
profile_list_of_slotobj ((), {}) 0:00:05.860749

毫无疑问。
您有数据,没有其他属性(没有方法,什么都没有)。因此,您有一个数据容器(在本例中为字典)

我通常更喜欢从数据建模的角度来思考。如果存在一些巨大的性能问题,那么我可以放弃抽象中的某些东西,但必须有很好的理由。
编程就是管理复杂性,而维护正确的抽象通常是实现这种结果最有用的方法之一

关于物体速度较慢的原因,我认为您的测量不正确。
在for循环中执行的赋值太少,因此实例化dict(内在对象)和“自定义”对象所需的时间不同。虽然从语言的角度来看,它们是相同的,但它们的实现却截然不同。

在这之后,两者的分配时间应该几乎相同,因为最终成员都保存在字典中。

您考虑过使用一个新的分配时间吗?()

它是表示结构化数据的新的标准方式,为您提供了元组的性能和类的便利性


与字典相比,它唯一的缺点是(像元组一样),它不能让您在创建后更改属性。

这是一份针对python 3.6.1的@hughdbrown answer的副本,我将计数增加了5倍,并在每次运行结束时添加了一些代码来测试python进程的内存占用

在下议院投票人开始之前,请注意,这种计算物体大小的方法是不准确的

from datetime import datetime
import os
import psutil

process = psutil.Process(os.getpid())


ITER_COUNT = 1000 * 1000 * 5

RESULT=None

def makeL(i):
    # Use this line to negate the effect of the strings on the test 
    # return "Python is smart and will only create one string with this line"

    # Use this if you want to see the difference with 5 million unique strings
    return "This is a sample string %s" % i

def timeit(method):
    def timed(*args, **kw):
        global RESULT
        s = datetime.now()
        RESULT = method(*args, **kw)
        e = datetime.now()

        sizeMb = process.memory_info().rss / 1024 / 1024
        sizeMbStr = "{0:,}".format(round(sizeMb, 2))

        print('Time Taken = %s, \t%s, \tSize = %s' % (e - s, method.__name__, sizeMbStr))

    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

from collections import namedtuple
NT = namedtuple("NT", ["i", 'l'])

@timeit
def profile_dict_of_nt():
    return [NT(i=i, l=makeL(i)) for i in range(ITER_COUNT)]

@timeit
def profile_list_of_nt():
    return dict((i, NT(i=i, l=makeL(i))) for i in range(ITER_COUNT))

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': makeL(i)}) for i in range(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': makeL(i)} for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_slot():
    return dict((i, SlotObj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_slot():
    return [SlotObj(i) for i in range(ITER_COUNT)]

profile_dict_of_nt()
profile_list_of_nt()
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slot()
profile_list_of_slot()
这是我的结果

Time Taken = 0:00:07.018720,    provile_dict_of_nt,     Size = 951.83
Time Taken = 0:00:07.716197,    provile_list_of_nt,     Size = 1,084.75
Time Taken = 0:00:03.237139,    profile_dict_of_dict,   Size = 1,926.29
Time Taken = 0:00:02.770469,    profile_list_of_dict,   Size = 1,778.58
Time Taken = 0:00:07.961045,    profile_dict_of_obj,    Size = 1,537.64
Time Taken = 0:00:05.899573,    profile_list_of_obj,    Size = 1,458.05
Time Taken = 0:00:06.567684,    profile_dict_of_slot,   Size = 1,035.65
Time Taken = 0:00:04.925101,    profile_list_of_slot,   Size = 887.49
我的结论是:

  • 插槽具有最佳的内存占用,并且速度合理
  • dicts是最快的,但是使用的内存最多

  • 如果数据结构不应该包含引用周期,那么还有另一种方法可以减少内存使用

    让我们比较两个类:

    class DataItem:
        __slots__ = ('name', 'age', 'address')
        def __init__(self, name, age, address):
            self.name = name
            self.age = age
            self.address = address
    

    这是因为基于
    structclass
    的类不支持循环垃圾收集,在这种情况下不需要循环垃圾收集

    与基于
    \uuuuuuuuuuuuuuuuuuuuuuuuuu>的类相比,还有一个优势:您可以添加额外的属性:

    >>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
    >>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
    >>> inst3.hobby = ['drawing', 'singing']
    >>> print(inst3)
    >>> print(sizeof(inst3), 'has dict:',  bool(inst3.__dict__))
    DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
    48 has dict: True
    

    下面是我对@Jarrod Chesney非常好的脚本的测试运行。 为了进行比较,我还对python2运行了它,并将“range”替换为“xrange”

    出于好奇,我还使用OrderedDict(ordict)添加了类似的测试以进行比较

    Python 3.6.9:

    Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
    Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
    Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
    Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
    Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
    Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
    Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
    Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67
    
    Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
    Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
    Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
    Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75
    
    Python 2.7.15+:

    Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
    Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
    Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
    Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
    Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
    Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
    Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
    Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0
    
    Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
    Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
    Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
    Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0
    

    因此,在这两个主要版本中,@Jarrod Chesney的结论看起来仍然不错。

    你测试过这个吗<代码>o.uu dict_uu[“attr”]
    是一个具有额外开销的代码,需要额外的字节码运算;obj.attr更快。(当然,属性访问不会比订阅访问慢——这是一个关键的、高度优化的代码路径。)显然,如果你真的执行o.uu dict_uu[“attr”]它会慢一些——我只是想说它等同于订阅访问,而不是说它是以那种方式实现的。我想从我的措辞看不清楚。我还提到了其他因素,如内存分配、构造函数调用时间等。11年后,python3的最新版本仍然如此吗?在生成这样的大序列时,您真的应该使用xrange而不是range。当然,由于您要处理的是几秒钟的执行时间,因此不会产生太大的差异
    class DataItem:
        __slots__ = ('name', 'age', 'address')
        def __init__(self, name, age, address):
            self.name = name
            self.age = age
            self.address = address
    
    $ pip install recordclass
    
    >>> from recordclass import structclass
    >>> DataItem2 = structclass('DataItem', 'name age address')
    >>> inst = DataItem('Mike', 10, 'Cherry Street 15')
    >>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
    >>> print(inst2)
    >>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
    DataItem(name='Mike', age=10, address='Cherry Street 15')
    64 40
    
    >>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
    >>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
    >>> inst3.hobby = ['drawing', 'singing']
    >>> print(inst3)
    >>> print(sizeof(inst3), 'has dict:',  bool(inst3.__dict__))
    DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
    48 has dict: True
    
    Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
    Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
    Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
    Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
    Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
    Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
    Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
    Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67
    
    Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
    Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
    Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
    Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75
    
    Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
    Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
    Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
    Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
    Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
    Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
    Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
    Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0
    
    Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
    Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
    Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
    Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0