探索一下Python中的dict

Oct. 12, 2018, 10:39 p.m.

今天在咖啡馆等人的时候,忽然想到,Python里的dict数据类型,有不同初始化方式,在性能上会不会有差异啊,差异有多大,这是使用得比较频繁的数据类型啊,如果有差异,差异的程度是否值得去重视。

dict的两种常用初始化方式

 # 1.
 my_dict = dict()  
 # 2. 
 my_dict = {}

测试数据展示

# === Part 1: 初始化一个空dict时 ===
%timeit -n 1000000 {}
55.1 ns ± 6.63 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit -n 1000000 dict()
248 ns ± 70.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

# === Part 2: 初始化只有1对键值数据的dict时 ===
%timeit -n 1000000 {'a': 'a'}
89.7 ns ± 11 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit -n 1000000 dict(a='a')
393 ns ± 45.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

# === Part 3: 初始化3对键值数据的dict时 ===
%timeit -n 1000000 {'a': 'a', 'b': 'b', 'c': 'c'}
190 ns ± 31.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit -n 1000000 dict(a='a', b='b', c='c')
497 ns ± 56 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

测试数据说明

  • %timeit 是Jupyter提供的魔法变量,可以直接使用,而不用import timeit后再使用
  • -n 是指定运行多少次,在这次测试中,均为100万次
  • 497 ns ± 56 ns per loop,
  • ns是纳秒
  • per loop是每次的运行用时
  • ±后面的数值是一个参考的波动范围,运行时间会受到多方面因素的影响,比如硬件资源,其它进程抢占资源,CPU的调度等,所以在没有给出一个准确的数值的情况下,就需要提供一个波动范围来表达出其性能的区间
  • 7 runs 是指运行了7个回合,每个回合是100万次
  • 1000000 loops each 是每个回合100万次

小结

  • 在初始化一个dict时,无论是否有数据,{ }都比dict( )的性能提升3-5倍
  • 我发现小伙伴们使用dict( )的都要多一些,应该是因为定义键的时候不需要写引号吧,其形式和一个普通变量没有区别,而且IDE对其的支持也比较友好,在后续的使用中,可以提供提示和自动补全
  • 从你写的是什么代码来看:
  • 如果是应用代码,就是一些比较高层的业务代码,为了充分借助IDE来提升开发效率,那就使用dict( )
  • 如果注重性能,比如一些工具类,基础服务类,性能的提升是点点滴滴的积累,那就使用 { }
  • 如果使用{ }的话,会更加的pythonic,比如使用字典推导式来创建一个字典,{k: v for k, v in iterable_object}
  • 从字典推导式的存在来大胆猜测,{ }才是python建议的,dict( )的存在只是为了显示python不那么异类,至少还有一个可读性比较好的语法
  • 突然想起来了,之前看的一些资料,都提倡使用推导式,列表推导式,字典推导式这些,原来最根本的原因就是性能问题啊

一步步寻找这背后的证据

1、准备2个.py文件

  • dict.py 文件内容: dict()
  • literal.py 文件内容: {}

2、分别运行以下命令

  • python -m dis dict.py
  1        0 LOAD_NAME                0 (dict)
           2 CALL_FUNCTION            0
           4 POP_TOP
           6 LOAD_CONST               0 (None)
           8 RETURN_VALUE
  • python -m dis literal.py
  1        0 BUILD_MAP                0
           2 POP_TOP
           4 LOAD_CONST               0 (None)
           6 RETURN_VALUE

3、寻找第2步中两个命令运行结果的差异

CALL_FUNCTION和BUILD_MAP这一步显示走了不同的逻辑,往后都是一样的,往前dict()的方式多了一个LOAD_NAME

4、如何开始寻找

  • 第一反应,去哪里看,肯定是源码啦:cpython源码

  • 第二反应,这个问题也不复杂应该很多人也发现了,应该有人已经研究过了,上google搜一下,还真找到了一个比较详细的探索过程,这篇文章发表于2012年,基于CPython 2.7

  • The Performance Impact of Using dict() Instead of {} in CPython 2.7
  • 我这里翻译一下文章在末尾处的结论

Conclusions 结论

In summary, calling dict() requires these steps: 总的来说,调用dict()来初始化时,需要经过以下这些步骤:

  1. Find the object associated with the name “dict” and push it onto the stack. 先找到名为dict的对象,并把它压入到运行时栈中

  2. Push the key/value pairs onto the stack as constant values. 把键值对数据作为常量压入到运行时栈中

  3. Get the key/value pairs off of the stack and create a dictionary to hold the keyword arguments to the function. 从运行时栈中取出键值对数据,并创建一个字典对象来把关键字参数存起来,以便后续的函数使用

  4. Call the constructor for dict to make a new object. 调用构造函数来为这个字典制造出一个新对象

  5. Initialize the new object by passing the keyword arguments to its initialization method. 在初始化方法中,用传入的关键字参数来完成新对象的初始化

  6. Resize the new dict and copy the key/value pairs into it from the keyword arguments. 调整这个新的字典对象的大小,然后再复制一份键值对数据进去

Whereas using {} to create a dictionary uses only these steps: 然而,使用{ }去创建一个字典对象仅仅需要以下几步:

  1. Create an empty but pre-allocated dictionary instance. 先创建一个空的,且是预先分配好一定大小的字典实例

  2. Push the key/value pairs onto the stack as constant values. 把键值对数据作为常量压入到运行时栈中

  3. Store each key/value pair in the dictionary. 把每一对键值数据存到之前创建的字典实例中

我们可以得知差异主要在以下方面:

  • 解释器寻找对象

    • dict( )由于存在关键字,解释器需要多出一步来找到它
    • { }不需要额外的一步
  • 中间对象

    • dict( )会创建一个中间的字典对象来暂时存储键值数据
    • { }只创建一个字典对象
  • 存储
    • dict( )先创建一个新的字典对象,然后调整到合适的大小,再把数据从中间对象复制到新的字典对象里
    • { }直接把从运行时栈中取出的数据,存到之前创建的字典对象里

其它

a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})

a == b == c == d == e
True  # a, b, c, d, e 他们的内容是完全相等的,但他们的id并不相等

id(a) == id(b)
False
id(b) == id(c)
False
id(c) == id(d)
False
id(d) == id(e)
False
  • 为了完整性,从官网中贴出来了其它的初始化方式,可以得知a, b, c, d, e的结果都是相等的
  • 参考的官网链接地址:Mapping Types — dict
  • 这里只列了超级常用的a, b的性能测试数据,不过使用Jupyter的话可以很方便进行c, d, e测试

运行环境

  • Python:3.6
  • 个人计算机:iMac
  • 处理器:2.9GHz 四核
  • 内存:8GB
  • software:Jupyter

返回首页