为什么(某些)字典视图是可哈希的?
在Python 3中,keys()
、values()
和items()
这几个方法可以提供它们各自元素的动态视图。这些功能在Python 2.7中也有,不过是以viewkeys
、viewvalues
和viewitems
的形式存在。我在这里会交替使用这几种说法。
那么,这里有没有什么合理的解释呢:
#!/usr/bin/python3.4
In [1]: hash({}.keys())
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-1-3727b260127e> in <module>()
----> 1 hash({}.keys())
TypeError: unhashable type: 'dict_keys'
In [2]: hash({}.items())
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-decac720f012> in <module>()
----> 1 hash({}.items())
TypeError: unhashable type: 'dict_items'
In [3]: hash({}.values())
Out[3]: -9223363248553358775
我觉得这还挺让人惊讶的。
Python文档中关于“可哈希”的解释
是这样的:一个对象是可哈希的,如果它在生命周期内有一个不会改变的哈希值(这需要一个
__hash__()
方法),并且可以与其他对象进行比较(这需要一个__eq__()
方法)。可哈希的对象如果相等,必须有相同的哈希值。
好的,第一部分确实是对的;看起来dict_values
对象的哈希值在它的生命周期内不会改变——尽管它底层的值是可以改变的。
In [11]: d = {}
In [12]: vals = d.values()
In [13]: vals.__hash__()
Out[13]: -9223363248553358718
In [14]: d['a'] = 'b'
In [15]: vals
Out[15]: dict_values(['b'])
In [16]: vals.__hash__()
Out[16]: -9223363248553358718
但是关于__eq__()
的部分... 实际上,它并没有这个方法。
In [17]: {'a':'a'}.values().__eq__('something else')
Out[17]: NotImplemented
所以... 是的。有没有人能解释一下这个情况?为什么在这三个viewfoo
方法中,只有dict_values
对象是可哈希的呢?
1 个回答
我认为这个问题的出现是因为 viewitems
和 viewkeys
提供了自定义的比较功能,但 viewvalues
没有。这里是每种视图类型的定义:
PyTypeObject PyDictKeys_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"dict_keys", /* tp_name */
sizeof(dictviewobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
(destructor)dictview_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)dictview_repr, /* tp_repr */
&dictviews_as_number, /* tp_as_number */
&dictkeys_as_sequence, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */
(traverseproc)dictview_traverse, /* tp_traverse */
0, /* tp_clear */
dictview_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
(getiterfunc)dictkeys_iter, /* tp_iter */
0, /* tp_iternext */
dictkeys_methods, /* tp_methods */
0,
};
PyTypeObject PyDictItems_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"dict_items", /* tp_name */
sizeof(dictviewobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
(destructor)dictview_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)dictview_repr, /* tp_repr */
&dictviews_as_number, /* tp_as_number */
&dictitems_as_sequence, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */
(traverseproc)dictview_traverse, /* tp_traverse */
0, /* tp_clear */
dictview_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
(getiterfunc)dictitems_iter, /* tp_iter */
0, /* tp_iternext */
dictitems_methods, /* tp_methods */
0,
};
PyTypeObject PyDictValues_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"dict_values", /* tp_name */
sizeof(dictviewobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
(destructor)dictview_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)dictview_repr, /* tp_repr */
0, /* tp_as_number */
&dictvalues_as_sequence, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */
(traverseproc)dictview_traverse, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
(getiterfunc)dictvalues_iter, /* tp_iter */
0, /* tp_iternext */
dictvalues_methods, /* tp_methods */
0,
};
注意,tp_richcompare
对于 items
和 keys
被定义为 dictview_richcompare
,但 values
没有。现在,关于 __hash__
的文档中提到:
如果一个类重写了
__eq__()
但没有定义__hash__()
,那么它的__hash__()
会被隐式设置为 None。...
如果一个重写了
__eq__()
的类需要保留父类的__hash__()
实现,解释器必须明确告诉它,通过设置__hash__ = <ParentClass>.__hash__
。如果一个没有重写
__eq__()
的类希望禁止哈希支持,它应该在类定义中包含__hash__ = None
。
所以,因为 items
/keys
重写了 __eq__()
(通过提供 tp_richcompare
函数),它们需要明确地将 __hash__
定义为等于父类的实现。由于 values
没有重写 __eq__()
,它就继承了来自 object
的 __hash__
,因为 tp_hash
和 tp_richcompare
在它们都为 NULL 时会从父类继承:
这个字段与 tp_richcompare 一起被子类型继承:当子类型的 tp_richcompare 和 tp_hash 都为 NULL 时,子类型会同时继承 tp_richcompare 和 tp_hash。
实际上,dict_values
的实现没有阻止这种自动继承,可能会被认为是一个 bug。