Skip to content

Commit 7f91ec4

Browse files
authored
CLN: Add allow_slice to is_hashable function (#62567)
1 parent ded274a commit 7f91ec4

File tree

5 files changed

+59
-17
lines changed

5 files changed

+59
-17
lines changed

pandas/core/dtypes/inference.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def is_named_tuple(obj: object) -> bool:
385385

386386

387387
@set_module("pandas.api.types")
388-
def is_hashable(obj: object) -> TypeGuard[Hashable]:
388+
def is_hashable(obj: object, allow_slice: bool = True) -> TypeGuard[Hashable]:
389389
"""
390390
Return True if hash(obj) will succeed, False otherwise.
391391
@@ -399,13 +399,17 @@ def is_hashable(obj: object) -> TypeGuard[Hashable]:
399399
----------
400400
obj : object
401401
The object to check for hashability. Any Python object can be passed here.
402+
allow_slice : bool
403+
If True, return True if the object is hashable (including slices).
404+
If False, return True if the object is hashable and not a slice.
402405
403406
Returns
404407
-------
405408
bool
406409
True if object can be hashed (i.e., does not raise TypeError when
407-
passed to hash()), and False otherwise (e.g., if object is mutable
408-
like a list or dictionary).
410+
passed to hash()) and passes the slice check according to 'allow_slice'.
411+
False otherwise (e.g., if object is mutable like a list or dictionary
412+
or if allow_slice is False and object is a slice or contains a slice).
409413
410414
See Also
411415
--------
@@ -431,6 +435,12 @@ def is_hashable(obj: object) -> TypeGuard[Hashable]:
431435
# Reconsider this decision once this numpy bug is fixed:
432436
# https://github.com/numpy/numpy/issues/5562
433437

438+
if allow_slice is False:
439+
if isinstance(obj, tuple) and any(isinstance(v, slice) for v in obj):
440+
return False
441+
elif isinstance(obj, slice):
442+
return False
443+
434444
try:
435445
hash(obj)
436446
except TypeError:

pandas/core/frame.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4156,7 +4156,7 @@ def __getitem__(self, key):
41564156
key = lib.item_from_zerodim(key)
41574157
key = com.apply_if_callable(key, self)
41584158

4159-
if is_hashable(key) and not is_iterator(key) and not isinstance(key, slice):
4159+
if is_hashable(key, allow_slice=False) and not is_iterator(key):
41604160
# is_iterator to exclude generator e.g. test_getitem_listlike
41614161
# As of Python 3.12, slice is hashable which breaks MultiIndex (GH#57500)
41624162

pandas/core/indexing.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -789,8 +789,7 @@ def _get_setitem_indexer(self, key):
789789
if (
790790
isinstance(ax, MultiIndex)
791791
and self.name != "iloc"
792-
and is_hashable(key)
793-
and not isinstance(key, slice)
792+
and is_hashable(key, allow_slice=False)
794793
):
795794
with suppress(KeyError, InvalidIndexError):
796795
# TypeError e.g. passed a bool
@@ -1124,14 +1123,6 @@ def _getitem_nested_tuple(self, tup: tuple):
11241123
# we have a nested tuple so have at least 1 multi-index level
11251124
# we should be able to match up the dimensionality here
11261125

1127-
def _contains_slice(x: object) -> bool:
1128-
# Check if object is a slice or a tuple containing a slice
1129-
if isinstance(x, tuple):
1130-
return any(isinstance(v, slice) for v in x)
1131-
elif isinstance(x, slice):
1132-
return True
1133-
return False
1134-
11351126
for key in tup:
11361127
check_dict_or_set_indexers(key)
11371128

@@ -1143,8 +1134,7 @@ def _contains_slice(x: object) -> bool:
11431134
# This should never be reached, but let's be explicit about it
11441135
raise ValueError("Too many indices") # pragma: no cover
11451136
if all(
1146-
(is_hashable(x) and not _contains_slice(x)) or com.is_null_slice(x)
1147-
for x in tup
1137+
is_hashable(x, allow_slice=False) or com.is_null_slice(x) for x in tup
11481138
):
11491139
# GH#10521 Series should reduce MultiIndex dimensions instead of
11501140
# DataFrame, IndexingError is not raised when slice(None,None,None)

pandas/core/series.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ def __getitem__(self, key):
956956
if is_iterator(key):
957957
key = list(key)
958958

959-
if is_hashable(key) and not isinstance(key, slice):
959+
if is_hashable(key, allow_slice=False):
960960
# Otherwise index.get_value will raise InvalidIndexError
961961
try:
962962
# For labels that don't resolve as scalars like tuples and frozensets

pandas/tests/dtypes/test_inference.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
missing as libmissing,
3535
ops as libops,
3636
)
37+
from pandas.compat import PY312
3738
from pandas.compat.numpy import np_version_gt2
3839
from pandas.errors import Pandas4Warning
3940

@@ -452,16 +453,57 @@ class UnhashableClass2:
452453
def __hash__(self):
453454
raise TypeError("Not hashable")
454455

456+
# Temporary helper for Python 3.11 compatibility.
457+
# This can be removed once support for Python 3.11 is dropped.
458+
class HashableSlice:
459+
def __init__(self, start, stop, step=None):
460+
self.slice = slice(start, stop, step)
461+
462+
def __eq__(self, other):
463+
return isinstance(other, HashableSlice) and self.slice == other.slice
464+
465+
def __hash__(self):
466+
return hash((self.slice.start, self.slice.stop, self.slice.step))
467+
468+
def __repr__(self):
469+
return (
470+
f"HashableSlice({self.slice.start}, {self.slice.stop}, "
471+
f"{self.slice.step})"
472+
)
473+
455474
hashable = (1, 3.14, np.float64(3.14), "a", (), (1,), HashableClass())
456475
not_hashable = ([], UnhashableClass1())
457476
abc_hashable_not_really_hashable = (([],), UnhashableClass2())
477+
hashable_slice = HashableSlice(1, 2)
478+
tuple_with_slice = (slice(1, 2), 3)
458479

459480
for i in hashable:
460481
assert inference.is_hashable(i)
482+
assert inference.is_hashable(i, allow_slice=True)
483+
assert inference.is_hashable(i, allow_slice=False)
461484
for i in not_hashable:
462485
assert not inference.is_hashable(i)
486+
assert not inference.is_hashable(i, allow_slice=True)
487+
assert not inference.is_hashable(i, allow_slice=False)
463488
for i in abc_hashable_not_really_hashable:
464489
assert not inference.is_hashable(i)
490+
assert not inference.is_hashable(i, allow_slice=True)
491+
assert not inference.is_hashable(i, allow_slice=False)
492+
493+
assert inference.is_hashable(hashable_slice)
494+
assert inference.is_hashable(hashable_slice, allow_slice=True)
495+
assert inference.is_hashable(hashable_slice, allow_slice=False)
496+
497+
if PY312:
498+
for obj in [slice(1, 2), tuple_with_slice]:
499+
assert inference.is_hashable(obj)
500+
assert inference.is_hashable(obj, allow_slice=True)
501+
assert not inference.is_hashable(obj, allow_slice=False)
502+
else:
503+
for obj in [slice(1, 2), tuple_with_slice]:
504+
assert not inference.is_hashable(obj)
505+
assert not inference.is_hashable(obj, allow_slice=True)
506+
assert not inference.is_hashable(obj, allow_slice=False)
465507

466508
# numpy.array is no longer collections.abc.Hashable as of
467509
# https://github.com/numpy/numpy/pull/5326, just test

0 commit comments

Comments
 (0)