在python3里面,我们经常会用if k in d.keys()
来判断某个key是不是在某个dict里面,或者是用a_dict.keys() - b_dict.keys()
来获取两个字典之间keys的差集。那么这里就有一个问题,dict的keys()
返回了什么数据类型呢?
list?set?两者都是错误答案。Don’t say so much,打印一下type,发现是这么个数据类型:<class 'dict_keys'>
dict_keys是什么东西?
在python dict数据结构定义中(dictobject.c
),可以看到dict_keys
的定义
1 | // dictobject.c |
dict_keys
数据结构的size,是以_PyDictViewObject
为准的。_PyDictViewObject
从语义上看是dict的一个视图,从逻辑上看只包含了一个对应dict的指针
1 | // dictobject.h |
如何理解DictView
的设计?其实这种相当于一个dict实例的代理(Agent/Proxy),用户(开发者)侧请求对应的操作(in、运算符等),代理侧来给出一个最有效率的方案。举一些例子:
in操作
in涉及到for k in d.keys()
跟has_key = (k in d.keys())
两种形式,对应迭代遍历跟包含两种操作。for k in d.keys()
操作对应的是PyDictKeys_Type
里的dictkeys_iter
函数,返回了这个DictView
视图对应的dict的key的iterator,类型为PyDictIterKey_Type
。在迭代遍历时候,会一直调用PyDictIterKey_Type
里定义的dictiter_iternextkey
执行迭代过程中的next操作,从而一个个地获得dict里所有key。
1 | // dictobject.c |
has_key = (k in d.keys())
对应的是包含操作,在PyDictKeys_Type
里面,对应的是dictkeys_as_sequence
的dictkeys_contains
回调。在上一讲list可变、tuple不可变中已经提到,python里面实现对特定数据的多种操作,实际上会尝试将数据看成sequence、mapping等形式,执行对应数据形式中定义的回调函数,而这里便是将keys看作是sequence,执行sq_contains
对应的回调,表示一个是否包含的判断。dictkeys_contains
实质上调用的是dict自己的contains操作,也就是说k in d.keys()
和k in d
这两种写法,实质上是等价的
运算符操作
dict_keys支持多种运算符操作。比如我们在对比作为counter的dict时(不是内置的Counter类),会用keys相减的方式来得到两次统计里新增/删除的key。相减的操作,比如a.keys() - b.keys()
,会执行将keys看作为number时的dictviews_sub
函数。在函数内部的实现里,会首先将a.keys()
转化为一个set,然后调用set数据结构的difference_update
函数,逐步remove掉右侧b.keys()
里面的元素。
1 | // dictobject.c |
值得注意的是,dictviews_sub
内部指定了一个标识符PyId_difference_update
,通过_PyObject_CallMethodIdOneArg
函数去调用result
实例里标识为PyId_difference_update
的函数,入参为other
。这样调用接口的方式,在python里称之为vectorcall,是3.9完全应用的cpython的特性,相对于以前的版本,显著优化了cpython内部不同数据结构间函数调用的效率。有兴趣的同学可以深入了解。
如果是类似=、>、<之类的操作,dict_keys也是支持的,我们可以在dictview_richcompare
函数中看到这些比较符对应的计算方式:
1 | // dictobject.c |
如果两个dict的keys相等,则这两组keys需要长度一样,并且包含相同的元素(类似于set相等)
如果是比大小,比如a.keys()
要比b.keys()
大的话,除了a.keys()
长度比b.keys()
大之外,还需要a.keys()
包含b.keys()
所有的元素才行。所以大小于号主要体现的是包含/被包含的关系。
View概念的其它应用
在dict里,除了keys()
之外,dict的values()
、items()
,返回的实际上也是DictView
的视图结构,定义的方式也基本上相似,但也有少许区别。比如values()
,由于没有指定tp_richcompare
,所以无法将两组values
进行大小或==的比较(都会返回false)
在python里有很多地方应用了视图的概念/手法。如果硬要套View这个单词的话,就还有这么一个地方应用到了,叫做memoryview。memoryview
在业务代码中不常用,主要的作用是提供一块内存的“代理”,让调用方安全地对一个数据实例的内存进行信息读取及管理操作。比如我们对各种数据做pickle.dumps
序列化后,通过memoryview
,就能看到序列化后的数据在内存里的组成:
1 | def memoryview_test(): |