Dictionary vs Object-哪个更有效? 为什么?

就内存使用和 CPU 消耗而言,Python 中哪个更高效—— Dictionary 还是 Object?

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

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

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

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

Dictionary (执行时间 ~ 12秒) :

all = {}
for i in range(1000000):
o = {}
o['i'] = i
o['l'] = []
all[i] = o

问题: 是我做错了什么,还是 dictionary 比 object 快?如果字典的确表现得更好,有人能解释一下为什么吗?

73600 次浏览

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

在您的代码中,如果 o是一个 Obj实例,那么 o.attr就相当于 o.__dict__['attr'],只有少量的额外开销。

你试过使用 __slots__吗?

来自 文件:

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

可以通过在新样式的类定义中定义 __slots__来覆盖默认值。__slots__声明接受一系列实例变量,并在每个实例中保留足够的空间,以便为每个变量保存一个值。由于没有为每个实例创建 __dict__,因此节省了空间。

那么,这样做是否既节省了时间,又节省了内存呢?

在我的电脑上比较这三种方法:

Test _ lots.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)

Test _ obj. py:

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

Test _ dict. py:

all = {}
for i in range(1000000):
o = {}
o['i'] = i
o['l'] = []
all[i] = o

Test _ namedtuple.py (2.6中支持) :

import collections


Obj = collections.namedtuple('Obj', 'i l')


all = {}
for i in range(1000000):
all[i] = Obj(i, [])

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

$ lshw | grep product | head -n 1
product: Intel(R) Pentium(R) M processor 1.60GHz
$ python --version
Python 2.5
$ time python test_obj.py && time python test_dict.py && time python test_slots.py


real    0m27.398s (using 'normal' object)
real    0m16.747s (using __dict__)
real    0m11.777s (using __slots__)

使用 CPython 2.6.2,包括命名的 tuple 测试:

$ 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)

所以是的(这并不奇怪) ,使用 __slots__是一种性能优化。使用命名元组的性能与 __slots__相似。

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

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

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

关于 原因一个物体是慢的,我认为你的测量是不正确的。
您在 for 循环中执行的任务太少,因此您所看到的是实例化 dict (内部对象)和“自定义”对象所需的不同时间。尽管从语言的角度来看,它们是相同的,但是它们有完全不同的实现。
之后,两者的赋值时间应该几乎相同,因为最终成员保存在字典中。

你考虑过使用 命名元组吗? (Python 2.4/2.5的链接)

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

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

下面是 python 3.6.1的@Hughdbrown 应答,我将计数放大了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

我的结论是:

  1. 插槽具有最好的内存占用,并且速度合理。
  2. 字典是最快的,但使用最多的内存。

如果数据结构不应该包含引用周期,那么在 记录类库的帮助下还有另一种方法来减少内存使用。

让我们比较两类:

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 make_dataclass
>>> DataItem2 = make_dataclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
DataItem(name='Mike', age=10, address='Cherry Street 15')
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
64 40

这是可能的,因为基于 dataobject的子类不支持循环垃圾收集,在这种情况下不需要这样做。

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

出于好奇,我还使用 OrderedDect (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 的结论看起来都还不错。