python新知识 字典视图
从bug中学习:字典返回的居然是视图
bug
与之前相同,我在试图从dataframe
和series
中提取值。
samples = ['R1','R2','R3','R4']df = pd.DataFrame([[True,False,False,True]],columns=samples)series = pd.Series({'R1':True,'R2':False,'R3':False,'R4':True})print(df[samples])print(series[samples])
out:
R1 R2 R3 R40 True False False TrueR1 TrueR2 FalseR3 FalseR4 Truedtype: bool
正常。
但是呢,我在实际代码中用到的samples
是一个字典。
samples = {'R1':'R1','R2':'R2','R3':'R3','R4':'R4'} #键值相等,仅供演示print(df[samples.keys()])print(series[samples.keys()])
out:
R1 R2 R3 R40 True False False TrueR1 TrueR2 FalseR3 FalseR4 Truedtype: bool
还正常,别急,bug来了。
print(df[samples_map.values()])print(series[samples_map.values()])
out:
R1 R2 R3 R40 True False False TrueKeyError Traceback (most recent call last)-44-c2d1464b8ebd> 1 print(df[samples.values()])----> 2 print(series[samples.values()])KeyError: dict_values(['R1', 'R2', 'R3', 'R4'])
错误,而且是取值的键错误,键值是dict_values(['R1', 'R2', 'R3', 'R4'])
,what?
这时候可能就要问了,这突然出现的是dict_values
是什么鬼东西。为什么取keys
正常,取values
就错误呢?
把keys
和values
都输出看看。
print(samples.keys())print(samples.values())
out:
dict_keys(['R1', 'R2', 'R3', 'R4'])dict_values(['R1', 'R2', 'R3', 'R4'])
啊,返回的是dict_keys
和dict_values
类的对象,不是列表。
字典视图
那我们就来研究一下这两个类。
查阅stackoverflow
[1]和python官方文档[2]后得知,dict.keys()
, dict.values()
和dict.items()
返回的都是视图对象。他们能够提供字典记录的动态视图,当字典变化时,视图也会变化。视图可以通过遍历产生相应的数据,并支持成员测试。
视图支持如下操作:
len(dictview)
iter(dictview)
x in dictview
reversed(dictview)
for loop
dict.keys()
返回的键视图是类似集合的,因为键记录的值是唯一并且可hash
的。如果所有的值都可hash
,那么键值对也是唯一且可hash
的,dict.items()
键值对视图也是类似集合的。 dict.values()
值视图通常不被作为类似集合的对象处理,因为值记录通常不唯一。
对于类似集合的视图,所有抽象基础类collections.abc.Set
定义的操作符都是可以进行的,比如 ==
,<
或者 ^
。
来一个直观的例子。
dishes = { 'sausage': 1, 'bacon': 1, 'spam': 500}keys = dishes.keys() # 取出keysvalues = dishes.values() # 取出valuesdel dishes['spam']dishes['eggs'] = 2print(list(keys))print(list(values))
out:
['sausage', 'bacon', 'eggs'][1, 1, 2]
仔细看一下,我明明事先取出了keys
和values
,然后删除字典的一个键值对和添加键值对后,取出的keys
和values
居然也跟随着发生了动态变化。希望你能从这个例子中更好的理解什么叫视图。
动态变化会避免以下错误发生。
dishes = { 'sausage': 1, 'bacon': 1, 'spam': 500}keys = list(dishes.keys()) #使用list模拟python2del dishes['spam']dishes['eggs'] = 2for i in keys: print(i,dishes[i])
out:
sausage 1bacon 1---------------------------------------------------------------------------KeyError Traceback (most recent call last)-127-e0e39cb366d4> 4 dishes['eggs'] = 2 5 for i in keys:----> 6 print(i,dishes[i])KeyError: 'spam'
不加list
时。
dishes = { 'sausage': 1, 'bacon': 1, 'spam': 500}keys = dishes.keys() del dishes['spam']dishes['eggs'] = 2for i in keys: print(i,dishes[i])
out:
sausage 1bacon 1eggs 2
长度、成员测试、迭代器、集合运算的例子。
print('eggs' in keys)print(len(keys))print(next(iter(keys))) #iter使用可迭代对象生成迭代器,next进行迭代器取值print(keys & {'eggs', 'bacon', 'salad'}) #交集print(keys | {'sausage', 'juice'}) #并集
out:
True3sausage{'eggs', 'bacon'}{'bacon', 'eggs', 'juice', 'sausage'}
视图的优点在于能够直接计算长度、进行成员测试且跟随字典动态变化。
思考
那视图和之前的bug
有什么关系呢。从官方文档可以看出,keys
视图和values
视图的唯一区别在于,keys
里的值可hash
,而values
里的值默认不可hash
。
但是更奇怪的事发生了。
hash(samples.keys())
out:
---------------------------------------------------------------------------TypeError Traceback (most recent call last)input----> 1 hash(samples.keys())TypeError: unhashable type: 'dict_keys'
hash(samples.values())
out:
-9223371939388789928
values
视图对象整体居然可hash
,而且官方说d.values() == d.values()
恒为False
。啊这,不会是因为hash
值可变吧,再来一次。
hash(samples.values())
out:
-9223371939388789799
果然每次值都不一样。
不过,真的不知道values
视图对象可hash
是为了什么,反倒是如果keys
视图对象可hash
我倒是能理解。
与此同时,pandas
官方文档[3]中说。
An Index instance can only contain hashable objects
index
只能包含可hash
对象,而且之前的错误详细信息里也有部分提到了hashable
(之前省略了)。
pandas\_libs\index.pyx in pandas._libs.index.IndexEngine.get_value()pandas\_libs\index.pyx in pandas._libs.index.IndexEngine.get_value()pandas\_libs\index.pyx in pandas._libs.index.IndexEngine.get_loc()pandas\_libs\hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()pandas\_libs\hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()KeyError: dict_values(['R1', 'R2', 'R3', 'R4'])
所以有可能是因为values
视图对象可hash
导致series
认为该返回值是一个单独的键,而不是多个键的集合,于是series
直接提取该键的对应值,从而产生错误。
fix bug
解决起来当然很简单了。
print(series[list(samples.values())])print(series[iter(samples.values())])
out:
R1 TrueR2 FalseR3 FalseR4 Truedtype: boolR1 TrueR2 FalseR3 FalseR4 Truedtype: bool
但是我又发现,使用.loc
也可以解决该bug。
print(series.loc[samples.values()])
out:
R1 TrueR2 FalseR3 FalseR4 Truedtype: bool
.loc
就可以识别出该返回值包含多值并逐个取值,但是直接[]
却不行。难道是因为.loc
优先检验提供的值是否可迭代?
本文涉及内容过于复杂,结论仅为猜想。欢迎提出意见。
我
我是 SSSimon Yang,关注我,用code解读世界
References
[1]
stackoverflow
: https://stackoverflow.com/questions/8957750/what-are-dictionary-view-objects[2]
python官方文档: https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects[3]
pandas
官方文档: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Index.html