diff --git a/pandas/core/arrays/boolean.py b/pandas/core/arrays/boolean.py index 4dd117e407961..9ffe00cf3189a 100644 --- a/pandas/core/arrays/boolean.py +++ b/pandas/core/arrays/boolean.py @@ -6,7 +6,6 @@ from pandas._libs import lib, missing as libmissing from pandas._typing import ArrayLike -from pandas.compat import set_function_name from pandas.compat.numpy import function as nv from pandas.core.dtypes.common import ( @@ -23,7 +22,6 @@ from pandas.core.dtypes.missing import isna from pandas.core import ops -from pandas.core.arraylike import OpsMixin from .masked import BaseMaskedArray, BaseMaskedDtype @@ -203,7 +201,7 @@ def coerce_to_array( return values, mask -class BooleanArray(OpsMixin, BaseMaskedArray): +class BooleanArray(BaseMaskedArray): """ Array of boolean (True/False) data with missing values. @@ -561,48 +559,40 @@ def all(self, skipna: bool = True, **kwargs): else: return self.dtype.na_value - @classmethod - def _create_logical_method(cls, op): - @ops.unpack_zerodim_and_defer(op.__name__) - def logical_method(self, other): - - assert op.__name__ in {"or_", "ror_", "and_", "rand_", "xor", "rxor"} - other_is_booleanarray = isinstance(other, BooleanArray) - other_is_scalar = lib.is_scalar(other) - mask = None - - if other_is_booleanarray: - other, mask = other._data, other._mask - elif is_list_like(other): - other = np.asarray(other, dtype="bool") - if other.ndim > 1: - raise NotImplementedError( - "can only perform ops with 1-d structures" - ) - other, mask = coerce_to_array(other, copy=False) - elif isinstance(other, np.bool_): - other = other.item() - - if other_is_scalar and not (other is libmissing.NA or lib.is_bool(other)): - raise TypeError( - "'other' should be pandas.NA or a bool. " - f"Got {type(other).__name__} instead." - ) - - if not other_is_scalar and len(self) != len(other): - raise ValueError("Lengths must match to compare") + def _logical_method(self, other, op): + + assert op.__name__ in {"or_", "ror_", "and_", "rand_", "xor", "rxor"} + other_is_booleanarray = isinstance(other, BooleanArray) + other_is_scalar = lib.is_scalar(other) + mask = None + + if other_is_booleanarray: + other, mask = other._data, other._mask + elif is_list_like(other): + other = np.asarray(other, dtype="bool") + if other.ndim > 1: + raise NotImplementedError("can only perform ops with 1-d structures") + other, mask = coerce_to_array(other, copy=False) + elif isinstance(other, np.bool_): + other = other.item() - if op.__name__ in {"or_", "ror_"}: - result, mask = ops.kleene_or(self._data, other, self._mask, mask) - elif op.__name__ in {"and_", "rand_"}: - result, mask = ops.kleene_and(self._data, other, self._mask, mask) - elif op.__name__ in {"xor", "rxor"}: - result, mask = ops.kleene_xor(self._data, other, self._mask, mask) + if other_is_scalar and not (other is libmissing.NA or lib.is_bool(other)): + raise TypeError( + "'other' should be pandas.NA or a bool. " + f"Got {type(other).__name__} instead." + ) - return BooleanArray(result, mask) + if not other_is_scalar and len(self) != len(other): + raise ValueError("Lengths must match to compare") - name = f"__{op.__name__}__" - return set_function_name(logical_method, name, cls) + if op.__name__ in {"or_", "ror_"}: + result, mask = ops.kleene_or(self._data, other, self._mask, mask) + elif op.__name__ in {"and_", "rand_"}: + result, mask = ops.kleene_and(self._data, other, self._mask, mask) + elif op.__name__ in {"xor", "rxor"}: + result, mask = ops.kleene_xor(self._data, other, self._mask, mask) + + return BooleanArray(result, mask) def _cmp_method(self, other, op): from pandas.arrays import FloatingArray, IntegerArray @@ -643,6 +633,50 @@ def _cmp_method(self, other, op): return BooleanArray(result, mask, copy=False) + def _arith_method(self, other, op): + mask = None + op_name = op.__name__ + + if isinstance(other, BooleanArray): + other, mask = other._data, other._mask + + elif is_list_like(other): + other = np.asarray(other) + if other.ndim > 1: + raise NotImplementedError("can only perform ops with 1-d structures") + if len(self) != len(other): + raise ValueError("Lengths must match") + + # nans propagate + if mask is None: + mask = self._mask + if other is libmissing.NA: + mask |= True + else: + mask = self._mask | mask + + if other is libmissing.NA: + # if other is NA, the result will be all NA and we can't run the + # actual op, so we need to choose the resulting dtype manually + if op_name in {"floordiv", "rfloordiv", "mod", "rmod", "pow", "rpow"}: + dtype = "int8" + else: + dtype = "bool" + result = np.zeros(len(self._data), dtype=dtype) + else: + with np.errstate(all="ignore"): + result = op(self._data, other) + + # divmod returns a tuple + if op_name == "divmod": + div, mod = result + return ( + self._maybe_mask_result(div, mask, other, "floordiv"), + self._maybe_mask_result(mod, mask, other, "mod"), + ) + + return self._maybe_mask_result(result, mask, other, op_name) + def _reduce(self, name: str, skipna: bool = True, **kwargs): if name in {"any", "all"}: @@ -678,60 +712,3 @@ def _maybe_mask_result(self, result, mask, other, op_name: str): else: result[mask] = np.nan return result - - @classmethod - def _create_arithmetic_method(cls, op): - op_name = op.__name__ - - @ops.unpack_zerodim_and_defer(op_name) - def boolean_arithmetic_method(self, other): - mask = None - - if isinstance(other, BooleanArray): - other, mask = other._data, other._mask - - elif is_list_like(other): - other = np.asarray(other) - if other.ndim > 1: - raise NotImplementedError( - "can only perform ops with 1-d structures" - ) - if len(self) != len(other): - raise ValueError("Lengths must match") - - # nans propagate - if mask is None: - mask = self._mask - if other is libmissing.NA: - mask |= True - else: - mask = self._mask | mask - - if other is libmissing.NA: - # if other is NA, the result will be all NA and we can't run the - # actual op, so we need to choose the resulting dtype manually - if op_name in {"floordiv", "rfloordiv", "mod", "rmod", "pow", "rpow"}: - dtype = "int8" - else: - dtype = "bool" - result = np.zeros(len(self._data), dtype=dtype) - else: - with np.errstate(all="ignore"): - result = op(self._data, other) - - # divmod returns a tuple - if op_name == "divmod": - div, mod = result - return ( - self._maybe_mask_result(div, mask, other, "floordiv"), - self._maybe_mask_result(mod, mask, other, "mod"), - ) - - return self._maybe_mask_result(result, mask, other, op_name) - - name = f"__{op_name}__" - return set_function_name(boolean_arithmetic_method, name, cls) - - -BooleanArray._add_logical_ops() -BooleanArray._add_arithmetic_ops() diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index aa272f13b045c..02f434342191f 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -6,7 +6,6 @@ from pandas._libs import lib, missing as libmissing from pandas._typing import ArrayLike, DtypeObj -from pandas.compat import set_function_name from pandas.compat.numpy import function as nv from pandas.util._decorators import cache_readonly @@ -26,9 +25,7 @@ from pandas.core.dtypes.missing import isna from pandas.core import ops -from pandas.core.arraylike import OpsMixin from pandas.core.ops import invalid_comparison -from pandas.core.ops.common import unpack_zerodim_and_defer from pandas.core.tools.numeric import to_numeric from .masked import BaseMaskedArray, BaseMaskedDtype @@ -202,7 +199,7 @@ def coerce_to_array( return values, mask -class FloatingArray(OpsMixin, BaseMaskedArray): +class FloatingArray(BaseMaskedArray): """ Array of floating (optional missing) values. @@ -479,83 +476,70 @@ def _maybe_mask_result(self, result, mask, other, op_name: str): return type(self)(result, mask, copy=False) - @classmethod - def _create_arithmetic_method(cls, op): - op_name = op.__name__ - - @unpack_zerodim_and_defer(op.__name__) - def floating_arithmetic_method(self, other): - from pandas.arrays import IntegerArray - - omask = None + def _arith_method(self, other, op): + from pandas.arrays import IntegerArray - if getattr(other, "ndim", 0) > 1: - raise NotImplementedError("can only perform ops with 1-d structures") + omask = None - if isinstance(other, (IntegerArray, FloatingArray)): - other, omask = other._data, other._mask + if getattr(other, "ndim", 0) > 1: + raise NotImplementedError("can only perform ops with 1-d structures") - elif is_list_like(other): - other = np.asarray(other) - if other.ndim > 1: - raise NotImplementedError( - "can only perform ops with 1-d structures" - ) - if len(self) != len(other): - raise ValueError("Lengths must match") - if not (is_float_dtype(other) or is_integer_dtype(other)): - raise TypeError("can only perform ops with numeric values") + if isinstance(other, (IntegerArray, FloatingArray)): + other, omask = other._data, other._mask - else: - if not (is_float(other) or is_integer(other) or other is libmissing.NA): - raise TypeError("can only perform ops with numeric values") + elif is_list_like(other): + other = np.asarray(other) + if other.ndim > 1: + raise NotImplementedError("can only perform ops with 1-d structures") + if len(self) != len(other): + raise ValueError("Lengths must match") + if not (is_float_dtype(other) or is_integer_dtype(other)): + raise TypeError("can only perform ops with numeric values") - if omask is None: - mask = self._mask.copy() - if other is libmissing.NA: - mask |= True - else: - mask = self._mask | omask - - if op_name == "pow": - # 1 ** x is 1. - mask = np.where((self._data == 1) & ~self._mask, False, mask) - # x ** 0 is 1. - if omask is not None: - mask = np.where((other == 0) & ~omask, False, mask) - elif other is not libmissing.NA: - mask = np.where(other == 0, False, mask) - - elif op_name == "rpow": - # 1 ** x is 1. - if omask is not None: - mask = np.where((other == 1) & ~omask, False, mask) - elif other is not libmissing.NA: - mask = np.where(other == 1, False, mask) - # x ** 0 is 1. - mask = np.where((self._data == 0) & ~self._mask, False, mask) + else: + if not (is_float(other) or is_integer(other) or other is libmissing.NA): + raise TypeError("can only perform ops with numeric values") + if omask is None: + mask = self._mask.copy() if other is libmissing.NA: - result = np.ones_like(self._data) - else: - with np.errstate(all="ignore"): - result = op(self._data, other) - - # divmod returns a tuple - if op_name == "divmod": - div, mod = result - return ( - self._maybe_mask_result(div, mask, other, "floordiv"), - self._maybe_mask_result(mod, mask, other, "mod"), - ) - - return self._maybe_mask_result(result, mask, other, op_name) - - name = f"__{op.__name__}__" - return set_function_name(floating_arithmetic_method, name, cls) + mask |= True + else: + mask = self._mask | omask + + if op.__name__ == "pow": + # 1 ** x is 1. + mask = np.where((self._data == 1) & ~self._mask, False, mask) + # x ** 0 is 1. + if omask is not None: + mask = np.where((other == 0) & ~omask, False, mask) + elif other is not libmissing.NA: + mask = np.where(other == 0, False, mask) + + elif op.__name__ == "rpow": + # 1 ** x is 1. + if omask is not None: + mask = np.where((other == 1) & ~omask, False, mask) + elif other is not libmissing.NA: + mask = np.where(other == 1, False, mask) + # x ** 0 is 1. + mask = np.where((self._data == 0) & ~self._mask, False, mask) + if other is libmissing.NA: + result = np.ones_like(self._data) + else: + with np.errstate(all="ignore"): + result = op(self._data, other) + + # divmod returns a tuple + if op.__name__ == "divmod": + div, mod = result + return ( + self._maybe_mask_result(div, mask, other, "floordiv"), + self._maybe_mask_result(mod, mask, other, "mod"), + ) -FloatingArray._add_arithmetic_ops() + return self._maybe_mask_result(result, mask, other, op.__name__) _dtype_docstring = """ diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 856b4bcbda048..88a5a88efe146 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -7,7 +7,6 @@ from pandas._libs import Timedelta, iNaT, lib, missing as libmissing from pandas._typing import ArrayLike, DtypeObj -from pandas.compat import set_function_name from pandas.compat.numpy import function as nv from pandas.util._decorators import cache_readonly @@ -26,9 +25,7 @@ from pandas.core.dtypes.missing import isna from pandas.core import ops -from pandas.core.arraylike import OpsMixin from pandas.core.ops import invalid_comparison -from pandas.core.ops.common import unpack_zerodim_and_defer from pandas.core.tools.numeric import to_numeric from .masked import BaseMaskedArray, BaseMaskedDtype @@ -266,7 +263,7 @@ def coerce_to_array( return values, mask -class IntegerArray(OpsMixin, BaseMaskedArray): +class IntegerArray(BaseMaskedArray): """ Array of integer (optional missing) values. @@ -539,6 +536,73 @@ def _cmp_method(self, other, op): return BooleanArray(result, mask) + def _arith_method(self, other, op): + op_name = op.__name__ + omask = None + + if getattr(other, "ndim", 0) > 1: + raise NotImplementedError("can only perform ops with 1-d structures") + + if isinstance(other, IntegerArray): + other, omask = other._data, other._mask + + elif is_list_like(other): + other = np.asarray(other) + if other.ndim > 1: + raise NotImplementedError("can only perform ops with 1-d structures") + if len(self) != len(other): + raise ValueError("Lengths must match") + if not (is_float_dtype(other) or is_integer_dtype(other)): + raise TypeError("can only perform ops with numeric values") + + elif isinstance(other, (timedelta, np.timedelta64)): + other = Timedelta(other) + + else: + if not (is_float(other) or is_integer(other) or other is libmissing.NA): + raise TypeError("can only perform ops with numeric values") + + if omask is None: + mask = self._mask.copy() + if other is libmissing.NA: + mask |= True + else: + mask = self._mask | omask + + if op_name == "pow": + # 1 ** x is 1. + mask = np.where((self._data == 1) & ~self._mask, False, mask) + # x ** 0 is 1. + if omask is not None: + mask = np.where((other == 0) & ~omask, False, mask) + elif other is not libmissing.NA: + mask = np.where(other == 0, False, mask) + + elif op_name == "rpow": + # 1 ** x is 1. + if omask is not None: + mask = np.where((other == 1) & ~omask, False, mask) + elif other is not libmissing.NA: + mask = np.where(other == 1, False, mask) + # x ** 0 is 1. + mask = np.where((self._data == 0) & ~self._mask, False, mask) + + if other is libmissing.NA: + result = np.ones_like(self._data) + else: + with np.errstate(all="ignore"): + result = op(self._data, other) + + # divmod returns a tuple + if op_name == "divmod": + div, mod = result + return ( + self._maybe_mask_result(div, mask, other, "floordiv"), + self._maybe_mask_result(mod, mask, other, "mod"), + ) + + return self._maybe_mask_result(result, mask, other, op_name) + def sum(self, skipna=True, min_count=0, **kwargs): nv.validate_sum((), kwargs) return super()._reduce("sum", skipna=skipna, min_count=min_count) @@ -581,86 +645,6 @@ def _maybe_mask_result(self, result, mask, other, op_name: str): return type(self)(result, mask, copy=False) - @classmethod - def _create_arithmetic_method(cls, op): - op_name = op.__name__ - - @unpack_zerodim_and_defer(op.__name__) - def integer_arithmetic_method(self, other): - - omask = None - - if getattr(other, "ndim", 0) > 1: - raise NotImplementedError("can only perform ops with 1-d structures") - - if isinstance(other, IntegerArray): - other, omask = other._data, other._mask - - elif is_list_like(other): - other = np.asarray(other) - if other.ndim > 1: - raise NotImplementedError( - "can only perform ops with 1-d structures" - ) - if len(self) != len(other): - raise ValueError("Lengths must match") - if not (is_float_dtype(other) or is_integer_dtype(other)): - raise TypeError("can only perform ops with numeric values") - - elif isinstance(other, (timedelta, np.timedelta64)): - other = Timedelta(other) - - else: - if not (is_float(other) or is_integer(other) or other is libmissing.NA): - raise TypeError("can only perform ops with numeric values") - - if omask is None: - mask = self._mask.copy() - if other is libmissing.NA: - mask |= True - else: - mask = self._mask | omask - - if op_name == "pow": - # 1 ** x is 1. - mask = np.where((self._data == 1) & ~self._mask, False, mask) - # x ** 0 is 1. - if omask is not None: - mask = np.where((other == 0) & ~omask, False, mask) - elif other is not libmissing.NA: - mask = np.where(other == 0, False, mask) - - elif op_name == "rpow": - # 1 ** x is 1. - if omask is not None: - mask = np.where((other == 1) & ~omask, False, mask) - elif other is not libmissing.NA: - mask = np.where(other == 1, False, mask) - # x ** 0 is 1. - mask = np.where((self._data == 0) & ~self._mask, False, mask) - - if other is libmissing.NA: - result = np.ones_like(self._data) - else: - with np.errstate(all="ignore"): - result = op(self._data, other) - - # divmod returns a tuple - if op_name == "divmod": - div, mod = result - return ( - self._maybe_mask_result(div, mask, other, "floordiv"), - self._maybe_mask_result(mod, mask, other, "mod"), - ) - - return self._maybe_mask_result(result, mask, other, op_name) - - name = f"__{op.__name__}__" - return set_function_name(integer_arithmetic_method, name, cls) - - -IntegerArray._add_arithmetic_ops() - _dtype_docstring = """ An ExtensionDtype for {dtype} integer data. diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 97ade0dc70843..9febba0f544ac 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -19,7 +19,8 @@ from pandas.core import nanops from pandas.core.algorithms import factorize_array, take from pandas.core.array_algos import masked_reductions -from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin +from pandas.core.arraylike import OpsMixin +from pandas.core.arrays import ExtensionArray from pandas.core.indexers import check_array_indexer if TYPE_CHECKING: @@ -66,7 +67,7 @@ def construct_array_type(cls) -> Type["BaseMaskedArray"]: raise NotImplementedError -class BaseMaskedArray(ExtensionArray, ExtensionOpsMixin): +class BaseMaskedArray(OpsMixin, ExtensionArray): """ Base class for masked arrays (which use _data and _mask to store the data). diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index b5103fb7f9d5d..810a2ce0cfde5 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -11,12 +11,10 @@ from pandas.core.dtypes.dtypes import ExtensionDtype from pandas.core.dtypes.missing import isna -from pandas import compat from pandas.core import nanops, ops from pandas.core.array_algos import masked_reductions from pandas.core.arraylike import OpsMixin from pandas.core.arrays._mixins import NDArrayBackedExtensionArray -from pandas.core.arrays.base import ExtensionOpsMixin from pandas.core.strings.object_array import ObjectStringArrayMixin @@ -118,7 +116,6 @@ def itemsize(self) -> int: class PandasArray( OpsMixin, NDArrayBackedExtensionArray, - ExtensionOpsMixin, NDArrayOperatorsMixin, ObjectStringArrayMixin, ): @@ -393,15 +390,7 @@ def _cmp_method(self, other, op): return self._wrap_ndarray_result(result) return result - @classmethod - def _create_arithmetic_method(cls, op): - @ops.unpack_zerodim_and_defer(op.__name__) - def arithmetic_method(self, other): - return self._cmp_method(other, op) - - return compat.set_function_name(arithmetic_method, f"__{op.__name__}__", cls) - - _create_comparison_method = _create_arithmetic_method + _arith_method = _cmp_method def _wrap_ndarray_result(self, result: np.ndarray): # If we have timedelta64[ns] result, return a TimedeltaArray instead @@ -415,6 +404,3 @@ def _wrap_ndarray_result(self, result: np.ndarray): # ------------------------------------------------------------------------ # String methods interface _str_na_value = np.nan - - -PandasArray._add_arithmetic_ops() diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index 5a66bf522215a..480bae199683f 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -14,7 +14,6 @@ from pandas._libs.sparse import BlockIndex, IntIndex, SparseIndex from pandas._libs.tslibs import NaT from pandas._typing import Scalar -import pandas.compat as compat from pandas.compat.numpy import function as nv from pandas.errors import PerformanceWarning @@ -41,7 +40,7 @@ import pandas.core.algorithms as algos from pandas.core.arraylike import OpsMixin -from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin +from pandas.core.arrays import ExtensionArray from pandas.core.arrays.sparse.dtype import SparseDtype from pandas.core.base import PandasObject import pandas.core.common as com @@ -50,7 +49,6 @@ from pandas.core.missing import interpolate_2d from pandas.core.nanops import check_below_min_count import pandas.core.ops as ops -from pandas.core.ops.common import unpack_zerodim_and_defer import pandas.io.formats.printing as printing @@ -196,7 +194,7 @@ def _wrap_result(name, data, sparse_index, fill_value, dtype=None): ) -class SparseArray(OpsMixin, PandasObject, ExtensionArray, ExtensionOpsMixin): +class SparseArray(OpsMixin, PandasObject, ExtensionArray): """ An ExtensionArray for storing sparse data. @@ -1388,48 +1386,39 @@ def __abs__(self): # Ops # ------------------------------------------------------------------------ - @classmethod - def _create_arithmetic_method(cls, op): + def _arith_method(self, other, op): op_name = op.__name__ - @unpack_zerodim_and_defer(op_name) - def sparse_arithmetic_method(self, other): + if isinstance(other, SparseArray): + return _sparse_array_op(self, other, op, op_name) - if isinstance(other, SparseArray): - return _sparse_array_op(self, other, op, op_name) + elif is_scalar(other): + with np.errstate(all="ignore"): + fill = op(_get_fill(self), np.asarray(other)) + result = op(self.sp_values, other) - elif is_scalar(other): - with np.errstate(all="ignore"): - fill = op(_get_fill(self), np.asarray(other)) - result = op(self.sp_values, other) - - if op_name == "divmod": - left, right = result - lfill, rfill = fill - return ( - _wrap_result(op_name, left, self.sp_index, lfill), - _wrap_result(op_name, right, self.sp_index, rfill), - ) + if op_name == "divmod": + left, right = result + lfill, rfill = fill + return ( + _wrap_result(op_name, left, self.sp_index, lfill), + _wrap_result(op_name, right, self.sp_index, rfill), + ) - return _wrap_result(op_name, result, self.sp_index, fill) + return _wrap_result(op_name, result, self.sp_index, fill) - else: - other = np.asarray(other) - with np.errstate(all="ignore"): - # TODO: look into _wrap_result - if len(self) != len(other): - raise AssertionError( - f"length mismatch: {len(self)} vs. {len(other)}" - ) - if not isinstance(other, SparseArray): - dtype = getattr(other, "dtype", None) - other = SparseArray( - other, fill_value=self.fill_value, dtype=dtype - ) - return _sparse_array_op(self, other, op, op_name) - - name = f"__{op.__name__}__" - return compat.set_function_name(sparse_arithmetic_method, name, cls) + else: + other = np.asarray(other) + with np.errstate(all="ignore"): + # TODO: look into _wrap_result + if len(self) != len(other): + raise AssertionError( + f"length mismatch: {len(self)} vs. {len(other)}" + ) + if not isinstance(other, SparseArray): + dtype = getattr(other, "dtype", None) + other = SparseArray(other, fill_value=self.fill_value, dtype=dtype) + return _sparse_array_op(self, other, op, op_name) def _cmp_method(self, other, op) -> "SparseArray": if not is_scalar(other) and not isinstance(other, type(self)): @@ -1489,9 +1478,6 @@ def _formatter(self, boxed=False): return None -SparseArray._add_arithmetic_ops() - - def make_sparse(arr: np.ndarray, kind="block", fill_value=None, dtype=None): """ Convert ndarray to sparse format