Skip to content

Commit b05fdcd

Browse files
authored
ENH: enable mul, div on Index by dispatching to Series (#34160)
1 parent 6af60b2 commit b05fdcd

File tree

9 files changed

+73
-82
lines changed

9 files changed

+73
-82
lines changed

doc/source/whatsnew/v1.2.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Enhancements
1717

1818
Other enhancements
1919
^^^^^^^^^^^^^^^^^^
20-
20+
- :class:`Index` with object dtype supports division and multiplication (:issue:`34160`)
2121
-
2222
-
2323

pandas/core/indexes/base.py

+3-55
Original file line numberDiff line numberDiff line change
@@ -2377,31 +2377,10 @@ def _get_unique_index(self, dropna: bool = False):
23772377
# --------------------------------------------------------------------
23782378
# Arithmetic & Logical Methods
23792379

2380-
def __add__(self, other):
2381-
if isinstance(other, (ABCSeries, ABCDataFrame)):
2382-
return NotImplemented
2383-
from pandas import Series
2384-
2385-
return Index(Series(self) + other)
2386-
2387-
def __radd__(self, other):
2388-
from pandas import Series
2389-
2390-
return Index(other + Series(self))
2391-
23922380
def __iadd__(self, other):
23932381
# alias for __add__
23942382
return self + other
23952383

2396-
def __sub__(self, other):
2397-
return Index(np.array(self) - other)
2398-
2399-
def __rsub__(self, other):
2400-
# wrap Series to ensure we pin name correctly
2401-
from pandas import Series
2402-
2403-
return Index(other - Series(self))
2404-
24052384
def __and__(self, other):
24062385
return self.intersection(other)
24072386

@@ -5293,38 +5272,6 @@ def _add_comparison_methods(cls):
52935272
cls.__le__ = _make_comparison_op(operator.le, cls)
52945273
cls.__ge__ = _make_comparison_op(operator.ge, cls)
52955274

5296-
@classmethod
5297-
def _add_numeric_methods_add_sub_disabled(cls):
5298-
"""
5299-
Add in the numeric add/sub methods to disable.
5300-
"""
5301-
cls.__add__ = make_invalid_op("__add__")
5302-
cls.__radd__ = make_invalid_op("__radd__")
5303-
cls.__iadd__ = make_invalid_op("__iadd__")
5304-
cls.__sub__ = make_invalid_op("__sub__")
5305-
cls.__rsub__ = make_invalid_op("__rsub__")
5306-
cls.__isub__ = make_invalid_op("__isub__")
5307-
5308-
@classmethod
5309-
def _add_numeric_methods_disabled(cls):
5310-
"""
5311-
Add in numeric methods to disable other than add/sub.
5312-
"""
5313-
cls.__pow__ = make_invalid_op("__pow__")
5314-
cls.__rpow__ = make_invalid_op("__rpow__")
5315-
cls.__mul__ = make_invalid_op("__mul__")
5316-
cls.__rmul__ = make_invalid_op("__rmul__")
5317-
cls.__floordiv__ = make_invalid_op("__floordiv__")
5318-
cls.__rfloordiv__ = make_invalid_op("__rfloordiv__")
5319-
cls.__truediv__ = make_invalid_op("__truediv__")
5320-
cls.__rtruediv__ = make_invalid_op("__rtruediv__")
5321-
cls.__mod__ = make_invalid_op("__mod__")
5322-
cls.__divmod__ = make_invalid_op("__divmod__")
5323-
cls.__neg__ = make_invalid_op("__neg__")
5324-
cls.__pos__ = make_invalid_op("__pos__")
5325-
cls.__abs__ = make_invalid_op("__abs__")
5326-
cls.__inv__ = make_invalid_op("__inv__")
5327-
53285275
@classmethod
53295276
def _add_numeric_methods_binary(cls):
53305277
"""
@@ -5340,11 +5287,12 @@ def _add_numeric_methods_binary(cls):
53405287
cls.__truediv__ = _make_arithmetic_op(operator.truediv, cls)
53415288
cls.__rtruediv__ = _make_arithmetic_op(ops.rtruediv, cls)
53425289

5343-
# TODO: rmod? rdivmod?
53445290
cls.__mod__ = _make_arithmetic_op(operator.mod, cls)
5291+
cls.__rmod__ = _make_arithmetic_op(ops.rmod, cls)
53455292
cls.__floordiv__ = _make_arithmetic_op(operator.floordiv, cls)
53465293
cls.__rfloordiv__ = _make_arithmetic_op(ops.rfloordiv, cls)
53475294
cls.__divmod__ = _make_arithmetic_op(divmod, cls)
5295+
cls.__rdivmod__ = _make_arithmetic_op(ops.rdivmod, cls)
53485296
cls.__mul__ = _make_arithmetic_op(operator.mul, cls)
53495297
cls.__rmul__ = _make_arithmetic_op(ops.rmul, cls)
53505298

@@ -5504,7 +5452,7 @@ def shape(self):
55045452
return self._values.shape
55055453

55065454

5507-
Index._add_numeric_methods_disabled()
5455+
Index._add_numeric_methods()
55085456
Index._add_logical_methods()
55095457
Index._add_comparison_methods()
55105458

pandas/core/indexes/category.py

-2
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,4 @@ def _wrap_joined_index(
770770
return self._create_from_codes(joined, name=name)
771771

772772

773-
CategoricalIndex._add_numeric_methods_add_sub_disabled()
774-
CategoricalIndex._add_numeric_methods_disabled()
775773
CategoricalIndex._add_logical_methods_disabled()

pandas/core/indexes/datetimes.py

-1
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,6 @@ def indexer_between_time(
842842
return mask.nonzero()[0]
843843

844844

845-
DatetimeIndex._add_numeric_methods_disabled()
846845
DatetimeIndex._add_logical_methods_disabled()
847846

848847

pandas/core/indexes/multi.py

+35
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from pandas.core.indexes.frozen import FrozenList
5151
from pandas.core.indexes.numeric import Int64Index
5252
import pandas.core.missing as missing
53+
from pandas.core.ops.invalid import make_invalid_op
5354
from pandas.core.sorting import (
5455
get_group_index,
5556
indexer_from_factorized,
@@ -3606,6 +3607,40 @@ def isin(self, values, level=None):
36063607
return np.zeros(len(levs), dtype=np.bool_)
36073608
return levs.isin(values)
36083609

3610+
@classmethod
3611+
def _add_numeric_methods_add_sub_disabled(cls):
3612+
"""
3613+
Add in the numeric add/sub methods to disable.
3614+
"""
3615+
cls.__add__ = make_invalid_op("__add__")
3616+
cls.__radd__ = make_invalid_op("__radd__")
3617+
cls.__iadd__ = make_invalid_op("__iadd__")
3618+
cls.__sub__ = make_invalid_op("__sub__")
3619+
cls.__rsub__ = make_invalid_op("__rsub__")
3620+
cls.__isub__ = make_invalid_op("__isub__")
3621+
3622+
@classmethod
3623+
def _add_numeric_methods_disabled(cls):
3624+
"""
3625+
Add in numeric methods to disable other than add/sub.
3626+
"""
3627+
cls.__pow__ = make_invalid_op("__pow__")
3628+
cls.__rpow__ = make_invalid_op("__rpow__")
3629+
cls.__mul__ = make_invalid_op("__mul__")
3630+
cls.__rmul__ = make_invalid_op("__rmul__")
3631+
cls.__floordiv__ = make_invalid_op("__floordiv__")
3632+
cls.__rfloordiv__ = make_invalid_op("__rfloordiv__")
3633+
cls.__truediv__ = make_invalid_op("__truediv__")
3634+
cls.__rtruediv__ = make_invalid_op("__rtruediv__")
3635+
cls.__mod__ = make_invalid_op("__mod__")
3636+
cls.__rmod__ = make_invalid_op("__rmod__")
3637+
cls.__divmod__ = make_invalid_op("__divmod__")
3638+
cls.__rdivmod__ = make_invalid_op("__rdivmod__")
3639+
cls.__neg__ = make_invalid_op("__neg__")
3640+
cls.__pos__ = make_invalid_op("__pos__")
3641+
cls.__abs__ = make_invalid_op("__abs__")
3642+
cls.__inv__ = make_invalid_op("__inv__")
3643+
36093644

36103645
MultiIndex._add_numeric_methods_disabled()
36113646
MultiIndex._add_numeric_methods_add_sub_disabled()

pandas/core/indexes/period.py

-1
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,6 @@ def memory_usage(self, deep=False):
724724
return result
725725

726726

727-
PeriodIndex._add_numeric_methods_disabled()
728727
PeriodIndex._add_logical_methods_disabled()
729728

730729

pandas/tests/arithmetic/test_numeric.py

-14
Original file line numberDiff line numberDiff line change
@@ -548,20 +548,6 @@ class TestMultiplicationDivision:
548548
# __mul__, __rmul__, __div__, __rdiv__, __floordiv__, __rfloordiv__
549549
# for non-timestamp/timedelta/period dtypes
550550

551-
@pytest.mark.parametrize(
552-
"box",
553-
[
554-
pytest.param(
555-
pd.Index,
556-
marks=pytest.mark.xfail(
557-
reason="Index.__div__ always raises", raises=TypeError
558-
),
559-
),
560-
pd.Series,
561-
pd.DataFrame,
562-
],
563-
ids=lambda x: x.__name__,
564-
)
565551
def test_divide_decimal(self, box):
566552
# resolves issue GH#9787
567553
ser = Series([Decimal(10)])

pandas/tests/indexes/categorical/test_category.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ def test_disallow_addsub_ops(self, func, op_name):
4343
# GH 10039
4444
# set ops (+/-) raise TypeError
4545
idx = pd.Index(pd.Categorical(["a", "b"]))
46-
msg = f"cannot perform {op_name} with this index type: CategoricalIndex"
46+
cat_or_list = "'(Categorical|list)' and '(Categorical|list)'"
47+
msg = "|".join(
48+
[
49+
f"cannot perform {op_name} with this index type: CategoricalIndex",
50+
"can only concatenate list",
51+
rf"unsupported operand type\(s\) for [\+-]: {cat_or_list}",
52+
]
53+
)
4754
with pytest.raises(TypeError, match=msg):
4855
func(idx)
4956

pandas/tests/indexes/common.py

+26-7
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,41 @@ def test_numeric_compat(self):
146146
# Check that this doesn't cover MultiIndex case, if/when it does,
147147
# we can remove multi.test_compat.test_numeric_compat
148148
assert not isinstance(idx, MultiIndex)
149+
if type(idx) is Index:
150+
return
149151

150-
with pytest.raises(TypeError, match="cannot perform __mul__"):
152+
typ = type(idx._data).__name__
153+
lmsg = "|".join(
154+
[
155+
rf"unsupported operand type\(s\) for \*: '{typ}' and 'int'",
156+
"cannot perform (__mul__|__truediv__|__floordiv__) with "
157+
f"this index type: {typ}",
158+
]
159+
)
160+
with pytest.raises(TypeError, match=lmsg):
151161
idx * 1
152-
with pytest.raises(TypeError, match="cannot perform __rmul__"):
162+
rmsg = "|".join(
163+
[
164+
rf"unsupported operand type\(s\) for \*: 'int' and '{typ}'",
165+
"cannot perform (__rmul__|__rtruediv__|__rfloordiv__) with "
166+
f"this index type: {typ}",
167+
]
168+
)
169+
with pytest.raises(TypeError, match=rmsg):
153170
1 * idx
154171

155-
div_err = "cannot perform __truediv__"
172+
div_err = lmsg.replace("*", "/")
156173
with pytest.raises(TypeError, match=div_err):
157174
idx / 1
158-
159-
div_err = div_err.replace(" __", " __r")
175+
div_err = rmsg.replace("*", "/")
160176
with pytest.raises(TypeError, match=div_err):
161177
1 / idx
162-
with pytest.raises(TypeError, match="cannot perform __floordiv__"):
178+
179+
floordiv_err = lmsg.replace("*", "//")
180+
with pytest.raises(TypeError, match=floordiv_err):
163181
idx // 1
164-
with pytest.raises(TypeError, match="cannot perform __rfloordiv__"):
182+
floordiv_err = rmsg.replace("*", "//")
183+
with pytest.raises(TypeError, match=floordiv_err):
165184
1 // idx
166185

167186
def test_logical_compat(self):

0 commit comments

Comments
 (0)