Skip to content

Commit

Permalink
GH-84783: Make the slice object hashable (GH-101264)
Browse files Browse the repository at this point in the history
  • Loading branch information
furkanonder authored Feb 19, 2023
1 parent 5170caf commit 61f1e67
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 12 deletions.
3 changes: 3 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,9 @@ are always available. They are listed here in alphabetical order.
example: ``a[start:stop:step]`` or ``a[start:stop, i]``. See
:func:`itertools.islice` for an alternate version that returns an iterator.

.. versionchanged:: 3.12
Slice objects are now :term:`hashable` (provided :attr:`~slice.start`,
:attr:`~slice.stop`, and :attr:`~slice.step` are hashable).

.. function:: sorted(iterable, /, *, key=None, reverse=False)

Expand Down
7 changes: 1 addition & 6 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,6 @@ def __setitem__(self, index, value):
with self.assertRaises(TypeError):
_testcapi.sequence_set_slice(None, 1, 3, 'xy')

mapping = {1: 'a', 2: 'b', 3: 'c'}
with self.assertRaises(TypeError):
_testcapi.sequence_set_slice(mapping, 1, 3, 'xy')
self.assertEqual(mapping, {1: 'a', 2: 'b', 3: 'c'})

def test_sequence_del_slice(self):
# Correct case:
data = [1, 2, 3, 4, 5]
Expand Down Expand Up @@ -459,7 +454,7 @@ def __delitem__(self, index):
_testcapi.sequence_del_slice(None, 1, 3)

mapping = {1: 'a', 2: 'b', 3: 'c'}
with self.assertRaises(TypeError):
with self.assertRaises(KeyError):
_testcapi.sequence_del_slice(mapping, 1, 3)
self.assertEqual(mapping, {1: 'a', 2: 'b', 3: 'c'})

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ def non_Python_modules(): r"""
>>> import builtins
>>> tests = doctest.DocTestFinder().find(builtins)
>>> 825 < len(tests) < 845 # approximate number of objects with docstrings
>>> 830 < len(tests) < 850 # approximate number of objects with docstrings
True
>>> real_tests = [t for t in tests if len(t.examples) > 0]
>>> len(real_tests) # objects that actually have doctests
Expand Down
12 changes: 9 additions & 3 deletions Lib/test/test_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ def test_repr(self):
self.assertEqual(repr(slice(1, 2, 3)), "slice(1, 2, 3)")

def test_hash(self):
# Verify clearing of SF bug #800796
self.assertRaises(TypeError, hash, slice(5))
self.assertEqual(hash(slice(5)), slice(5).__hash__())
self.assertEqual(hash(slice(1, 2)), slice(1, 2).__hash__())
self.assertEqual(hash(slice(1, 2, 3)), slice(1, 2, 3).__hash__())
self.assertNotEqual(slice(5), slice(6))

with self.assertRaises(TypeError):
hash(slice(1, 2, []))

with self.assertRaises(TypeError):
slice(5).__hash__()
hash(slice(4, {}))

def test_cmp(self):
s1 = slice(1, 2, 3)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the slice object hashable.
38 changes: 37 additions & 1 deletion Objects/sliceobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,42 @@ slice_traverse(PySliceObject *v, visitproc visit, void *arg)
return 0;
}

/* code based on tuplehash() of Objects/tupleobject.c */
#if SIZEOF_PY_UHASH_T > 4
#define _PyHASH_XXPRIME_1 ((Py_uhash_t)11400714785074694791ULL)
#define _PyHASH_XXPRIME_2 ((Py_uhash_t)14029467366897019727ULL)
#define _PyHASH_XXPRIME_5 ((Py_uhash_t)2870177450012600261ULL)
#define _PyHASH_XXROTATE(x) ((x << 31) | (x >> 33)) /* Rotate left 31 bits */
#else
#define _PyHASH_XXPRIME_1 ((Py_uhash_t)2654435761UL)
#define _PyHASH_XXPRIME_2 ((Py_uhash_t)2246822519UL)
#define _PyHASH_XXPRIME_5 ((Py_uhash_t)374761393UL)
#define _PyHASH_XXROTATE(x) ((x << 13) | (x >> 19)) /* Rotate left 13 bits */
#endif

static Py_hash_t
slicehash(PySliceObject *v)
{
Py_uhash_t acc = _PyHASH_XXPRIME_5;
#define _PyHASH_SLICE_PART(com) { \
Py_uhash_t lane = PyObject_Hash(v->com); \
if(lane == (Py_uhash_t)-1) { \
return -1; \
} \
acc += lane * _PyHASH_XXPRIME_2; \
acc = _PyHASH_XXROTATE(acc); \
acc *= _PyHASH_XXPRIME_1; \
}
_PyHASH_SLICE_PART(start);
_PyHASH_SLICE_PART(stop);
_PyHASH_SLICE_PART(step);
#undef _PyHASH_SLICE_PART
if(acc == (Py_uhash_t)-1) {
return 1546275796;
}
return acc;
}

PyTypeObject PySlice_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"slice", /* Name of this type */
Expand All @@ -642,7 +678,7 @@ PyTypeObject PySlice_Type = {
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
PyObject_HashNotImplemented, /* tp_hash */
(hashfunc)slicehash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
Expand Down
2 changes: 1 addition & 1 deletion Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ tuplerepr(PyTupleObject *v)

/* Hash for tuples. This is a slightly simplified version of the xxHash
non-cryptographic hash:
- we do not use any parallellism, there is only 1 accumulator.
- we do not use any parallelism, there is only 1 accumulator.
- we drop the final mixing since this is just a permutation of the
output space: it does not help against collisions.
- at the end, we mangle the length with a single constant.
Expand Down

0 comments on commit 61f1e67

Please sign in to comment.