【问题标题】:Dictionary vs Object - which is more efficient and why?字典 vs 对象 - 哪个更有效,为什么?
【发布时间】:2009-08-26 18:55:45
【问题描述】:

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

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

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

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

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

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

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

问题: 我做错了什么还是字典比对象快?如果确实字典性能更好,有人可以解释为什么吗?

【问题讨论】:

  • 在生成这样的大序列时,您应该真正使用 xrange 而不是 range。当然,由于您要处理几秒钟的执行时间,这不会有太大区别,但仍然是一个好习惯。
  • 除非是python3

标签: python performance dictionary object


【解决方案1】:

您是否尝试过使用__slots__

来自documentation

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

可以通过在新式类定义中定义__slots__ 来覆盖默认值。 __slots__ 声明采用一系列实例变量,并在每个实例中保留足够的空间来保存每个变量的值。节省空间是因为__dict__ 不是为每个实例创建的。

那么这样既节省时间又节省内存?

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

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)

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,包括命名元组测试:

$ 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__ 相似。

【讨论】:

  • 太好了 - 谢谢!我在我的机器上尝试过同样的方法 - 带有 slots 的对象是最有效的方法(我得到了大约 7 秒)。
  • 还有命名元组,docs.python.org/library/collections.html#collections.namedtuple,一个用于带槽对象的类工厂。它绝对更整洁,甚至可能更优化。
  • 我也测试了命名元组,并用结果更新了答案。
  • 我运行了你的代码几次,很惊讶我的结果不同 - slot=3sec obj=11sec dict=12sec namedtuple=16sec。我在 Win7 64bit 上使用 CPython 2.6.6
  • 如果普通对象使用字典,为什么使用普通对象比使用 __dict__ 慢得多?
【解决方案2】:

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

在您的代码中,如果oObj 实例,则o.attr 等效于o.__dict__['attr'],但有少量额外开销。

【讨论】:

  • 你测试过这个吗? o.__dict__["attr"] 是有额外开销的,需要额外的字节码操作; obj.attr 更快。 (当然,属性访问不会比订阅访问慢——它是一个关键的、高度优化的代码路径。)
  • 显然,如果你真的 do o.__dict__["attr"] 它会更慢 - 我只是想说它等同于它,而不是它已实现正是这样。我想我的措辞不清楚。我还提到了其他因素,例如内存分配、构造函数调用时间等。
  • 11年后的最新版本python3还是这样吗?
【解决方案3】:

您是否考虑过使用namedtuple? (link for python 2.4/2.5)

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

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

【讨论】:

    【解决方案4】:

    这里是 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. dict 速度最快,但占用的内存最多。

    【讨论】:

    • 伙计,你应该把它变成一个问题。我也在自己的计算机上运行它,只是为了确保(我没有安装 psutil,所以我把那部分去掉了)。无论如何,这让我感到莫名其妙,这意味着原始问题没有得到完全回答。所有其他答案都像“namedtuple 很棒”和“使用 slots”,而且显然一个全新的 dict 对象每次都比他们快?我猜dicts真的优化得很好?
    • 好像是makeL函数返回字符串的结果。相反,如果您返回一个空列表,则结果与 python2 中的hughdbrown 大致匹配。除了 namedtuples 总是比 SlotObj 慢:(
    • 可能有一个小问题:makeL 可以在每个 '@timeit' 轮中以不同的速度运行,因为字符串缓存在 python 中 - 但也许我错了。
    • @BarnabasSzabolcs 应该每次都创建一个新字符串,因为它必须替换值 "This is a sample string %s" % i
    • 是的,在循环中确实如此,但在第二个测试中,我再次从 0 开始。
    【解决方案5】:
    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
    

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

        【解决方案7】:

        这是我对@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 的结论看起来仍然不错。

        【讨论】:

          【解决方案8】:

          如果数据结构不应该包含引用循环,还有另一种方法可以在 recordclass library 的帮助下减少内存使用。

          让我们比较两个类:

          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 的子类不支持循环垃圾回收,在这种情况下不需要。

          【讨论】:

            猜你喜欢
            • 2010-10-10
            • 1970-01-01
            • 2016-12-20
            • 2012-03-12
            • 2015-05-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多