为什么(某些)字典视图是可哈希的?

6 投票
1 回答
513 浏览
提问于 2025-04-18 17:11

在Python 3中,keys()values()items()这几个方法可以提供它们各自元素的动态视图。这些功能在Python 2.7中也有,不过是以viewkeysviewvaluesviewitems的形式存在。我在这里会交替使用这几种说法。

那么,这里有没有什么合理的解释呢:

#!/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 个回答

8

我认为这个问题的出现是因为 viewitemsviewkeys 提供了自定义的比较功能,但 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 对于 itemskeys 被定义为 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_hashtp_richcompare 在它们都为 NULL 时会从父类继承

这个字段与 tp_richcompare 一起被子类型继承:当子类型的 tp_richcompare 和 tp_hash 都为 NULL 时,子类型会同时继承 tp_richcompare 和 tp_hash。

实际上,dict_values 的实现没有阻止这种自动继承,可能会被认为是一个 bug。

撰写回答