NumPy ndarray的哈希性

13

我有一些困惑,不明白numpy对象的哈希处理方式。

>>> import numpy as np
>>> class Vector(np.ndarray):
...     pass
>>> nparray = np.array([0.])
>>> vector = Vector(shape=(1,), buffer=nparray)
>>> ndarray = np.ndarray(shape=(1,), buffer=nparray)
>>> nparray
array([ 0.])
>>> ndarray
array([ 0.])
>>> vector
Vector([ 0.])
>>> '__hash__' in dir(nparray)
True
>>> '__hash__' in dir(ndarray)
True
>>> '__hash__' in dir(vector)
True
>>> hash(nparray)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'numpy.ndarray'
>>> hash(ndarray)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'numpy.ndarray'
>>> hash(vector)
-9223372036586049780
>>> nparray.__hash__()
269709177
>>> ndarray.__hash__()
269702147
>>> vector.__hash__()
-9223372036586049780
>>> id(nparray)
4315346832
>>> id(ndarray)
4315234352
>>> id(vector)
4299616456
>>> nparray.__hash__() == id(nparray)
False
>>> ndarray.__hash__() == id(ndarray)
False
>>> vector.__hash__() == id(vector)
False
>>> hash(vector) == vector.__hash__()
True
为什么 numpy 对象定义了 __hash__ 方法但是却不可哈希?而继承 numpy.ndarray 的类定义了 __hash__ 后又可以哈希?我错过了什么吗?我使用的是 Python 2.7.1 和 numpy 1.6.1。谢谢任何帮助!编辑:添加了对象的 id。根据 deinonychusaur 的评论并尝试找出哈希是否基于内容,我尝试使用 numpy.nparray.dtype,并发现了一些奇怪的东西:
>>> [Vector(shape=(1,), buffer=np.array([1], dtype=mytype), dtype=mytype) for mytype in ('float', 'int', 'float128')]
[Vector([ 1.]), Vector([1]), Vector([ 1.0], dtype=float128)]
>>> [id(Vector(shape=(1,), buffer=np.array([1], dtype=mytype), dtype=mytype)) for mytype in ('float', 'int', 'float128')]
[4317742576, 4317742576, 4317742576]
>>> [hash(Vector(shape=(1,), buffer=np.array([1], dtype=mytype), dtype=mytype)) for mytype in ('float', 'int', 'float128')]
[269858911, 269858911, 269858911]

我感到困惑...在numpy中是否有一些(与类型无关的)缓存机制?


这似乎展示了如何让它工作,似乎处理了数组可变的事实。https://dev59.com/bHI-5IYBdhLWcg3wQWGc#5173201 - deinonychusaur
我知道可变对象不应该是可哈希的。但是在这里,我的Vector类只是从不可哈希的numpy.ndarray派生而来,然而Vector类却是可哈希的,即使它是可变的。 - marchelbling
在我看来,被哈希的是内存引用或类似的东西,如果你只是重复向量=Vector(shape=(1,), buffer=nparray)并检查它的哈希值,它应该已经改变了。 - deinonychusaur
2个回答

8
我在Python 2.6.6和numpy 1.3.0中得到了相同的结果。根据Python词汇表,如果定义了__hash__(且不为None),并且定义了__eq____cmp__之一,则对象应该是可哈希的。 ndarray.__eq__ndarray.__hash__都被定义并返回有意义的内容,因此我不明白为什么hash会失败。在快速搜索后,我发现python.scientific.devel邮件列表上的这篇文章,它指出数组从来没有被设计成可哈希的 - 因此我不知道为什么定义了ndarray.__hash__。请注意,isinstance(nparray,collections.Hashable)返回True
编辑:请注意,nparray.__hash__() 的返回值与 id(nparray) 相同,因此这只是默认实现。也许在早期版本的Python中删除 __hash__ 的实现很困难或不可能(__hash__ = None 技术显然是在2.6中引入的),因此他们使用了某种C API魔法来以不会传播到子类的方式实现这一点,并且不会阻止您明确调用 ndarray.__hash__
在Python 3.2.2和当前的numpy 2.0.0版本中有所不同。方法__cmp__已经不存在,因此现在需要__hash____eq__来实现可哈希性(请参见Python 3词汇表)。在这个版本的numpy中,ndarray.__hash__被定义为None,因此无法调用。hash(nparray)失败,并且isinstance(nparray,collections.Hashable)返回预期的Falsehash(vector)也失败了。

非常感谢您的回答。关于您的编辑,我并没有复制您所说的内容。实际上,我的代码如下: >>> nparray.hash() 269709177 >>> id(nparray) 4315346832 所以我仍然感到困惑。我将这段代码添加到我的帖子中,因为在评论中无法阅读代码。 - marchelbling
嗯……在Python 2.6.6和numpy 1.3.0中,对于我来说哈希值是相同的,但在Python 2.7.2和numpy 1.5.1中则不同。这一切非常奇怪。如果我没有在每个Python版本上都有一个不同版本的numpy,那可能会更有帮助。无论如何,据我所见,__hash__的默认定义已经返回id相当长一段时间了,因此我想他们必须已经明确地覆盖了它以在至少某些版本的numpy中执行不同的操作。 - James

2

这并不是一个清晰的答案,但以下是一些可以跟随的线索来理解这种行为。

我指的是numpy 1.6.1版本的代码。

根据numpy.ndarray对象实现(查看numpy/core/src/multiarray/arrayobject.c),hash方法被设置为NULL

NPY_NO_EXPORT PyTypeObject PyArray_Type = {
#if defined(NPY_PY3K)
    PyVarObject_HEAD_INIT(NULL, 0)
#else
    PyObject_HEAD_INIT(NULL)
    0,                                          /* ob_size */
#endif
    "numpy.ndarray",                            /* tp_name */
    sizeof(PyArrayObject),                      /* tp_basicsize */
    &array_as_mapping,                          /* tp_as_mapping */
    (hashfunc)0,                                /* tp_hash */

这个tp_hash属性似乎在numpy/core/src/multiarray/multiarraymodule.c中被覆盖。请参见DUAL_INHERITDUAL_INHERIT2initmultiarray函数,其中修改了tp_hash属性。

例如: PyArrayDescr_Type.tp_hash = PyArray_DescrHash

根据hashdescr.c,哈希实现如下:

* How does this work ? The hash is computed from a list which contains all the
* information specific to a type. The hard work is to build the list
* (_array_descr_walk). The list is built as follows:
*      * If the dtype is builtin (no fields, no subarray), then the list
*      contains 6 items which uniquely define one dtype (_array_descr_builtin)
*      * If the dtype is a compound array, one walk on each field. For each
*      field, we append title, names, offset to the final list used for
*      hashing, and then append the list recursively built for each
*      corresponding dtype (_array_descr_walk_fields)
*      * If the dtype is a subarray, one adds the shape tuple to the list, and
*      then append the list recursively built for each corresponding type
*      (_array_descr_walk_subarray)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接