Skip to content

Commit fea3466

Browse files
committed
Implement Map.__hash__
1 parent 4e8a40d commit fea3466

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ test:
88
python setup.py test -v
99

1010
rtest:
11+
~/dev/venvs/36-debug/bin/python setup.py build_ext --inplace
12+
1113
env PYTHONPATH=. \
1214
~/dev/venvs/36-debug/bin/python -m test.regrtest -R3:3 --testdir tests/

immutables/_map.c

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2453,6 +2453,8 @@ map_alloc(void)
24532453
return NULL;
24542454
}
24552455
o->h_weakreflist = NULL;
2456+
o->h_hash = -1;
2457+
o->h_count = 0;
24562458
PyObject_GC_Track(o);
24572459
return o;
24582460
}
@@ -2478,8 +2480,6 @@ map_new(void)
24782480
return NULL;
24792481
}
24802482

2481-
o->h_count = 0;
2482-
24832483
if (_empty_map == NULL) {
24842484
Py_INCREF(o);
24852485
_empty_map = o;
@@ -2942,6 +2942,65 @@ map_py_repr(MapObject *self)
29422942
}
29432943

29442944

2945+
static Py_uhash_t
2946+
_shuffle_bits(Py_uhash_t h)
2947+
{
2948+
return ((h ^ 89869747UL) ^ (h << 16)) * 3644798167UL;
2949+
}
2950+
2951+
2952+
static Py_hash_t
2953+
map_py_hash(MapObject *self)
2954+
{
2955+
/* Adapted version of frozenset.__hash__: it's important
2956+
that Map.__hash__ is independant of key/values order.
2957+
2958+
Optimization idea: compute and memoize intermediate
2959+
hash values for HAMT nodes.
2960+
*/
2961+
2962+
if (self->h_hash != -1) {
2963+
return self->h_hash;
2964+
}
2965+
2966+
Py_uhash_t hash = 0;
2967+
2968+
MapIteratorState iter;
2969+
map_iter_t iter_res;
2970+
map_iterator_init(&iter, self->h_root);
2971+
do {
2972+
PyObject *v_key;
2973+
PyObject *v_val;
2974+
2975+
iter_res = map_iterator_next(&iter, &v_key, &v_val);
2976+
if (iter_res == I_ITEM) {
2977+
Py_hash_t vh = PyObject_Hash(v_key);
2978+
if (vh == -1) {
2979+
return -1;
2980+
}
2981+
hash ^= _shuffle_bits((Py_uhash_t)vh);
2982+
2983+
vh = PyObject_Hash(v_val);
2984+
if (vh == -1) {
2985+
return -1;
2986+
}
2987+
hash ^= _shuffle_bits((Py_uhash_t)vh);
2988+
}
2989+
} while (iter_res != I_END);
2990+
2991+
hash ^= ((Py_uhash_t)self->h_count * 2 + 1) * 1927868237UL;
2992+
2993+
hash ^= (hash >> 11) ^ (hash >> 25);
2994+
hash = hash * 69069U + 907133923UL;
2995+
2996+
self->h_hash = (Py_hash_t)hash;
2997+
if (self->h_hash == -1) {
2998+
self->h_hash = 1;
2999+
}
3000+
return self->h_hash;
3001+
}
3002+
3003+
29453004
static PyMethodDef Map_methods[] = {
29463005
{"set", (PyCFunction)map_py_set, METH_VARARGS, NULL},
29473006
{"get", (PyCFunction)map_py_get, METH_VARARGS, NULL},
@@ -2987,7 +3046,7 @@ PyTypeObject _Map_Type = {
29873046
.tp_clear = (inquiry)map_tp_clear,
29883047
.tp_new = map_tp_new,
29893048
.tp_weaklistoffset = offsetof(MapObject, h_weakreflist),
2990-
.tp_hash = PyObject_HashNotImplemented,
3049+
.tp_hash = (hashfunc)map_py_hash,
29913050
.tp_repr = (reprfunc)map_py_repr,
29923051
};
29933052

immutables/_map.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ typedef struct {
2222
MapNode *h_root;
2323
PyObject *h_weakreflist;
2424
Py_ssize_t h_count;
25+
Py_hash_t h_hash;
2526
} MapObject;
2627

2728

tests/test_map.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,33 @@ def __repr__(self):
757757
self.assertTrue(repr(h).startswith(
758758
'<immutables.Map({{...}: 1}) at 0x'))
759759

760+
def test_hash_1(self):
761+
h = Map()
762+
self.assertNotEqual(hash(h), -1)
763+
self.assertEqual(hash(h), hash(h))
764+
765+
h = h.set(1, 2).set('a', 'b')
766+
self.assertNotEqual(hash(h), -1)
767+
self.assertEqual(hash(h), hash(h))
768+
769+
self.assertEqual(
770+
hash(h.set(1, 2).set('a', 'b')),
771+
hash(h.set('a', 'b').set(1, 2)))
772+
773+
def test_hash_2(self):
774+
h = Map()
775+
A = HashKey(100, 'A')
776+
777+
m = h.set(1, 2).set(A, 3).set(3, 4)
778+
with self.assertRaises(HashingError):
779+
with HaskKeyCrasher(error_on_hash=True):
780+
hash(m)
781+
782+
m = h.set(1, 2).set(2, A).set(3, 4)
783+
with self.assertRaises(HashingError):
784+
with HaskKeyCrasher(error_on_hash=True):
785+
hash(m)
786+
760787

761788
if __name__ == "__main__":
762789
unittest.main()

0 commit comments

Comments
 (0)