From bf8680f1cc2831f388f8fd71d35303a4fe99fe11 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 12 Dec 2019 12:07:45 -0600 Subject: [PATCH 01/18] Implement NA.__array_ufunc__ This gives us consistent comparisions with NumPy scalars. --- pandas/_libs/missing.pyx | 116 +++++++++++++++++++++++++- pandas/core/ops/__init__.py | 2 +- pandas/core/ops/dispatch.py | 94 +-------------------- pandas/tests/scalar/test_na_scalar.py | 21 +++-- 4 files changed, 132 insertions(+), 101 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 63aa5501c5250..e2503893e705d 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -290,7 +290,8 @@ def _create_binary_propagating_op(name, divmod=False): def method(self, other): if (other is C_NA or isinstance(other, str) - or isinstance(other, (numbers.Number, np.bool_))): + or isinstance(other, (numbers.Number, np.bool_, np.int64, np.int_)) + or isinstance(other, np.ndarray) and not other.shape): if divmod: return NA, NA else: @@ -310,6 +311,98 @@ def _create_unary_propagating_op(name): return method +def maybe_dispatch_ufunc_to_dunder_op( + object self, object ufunc, str method, *inputs, **kwargs +): + """ + Dispatch a ufunc to the equivalent dunder method. + + Parameters + ---------- + self : ArrayLike + The array whose dunder method we dispatch to + ufunc : Callable + A NumPy ufunc + method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} + inputs : ArrayLike + The input arrays. + kwargs : Any + The additional keyword arguments, e.g. ``out``. + + Returns + ------- + result : Any + The result of applying the ufunc + """ + # special has the ufuncs we dispatch to the dunder op on + special = { + "add", + "sub", + "mul", + "pow", + "mod", + "floordiv", + "truediv", + "divmod", + "eq", + "ne", + "lt", + "gt", + "le", + "ge", + "remainder", + "matmul", + "or", + "xor", + "and", + } + aliases = { + "subtract": "sub", + "multiply": "mul", + "floor_divide": "floordiv", + "true_divide": "truediv", + "power": "pow", + "remainder": "mod", + "divide": "div", + "equal": "eq", + "not_equal": "ne", + "less": "lt", + "less_equal": "le", + "greater": "gt", + "greater_equal": "ge", + "bitwise_or": "or", + "bitwise_and": "and", + "bitwise_xor": "xor", + } + + # For op(., Array) -> Array.__r{op}__ + flipped = { + "lt": "__gt__", + "le": "__ge__", + "gt": "__lt__", + "ge": "__le__", + "eq": "__eq__", + "ne": "__ne__", + } + + op_name = ufunc.__name__ + op_name = aliases.get(op_name, op_name) + + def not_implemented(*args, **kwargs): + return NotImplemented + + if method == "__call__" and op_name in special and kwargs.get("out") is None: + if isinstance(inputs[0], type(self)): + name = "__{}__".format(op_name) + return getattr(self, name, not_implemented)(inputs[1]) + else: + name = flipped.get(op_name, "__r{}__".format(op_name)) + result = getattr(self, name, not_implemented)(inputs[0]) + return result + else: + return NotImplemented + + cdef class C_NAType: pass @@ -434,6 +527,27 @@ class NAType(C_NAType): __rxor__ = __xor__ + # What else to add here? datetime / Timestamp? Period? Interval? + # Note: we only handle 0-d ndarrays. + __array_priority__ = 1000 + _HANDLED_TYPES = (np.ndarray, numbers.Number, str, np.bool_, np.int64) + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + types = self._HANDLED_TYPES + (NAType,) + for x in inputs: + if not isinstance(x, types): + return NotImplemented + + if method != "__call__": + raise ValueError(f"ufunc method '{method}' not supported for NA") + result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) + if result is NotImplemented: + if ufunc.nout == 1: + result = NA + else: + result = (NA,) * ufunc.nout + return result + C_NA = NAType() # C-visible NA = C_NA # Python-visible diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index f3c01efed6d43..eb097831e04d4 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -10,6 +10,7 @@ import numpy as np from pandas._libs import Timedelta, Timestamp, lib +from pandas._libs.missing import maybe_dispatch_ufunc_to_dunder_op # noqa:F401 from pandas.util._decorators import Appender from pandas.core.dtypes.common import is_list_like, is_timedelta64_dtype @@ -30,7 +31,6 @@ ) from pandas.core.ops.array_ops import comp_method_OBJECT_ARRAY # noqa:F401 from pandas.core.ops.common import unpack_zerodim_and_defer -from pandas.core.ops.dispatch import maybe_dispatch_ufunc_to_dunder_op # noqa:F401 from pandas.core.ops.dispatch import should_series_dispatch from pandas.core.ops.docstrings import ( _arith_doc_FRAME, diff --git a/pandas/core/ops/dispatch.py b/pandas/core/ops/dispatch.py index 016a89eb56da3..0c27d09a0b12d 100644 --- a/pandas/core/ops/dispatch.py +++ b/pandas/core/ops/dispatch.py @@ -1,7 +1,7 @@ """ Functions for defining unary operations. """ -from typing import Any, Callable, Union +from typing import Any, Union import numpy as np @@ -17,7 +17,6 @@ ) from pandas.core.dtypes.generic import ABCExtensionArray, ABCSeries -from pandas._typing import ArrayLike from pandas.core.construction import array @@ -146,94 +145,3 @@ def dispatch_to_extension_op( "operation [{name}]".format(name=op.__name__) ) return res_values - - -def maybe_dispatch_ufunc_to_dunder_op( - self: ArrayLike, ufunc: Callable, method: str, *inputs: ArrayLike, **kwargs: Any -): - """ - Dispatch a ufunc to the equivalent dunder method. - - Parameters - ---------- - self : ArrayLike - The array whose dunder method we dispatch to - ufunc : Callable - A NumPy ufunc - method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} - inputs : ArrayLike - The input arrays. - kwargs : Any - The additional keyword arguments, e.g. ``out``. - - Returns - ------- - result : Any - The result of applying the ufunc - """ - # special has the ufuncs we dispatch to the dunder op on - special = { - "add", - "sub", - "mul", - "pow", - "mod", - "floordiv", - "truediv", - "divmod", - "eq", - "ne", - "lt", - "gt", - "le", - "ge", - "remainder", - "matmul", - "or", - "xor", - "and", - } - aliases = { - "subtract": "sub", - "multiply": "mul", - "floor_divide": "floordiv", - "true_divide": "truediv", - "power": "pow", - "remainder": "mod", - "divide": "div", - "equal": "eq", - "not_equal": "ne", - "less": "lt", - "less_equal": "le", - "greater": "gt", - "greater_equal": "ge", - "bitwise_or": "or", - "bitwise_and": "and", - "bitwise_xor": "xor", - } - - # For op(., Array) -> Array.__r{op}__ - flipped = { - "lt": "__gt__", - "le": "__ge__", - "gt": "__lt__", - "ge": "__le__", - "eq": "__eq__", - "ne": "__ne__", - } - - op_name = ufunc.__name__ - op_name = aliases.get(op_name, op_name) - - def not_implemented(*args, **kwargs): - return NotImplemented - - if method == "__call__" and op_name in special and kwargs.get("out") is None: - if isinstance(inputs[0], type(self)): - name = "__{}__".format(op_name) - return getattr(self, name, not_implemented)(inputs[1]) - else: - name = flipped.get(op_name, "__r{}__".format(op_name)) - return getattr(self, name, not_implemented)(inputs[0]) - else: - return NotImplemented diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index 40db617c64717..a65cb6a0cbb01 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -58,12 +58,6 @@ def test_comparison_ops(): assert (NA >= other) is NA assert (NA < other) is NA assert (NA <= other) is NA - - if isinstance(other, (np.int64, np.bool_)): - # for numpy scalars we get a deprecation warning and False as result - # for equality or error for larger/lesser than - continue - assert (other == NA) is NA assert (other != NA) is NA assert (other > NA) is NA @@ -175,3 +169,18 @@ def test_series_isna(): s = pd.Series([1, NA], dtype=object) expected = pd.Series([False, True]) tm.assert_series_equal(s.isna(), expected) + + +def test_ufunc(): + assert np.log(pd.NA) is pd.NA + assert np.add(pd.NA, 1) is pd.NA + result = np.divmod(pd.NA, 1) + assert result[0] is pd.NA and result[1] is pd.NA + + result = np.frexp(pd.NA) + assert result[0] is pd.NA and result[1] is pd.NA + + +def test_ufunc_raises(): + with pytest.raises(ValueError, match="ufunc method 'at'"): + np.log.at(pd.NA, 0) From 46f2327fa36dd110f5adae311dce487a06dc03f6 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Fri, 13 Dec 2019 10:22:03 -0600 Subject: [PATCH 02/18] ndarrays --- pandas/_libs/missing.pyx | 19 +++++++++++++++++++ pandas/tests/scalar/test_na_scalar.py | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index e2503893e705d..39970ee180994 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -289,6 +289,7 @@ cdef inline bint is_null_period(v): def _create_binary_propagating_op(name, divmod=False): def method(self, other): + print("binop", other, type(other)) if (other is C_NA or isinstance(other, str) or isinstance(other, (numbers.Number, np.bool_, np.int64, np.int_)) or isinstance(other, np.ndarray) and not other.shape): @@ -297,6 +298,15 @@ def _create_binary_propagating_op(name, divmod=False): else: return NA + elif isinstance(other, np.ndarray): + out = np.empty(other.shape, dtype=object) + out[:] = NA + + if divmod: + return out, out.copy() + else: + return out + return NotImplemented method.__name__ = name @@ -484,6 +494,8 @@ class NAType(C_NAType): return type(other)(1) else: return NA + elif isinstance(other, np.ndarray): + return np.where(other == 0, other.dtype.type(1), NA) return NotImplemented @@ -495,6 +507,8 @@ class NAType(C_NAType): return other else: return NA + elif isinstance(other, np.ndarray): + return np.where((other == 1) | (other == -1), other, NA) return NotImplemented @@ -534,18 +548,23 @@ class NAType(C_NAType): def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): types = self._HANDLED_TYPES + (NAType,) + print('array_ufunc', 'inputs', inputs) for x in inputs: if not isinstance(x, types): + print('defer', x) return NotImplemented if method != "__call__": raise ValueError(f"ufunc method '{method}' not supported for NA") result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) + print("dispatch result", result) if result is NotImplemented: + # TODO: this is wrong for binary, ternary ufuncs. Should handle shape stuff. if ufunc.nout == 1: result = NA else: result = (NA,) * ufunc.nout + return result diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index a65cb6a0cbb01..4b5515a80a263 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -156,6 +156,28 @@ def test_logical_not(): assert ~NA is NA +@pytest.mark.parametrize( + "shape", + [ + # (), # TODO: this fails, since we return a scalar. What to do? + (3,), + (3, 3), + (1, 2, 3), + ], +) +def test_arithmetic_ndarray(shape, all_arithmetic_functions): + op = all_arithmetic_functions + a = np.zeros(shape) + if op.__name__ == "pow": + a += 5 + result = op(pd.NA, a) + expected = np.full(a.shape, pd.NA, dtype=object) + tm.assert_numpy_array_equal(result, expected) + + +# TODO: test pow special with ndarray + + def test_is_scalar(): assert is_scalar(NA) is True From 075d58a85923f673228a7fed2dfabe9e912a3dd6 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 16 Dec 2019 10:54:11 -0600 Subject: [PATCH 03/18] move --- pandas/_libs/missing.pyx | 93 +---------------------------------- pandas/_libs/ops_dispatch.pyx | 93 +++++++++++++++++++++++++++++++++++ pandas/core/ops/__init__.py | 2 +- 3 files changed, 95 insertions(+), 93 deletions(-) create mode 100644 pandas/_libs/ops_dispatch.pyx diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 39970ee180994..3c7d21974105e 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -14,6 +14,7 @@ from pandas._libs.tslibs.np_datetime cimport ( get_timedelta64_value, get_datetime64_value) from pandas._libs.tslibs.nattype cimport ( checknull_with_nat, c_NaT as NaT, is_null_datetimelike) +from pandas._libs.ops_dispatch import maybe_dispatch_ufunc_to_dunder_op cdef: @@ -321,98 +322,6 @@ def _create_unary_propagating_op(name): return method -def maybe_dispatch_ufunc_to_dunder_op( - object self, object ufunc, str method, *inputs, **kwargs -): - """ - Dispatch a ufunc to the equivalent dunder method. - - Parameters - ---------- - self : ArrayLike - The array whose dunder method we dispatch to - ufunc : Callable - A NumPy ufunc - method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} - inputs : ArrayLike - The input arrays. - kwargs : Any - The additional keyword arguments, e.g. ``out``. - - Returns - ------- - result : Any - The result of applying the ufunc - """ - # special has the ufuncs we dispatch to the dunder op on - special = { - "add", - "sub", - "mul", - "pow", - "mod", - "floordiv", - "truediv", - "divmod", - "eq", - "ne", - "lt", - "gt", - "le", - "ge", - "remainder", - "matmul", - "or", - "xor", - "and", - } - aliases = { - "subtract": "sub", - "multiply": "mul", - "floor_divide": "floordiv", - "true_divide": "truediv", - "power": "pow", - "remainder": "mod", - "divide": "div", - "equal": "eq", - "not_equal": "ne", - "less": "lt", - "less_equal": "le", - "greater": "gt", - "greater_equal": "ge", - "bitwise_or": "or", - "bitwise_and": "and", - "bitwise_xor": "xor", - } - - # For op(., Array) -> Array.__r{op}__ - flipped = { - "lt": "__gt__", - "le": "__ge__", - "gt": "__lt__", - "ge": "__le__", - "eq": "__eq__", - "ne": "__ne__", - } - - op_name = ufunc.__name__ - op_name = aliases.get(op_name, op_name) - - def not_implemented(*args, **kwargs): - return NotImplemented - - if method == "__call__" and op_name in special and kwargs.get("out") is None: - if isinstance(inputs[0], type(self)): - name = "__{}__".format(op_name) - return getattr(self, name, not_implemented)(inputs[1]) - else: - name = flipped.get(op_name, "__r{}__".format(op_name)) - result = getattr(self, name, not_implemented)(inputs[0]) - return result - else: - return NotImplemented - - cdef class C_NAType: pass diff --git a/pandas/_libs/ops_dispatch.pyx b/pandas/_libs/ops_dispatch.pyx new file mode 100644 index 0000000000000..cc10da2498b29 --- /dev/null +++ b/pandas/_libs/ops_dispatch.pyx @@ -0,0 +1,93 @@ + +DISPATCHED_UFUNCS = { + "add", + "sub", + "mul", + "pow", + "mod", + "floordiv", + "truediv", + "divmod", + "eq", + "ne", + "lt", + "gt", + "le", + "ge", + "remainder", + "matmul", + "or", + "xor", + "and", +} +UFUNC_ALIASES = { + "subtract": "sub", + "multiply": "mul", + "floor_divide": "floordiv", + "true_divide": "truediv", + "power": "pow", + "remainder": "mod", + "divide": "div", + "equal": "eq", + "not_equal": "ne", + "less": "lt", + "less_equal": "le", + "greater": "gt", + "greater_equal": "ge", + "bitwise_or": "or", + "bitwise_and": "and", + "bitwise_xor": "xor", +} + +# For op(., Array) -> Array.__r{op}__ +REVERSED_NAMES = { + "lt": "__gt__", + "le": "__ge__", + "gt": "__lt__", + "ge": "__le__", + "eq": "__eq__", + "ne": "__ne__", +} + + +def maybe_dispatch_ufunc_to_dunder_op( + object self, object ufunc, str method, *inputs, **kwargs +): + """ + Dispatch a ufunc to the equivalent dunder method. + + Parameters + ---------- + self : ArrayLike + The array whose dunder method we dispatch to + ufunc : Callable + A NumPy ufunc + method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} + inputs : ArrayLike + The input arrays. + kwargs : Any + The additional keyword arguments, e.g. ``out``. + + Returns + ------- + result : Any + The result of applying the ufunc + """ + # special has the ufuncs we dispatch to the dunder op on + + op_name = ufunc.__name__ + op_name = UFUNC_ALIASES.get(op_name, op_name) + + def not_implemented(*args, **kwargs): + return NotImplemented + + if method == "__call__" and op_name in DISPATCHED_UFUNCS and kwargs.get("out") is None: + if isinstance(inputs[0], type(self)): + name = "__{}__".format(op_name) + return getattr(self, name, not_implemented)(inputs[1]) + else: + name = REVERSED_NAMES.get(op_name, "__r{}__".format(op_name)) + result = getattr(self, name, not_implemented)(inputs[0]) + return result + else: + return NotImplemented diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 91fe6fa908fe9..96cb4393cb315 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -10,7 +10,7 @@ import numpy as np from pandas._libs import Timedelta, Timestamp, lib -from pandas._libs.missing import maybe_dispatch_ufunc_to_dunder_op # noqa:F401 +from pandas._libs.ops_dispatch import maybe_dispatch_ufunc_to_dunder_op # noqa:F401 from pandas.util._decorators import Appender from pandas.core.dtypes.common import is_list_like, is_timedelta64_dtype From 0371cf45110679492b726c6d52fb315be6dd12f0 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 16 Dec 2019 10:56:47 -0600 Subject: [PATCH 04/18] setup, prints --- pandas/_libs/missing.pyx | 4 ---- setup.py | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 3c7d21974105e..d1614805c0a9a 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -290,7 +290,6 @@ cdef inline bint is_null_period(v): def _create_binary_propagating_op(name, divmod=False): def method(self, other): - print("binop", other, type(other)) if (other is C_NA or isinstance(other, str) or isinstance(other, (numbers.Number, np.bool_, np.int64, np.int_)) or isinstance(other, np.ndarray) and not other.shape): @@ -457,16 +456,13 @@ class NAType(C_NAType): def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): types = self._HANDLED_TYPES + (NAType,) - print('array_ufunc', 'inputs', inputs) for x in inputs: if not isinstance(x, types): - print('defer', x) return NotImplemented if method != "__call__": raise ValueError(f"ufunc method '{method}' not supported for NA") result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) - print("dispatch result", result) if result is NotImplemented: # TODO: this is wrong for binary, ternary ufuncs. Should handle shape stuff. if ufunc.nout == 1: diff --git a/setup.py b/setup.py index e42a570200dc3..3694b9526c140 100755 --- a/setup.py +++ b/setup.py @@ -609,6 +609,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"): }, "_libs.reduction": {"pyxfile": "_libs/reduction"}, "_libs.ops": {"pyxfile": "_libs/ops"}, + "_libs.ops_dispatch": {"pyxfile": "_libs/ops_dispatch"}, "_libs.properties": {"pyxfile": "_libs/properties"}, "_libs.reshape": {"pyxfile": "_libs/reshape", "depends": []}, "_libs.sparse": {"pyxfile": "_libs/sparse", "depends": _pxi_dep["sparse"]}, From 878ef707792aff8761de15334b6cba203a2d3aae Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 16 Dec 2019 11:07:39 -0600 Subject: [PATCH 05/18] docs --- doc/source/user_guide/missing_data.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/source/user_guide/missing_data.rst b/doc/source/user_guide/missing_data.rst index 1bfe196cb2f89..178b3557d3831 100644 --- a/doc/source/user_guide/missing_data.rst +++ b/doc/source/user_guide/missing_data.rst @@ -920,3 +920,27 @@ filling missing values beforehand. A similar situation occurs when using Series or DataFrame objects in ``if`` statements, see :ref:`gotchas.truth`. + +NumPy ufuncs +------------ + +:attr:`pandas.NA` implements NumPy's ``__array_ufunc__`` protocol. Most ufuncs +work with ``NA``, and generally return ``NA``: + +.. ipython:: python + + np.log(pd.NA) + np.add(pd.NA, 1) + +.. warning:: + + Currently, ufuncs involving an ndarray an NA will return an + object-dtype filled with NA values. + + .. ipython:: python + + a = np.array([1, 2, 3]) + np.greater(a, pd.NA) + + The return type here may change to return a different array type + in the future. From 97af2e9186027d3be9dc0f4a9d326c87f9c1628e Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 16 Dec 2019 11:20:33 -0600 Subject: [PATCH 06/18] fixup --- pandas/_libs/missing.pyx | 9 ++++++--- pandas/tests/scalar/test_na_scalar.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index d1614805c0a9a..f084508a493b1 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -465,9 +465,12 @@ class NAType(C_NAType): result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) if result is NotImplemented: # TODO: this is wrong for binary, ternary ufuncs. Should handle shape stuff. - if ufunc.nout == 1: - result = NA - else: + index, = [i for i, x in enumerate(inputs) if x is NA] + result = np.broadcast_arrays(*inputs)[index] + if result.ndim == 0: + result = result.item() + if ufunc.nout > 1: + result = (result,) * ufunc.nout result = (NA,) * ufunc.nout return result diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index 4b5515a80a263..3b83cbee09839 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -206,3 +206,13 @@ def test_ufunc(): def test_ufunc_raises(): with pytest.raises(ValueError, match="ufunc method 'at'"): np.log.at(pd.NA, 0) + + +def test_binary_input_not_dunder(): + a = np.array([1, 2, 3]) + expected = np.array([pd.NA, pd.NA, pd.NA], dtype=object) + result = np.logaddexp(a, pd.NA) + tm.assert_numpy_array_equal(result, expected) + + result = np.logaddexp(pd.NA, a) + tm.assert_numpy_array_equal(result, expected) From f175a34e73b7a4d518cf178ef002110b0c748d4c Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 16 Dec 2019 11:23:16 -0600 Subject: [PATCH 07/18] fixups --- pandas/tests/scalar/test_na_scalar.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index 3b83cbee09839..043cfbe7185a8 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -216,3 +216,20 @@ def test_binary_input_not_dunder(): result = np.logaddexp(pd.NA, a) tm.assert_numpy_array_equal(result, expected) + + +def test_divmod_ufunc(): + # binary in, binary out. + a = np.array([1, 2, 3]) + expected = np.array([pd.NA, pd.NA, pd.NA], dtype=object) + + result = np.divmod(a, pd.NA) + assert isinstance(result, tuple) + for arr in result: + tm.assert_numpy_array_equal(arr, expected) + tm.assert_numpy_array_equal(arr, expected) + + result = np.divmod(pd.NA, a) + for arr in result: + tm.assert_numpy_array_equal(arr, expected) + tm.assert_numpy_array_equal(arr, expected) From 0f4e121a229e3ede7d3b0fa4a974f5e2dcc8e30e Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 17 Dec 2019 09:50:29 -0600 Subject: [PATCH 08/18] lint --- pandas/_libs/missing.pyx | 4 +++- pandas/_libs/ops_dispatch.pyx | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index f084508a493b1..886e1f35b01e5 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -462,7 +462,9 @@ class NAType(C_NAType): if method != "__call__": raise ValueError(f"ufunc method '{method}' not supported for NA") - result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) + result = maybe_dispatch_ufunc_to_dunder_op( + self, ufunc, method, *inputs, **kwargs + ) if result is NotImplemented: # TODO: this is wrong for binary, ternary ufuncs. Should handle shape stuff. index, = [i for i, x in enumerate(inputs) if x is NA] diff --git a/pandas/_libs/ops_dispatch.pyx b/pandas/_libs/ops_dispatch.pyx index cc10da2498b29..c5c9deacc4eed 100644 --- a/pandas/_libs/ops_dispatch.pyx +++ b/pandas/_libs/ops_dispatch.pyx @@ -1,5 +1,4 @@ - -DISPATCHED_UFUNCS = { +DISPATCHED_UFUNCS = { "add", "sub", "mul", @@ -81,13 +80,15 @@ def maybe_dispatch_ufunc_to_dunder_op( def not_implemented(*args, **kwargs): return NotImplemented - if method == "__call__" and op_name in DISPATCHED_UFUNCS and kwargs.get("out") is None: + if (method == "__call__" + and op_name in DISPATCHED_UFUNCS + and kwargs.get("out") is None): if isinstance(inputs[0], type(self)): name = "__{}__".format(op_name) return getattr(self, name, not_implemented)(inputs[1]) else: name = REVERSED_NAMES.get(op_name, "__r{}__".format(op_name)) - result = getattr(self, name, not_implemented)(inputs[0]) + result = getattr(self, name, not_implemented)(inputs[0]) return result else: return NotImplemented From 72e2b679449913bd5657880287755b1885bb7436 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Fri, 20 Dec 2019 06:38:08 -0600 Subject: [PATCH 09/18] Fixups --- doc/source/user_guide/missing_data.rst | 2 +- pandas/_libs/missing.pyx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/source/user_guide/missing_data.rst b/doc/source/user_guide/missing_data.rst index 178b3557d3831..43672132371e4 100644 --- a/doc/source/user_guide/missing_data.rst +++ b/doc/source/user_guide/missing_data.rst @@ -934,7 +934,7 @@ work with ``NA``, and generally return ``NA``: .. warning:: - Currently, ufuncs involving an ndarray an NA will return an + Currently, ufuncs involving an ndarray and ``NA`` will return an object-dtype filled with NA values. .. ipython:: python diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 886e1f35b01e5..0b6fc298ce56b 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -291,8 +291,11 @@ def _create_binary_propagating_op(name, divmod=False): def method(self, other): if (other is C_NA or isinstance(other, str) - or isinstance(other, (numbers.Number, np.bool_, np.int64, np.int_)) + or isinstance(other, (numbers.Number, np.bool_)) or isinstance(other, np.ndarray) and not other.shape): + # Need the other.shape clause to handle NumPy scalars, + # since we do a setitem on `out` below, which + # won't work for NumPy scalars. if divmod: return NA, NA else: @@ -449,10 +452,8 @@ class NAType(C_NAType): __rxor__ = __xor__ - # What else to add here? datetime / Timestamp? Period? Interval? - # Note: we only handle 0-d ndarrays. __array_priority__ = 1000 - _HANDLED_TYPES = (np.ndarray, numbers.Number, str, np.bool_, np.int64) + _HANDLED_TYPES = (np.ndarray, numbers.Number, str, np.bool_) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): types = self._HANDLED_TYPES + (NAType,) From 7b1585a247adf0c76606adabfc34185aca20cdbb Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 08:48:19 -0600 Subject: [PATCH 10/18] fix bug --- pandas/_libs/missing.pyx | 4 ++-- pandas/tests/scalar/test_na_scalar.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 09c140cf32ce0..c806c22571ce8 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -473,8 +473,8 @@ class NAType(C_NAType): self, ufunc, method, *inputs, **kwargs ) if result is NotImplemented: - # TODO: this is wrong for binary, ternary ufuncs. Should handle shape stuff. - index, = [i for i, x in enumerate(inputs) if x is NA] + # For a NumPy ufunc that's not a binop, like np.logaddexp + index = [i for i, x in enumerate(inputs) if x is NA][0] result = np.broadcast_arrays(*inputs)[index] if result.ndim == 0: result = result.item() diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index 69d6807448d37..c246d9478b0c8 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -217,6 +217,13 @@ def test_binary_input_not_dunder(): result = np.logaddexp(pd.NA, a) tm.assert_numpy_array_equal(result, expected) + # all NA, multiple inputs + assert np.logaddexp(pd.NA, pd.NA) is pd.NA + + result = np.modf(pd.NA, pd.NA) + assert len(result) == 2 + assert all(x is pd.NA for x in result) + def test_divmod_ufunc(): # binary in, binary out. From b79e07f8f8db6fedd4dbba56b22f50532bc72f8e Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 08:49:25 -0600 Subject: [PATCH 11/18] update --- pandas/_libs/missing.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index c806c22571ce8..8d4d2c55688c4 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -291,7 +291,7 @@ cdef inline bint is_null_period(v): # Implementation of NA singleton -def _create_binary_propagating_op(name, divmod=False): +def _create_binary_propagating_op(name, is_divmod=False): def method(self, other): if (other is C_NA or isinstance(other, str) @@ -300,7 +300,7 @@ def _create_binary_propagating_op(name, divmod=False): # Need the other.shape clause to handle NumPy scalars, # since we do a setitem on `out` below, which # won't work for NumPy scalars. - if divmod: + if is_divmod: return NA, NA else: return NA @@ -309,7 +309,7 @@ def _create_binary_propagating_op(name, divmod=False): out = np.empty(other.shape, dtype=object) out[:] = NA - if divmod: + if is_divmod: return out, out.copy() else: return out @@ -383,8 +383,8 @@ class NAType(C_NAType): __rfloordiv__ = _create_binary_propagating_op("__rfloordiv__") __mod__ = _create_binary_propagating_op("__mod__") __rmod__ = _create_binary_propagating_op("__rmod__") - __divmod__ = _create_binary_propagating_op("__divmod__", divmod=True) - __rdivmod__ = _create_binary_propagating_op("__rdivmod__", divmod=True) + __divmod__ = _create_binary_propagating_op("__divmod__", is_divmod=True) + __rdivmod__ = _create_binary_propagating_op("__rdivmod__", is_divmod=True) # __lshift__ and __rshift__ are not implemented __eq__ = _create_binary_propagating_op("__eq__") From 567c584e0e91c08da0ebbc2ff139497cf029cb27 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 09:02:12 -0600 Subject: [PATCH 12/18] test special --- pandas/tests/scalar/test_na_scalar.py | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/pandas/tests/scalar/test_na_scalar.py b/pandas/tests/scalar/test_na_scalar.py index c246d9478b0c8..ea49835dfaa2d 100644 --- a/pandas/tests/scalar/test_na_scalar.py +++ b/pandas/tests/scalar/test_na_scalar.py @@ -81,9 +81,17 @@ def test_comparison_ops(): np.float_(-0), ], ) -def test_pow_special(value): +@pytest.mark.parametrize("asarray", [True, False]) +def test_pow_special(value, asarray): + if asarray: + value = np.array([value]) result = pd.NA ** value - assert isinstance(result, type(value)) + + if asarray: + result = result[0] + else: + # this assertion isn't possible for ndarray. + assert isinstance(result, type(value)) assert result == 1 @@ -102,12 +110,20 @@ def test_pow_special(value): np.float_(-1), ], ) -def test_rpow_special(value): +@pytest.mark.parametrize("asarray", [True, False]) +def test_rpow_special(value, asarray): + if asarray: + value = np.array([value]) result = value ** pd.NA - assert result == value - if not isinstance(value, (np.float_, np.bool_, np.int_)): + + if asarray: + result = result[0] + elif not isinstance(value, (np.float_, np.bool_, np.int_)): + # this assertion isn't possible with asarray=True assert isinstance(result, type(value)) + assert result == value + def test_unary_ops(): assert +NA is NA @@ -157,13 +173,7 @@ def test_logical_not(): @pytest.mark.parametrize( - "shape", - [ - # (), # TODO: this fails, since we return a scalar. What to do? - (3,), - (3, 3), - (1, 2, 3), - ], + "shape", [(3,), (3, 3), (1, 2, 3)], ) def test_arithmetic_ndarray(shape, all_arithmetic_functions): op = all_arithmetic_functions @@ -175,9 +185,6 @@ def test_arithmetic_ndarray(shape, all_arithmetic_functions): tm.assert_numpy_array_equal(result, expected) -# TODO: test pow special with ndarray - - def test_is_scalar(): assert is_scalar(NA) is True From b27470d905743d6bc4286c8b9292872ea57eb6d3 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 09:03:49 -0600 Subject: [PATCH 13/18] doc --- doc/source/user_guide/missing_data.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/user_guide/missing_data.rst b/doc/source/user_guide/missing_data.rst index 48052a01255f8..abbb6feef6056 100644 --- a/doc/source/user_guide/missing_data.rst +++ b/doc/source/user_guide/missing_data.rst @@ -944,3 +944,5 @@ work with ``NA``, and generally return ``NA``: The return type here may change to return a different array type in the future. + +See :ref:`dsintro.numpy_interop` for more on ufuncs. From c7c91840e9e5d3750c9f129e765b848f485ab9dd Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 10:27:45 -0600 Subject: [PATCH 14/18] move --- doc/source/getting_started/dsintro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/getting_started/dsintro.rst b/doc/source/getting_started/dsintro.rst index a07fcbd8b67c4..856806ca45e48 100644 --- a/doc/source/getting_started/dsintro.rst +++ b/doc/source/getting_started/dsintro.rst @@ -676,11 +676,11 @@ similar to an ndarray: # only show the first 5 rows df[:5].T +.. _dsintro.numpy_interop: + DataFrame interoperability with NumPy functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _dsintro.numpy_interop: - Elementwise NumPy ufuncs (log, exp, sqrt, ...) and various other NumPy functions can be used with no issues on Series and DataFrame, assuming the data within are numeric: From ce209f90a4ee65d395dc8caeb42d53ce094fd09f Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 2 Jan 2020 07:26:53 -0600 Subject: [PATCH 15/18] fixup --- pandas/_libs/missing.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/_libs/missing.pyx b/pandas/_libs/missing.pyx index 8d4d2c55688c4..afaf9115abfd3 100644 --- a/pandas/_libs/missing.pyx +++ b/pandas/_libs/missing.pyx @@ -479,7 +479,6 @@ class NAType(C_NAType): if result.ndim == 0: result = result.item() if ufunc.nout > 1: - result = (result,) * ufunc.nout result = (NA,) * ufunc.nout return result From e4ecadbdabd842dbaf9c7afbb566ac534fabd1ae Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 2 Jan 2020 07:28:14 -0600 Subject: [PATCH 16/18] fixup --- pandas/_libs/ops_dispatch.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/ops_dispatch.pyx b/pandas/_libs/ops_dispatch.pyx index c5c9deacc4eed..f6ecef2038cf3 100644 --- a/pandas/_libs/ops_dispatch.pyx +++ b/pandas/_libs/ops_dispatch.pyx @@ -84,10 +84,10 @@ def maybe_dispatch_ufunc_to_dunder_op( and op_name in DISPATCHED_UFUNCS and kwargs.get("out") is None): if isinstance(inputs[0], type(self)): - name = "__{}__".format(op_name) + name = f"__{op_name}__" return getattr(self, name, not_implemented)(inputs[1]) else: - name = REVERSED_NAMES.get(op_name, "__r{}__".format(op_name)) + name = REVERSED_NAMES.get(op_name, f"__r{op_name}__") result = getattr(self, name, not_implemented)(inputs[0]) return result else: From f68e178cbb36da2f994afbb3664bd70202bfff38 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Sat, 4 Jan 2020 15:39:48 -0600 Subject: [PATCH 17/18] restore --- pandas/core/ops/dispatch.py | 219 ++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 pandas/core/ops/dispatch.py diff --git a/pandas/core/ops/dispatch.py b/pandas/core/ops/dispatch.py new file mode 100644 index 0000000000000..f35279378dc65 --- /dev/null +++ b/pandas/core/ops/dispatch.py @@ -0,0 +1,219 @@ +""" +Functions for defining unary operations. +""" +from typing import Any, Callable, Union + +import numpy as np + +from pandas._typing import ArrayLike + +from pandas.core.dtypes.common import ( + is_datetime64_dtype, + is_extension_array_dtype, + is_integer_dtype, + is_object_dtype, + is_scalar, + is_timedelta64_dtype, +) +from pandas.core.dtypes.generic import ABCExtensionArray, ABCSeries + +from pandas.core.construction import array + + +def should_extension_dispatch(left: ABCSeries, right: Any) -> bool: + """ + Identify cases where Series operation should use dispatch_to_extension_op. + + Parameters + ---------- + left : Series + right : object + + Returns + ------- + bool + """ + if ( + is_extension_array_dtype(left.dtype) + or is_datetime64_dtype(left.dtype) + or is_timedelta64_dtype(left.dtype) + ): + return True + + if not is_scalar(right) and is_extension_array_dtype(right): + # GH#22378 disallow scalar to exclude e.g. "category", "Int64" + return True + + return False + + +def should_series_dispatch(left, right, op): + """ + Identify cases where a DataFrame operation should dispatch to its + Series counterpart. + + Parameters + ---------- + left : DataFrame + right : DataFrame or Series + op : binary operator + + Returns + ------- + override : bool + """ + if left._is_mixed_type or right._is_mixed_type: + return True + + if op.__name__.strip("_") in ["and", "or", "xor", "rand", "ror", "rxor"]: + # TODO: GH references for what this fixes + # Note: this check must come before the check for nonempty columns. + return True + + if right.ndim == 1: + # operating with Series, short-circuit checks that would fail + # with AttributeError. + return False + + if not len(left.columns) or not len(right.columns): + # ensure obj.dtypes[0] exists for each obj + return False + + ldtype = left.dtypes.iloc[0] + rdtype = right.dtypes.iloc[0] + + if (is_timedelta64_dtype(ldtype) and is_integer_dtype(rdtype)) or ( + is_timedelta64_dtype(rdtype) and is_integer_dtype(ldtype) + ): + # numpy integer dtypes as timedelta64 dtypes in this scenario + return True + + if is_datetime64_dtype(ldtype) and is_object_dtype(rdtype): + # in particular case where right is an array of DateOffsets + return True + + return False + + +def dispatch_to_extension_op( + op, left: Union[ABCExtensionArray, np.ndarray], right: Any, +): + """ + Assume that left or right is a Series backed by an ExtensionArray, + apply the operator defined by op. + + Parameters + ---------- + op : binary operator + left : ExtensionArray or np.ndarray + right : object + + Returns + ------- + ExtensionArray or np.ndarray + 2-tuple of these if op is divmod or rdivmod + """ + # NB: left and right should already be unboxed, so neither should be + # a Series or Index. + + if left.dtype.kind in "mM" and isinstance(left, np.ndarray): + # We need to cast datetime64 and timedelta64 ndarrays to + # DatetimeArray/TimedeltaArray. But we avoid wrapping others in + # PandasArray as that behaves poorly with e.g. IntegerArray. + left = array(left) + + # The op calls will raise TypeError if the op is not defined + # on the ExtensionArray + res_values = op(left, right) + return res_values + + +def maybe_dispatch_ufunc_to_dunder_op( + self: ArrayLike, ufunc: Callable, method: str, *inputs: ArrayLike, **kwargs: Any +): + """ + Dispatch a ufunc to the equivalent dunder method. + + Parameters + ---------- + self : ArrayLike + The array whose dunder method we dispatch to + ufunc : Callable + A NumPy ufunc + method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} + inputs : ArrayLike + The input arrays. + kwargs : Any + The additional keyword arguments, e.g. ``out``. + + Returns + ------- + result : Any + The result of applying the ufunc + """ + # special has the ufuncs we dispatch to the dunder op on + special = { + "add", + "sub", + "mul", + "pow", + "mod", + "floordiv", + "truediv", + "divmod", + "eq", + "ne", + "lt", + "gt", + "le", + "ge", + "remainder", + "matmul", + "or", + "xor", + "and", + } + aliases = { + "subtract": "sub", + "multiply": "mul", + "floor_divide": "floordiv", + "true_divide": "truediv", + "power": "pow", + "remainder": "mod", + "divide": "div", + "equal": "eq", + "not_equal": "ne", + "less": "lt", + "less_equal": "le", + "greater": "gt", + "greater_equal": "ge", + "bitwise_or": "or", + "bitwise_and": "and", + "bitwise_xor": "xor", + } + + # For op(., Array) -> Array.__r{op}__ + flipped = { + "lt": "__gt__", + "le": "__ge__", + "gt": "__lt__", + "ge": "__le__", + "eq": "__eq__", + "ne": "__ne__", + } + + op_name = ufunc.__name__ + op_name = aliases.get(op_name, op_name) + + def not_implemented(*args, **kwargs): + return NotImplemented + + if method == "__call__" and op_name in special and kwargs.get("out") is None: + if isinstance(inputs[0], type(self)): + name = f"__{op_name}__" + return getattr(self, name, not_implemented)(inputs[1]) + else: + name = flipped.get(op_name, f"__r{op_name}__") + return getattr(self, name, not_implemented)(inputs[0]) + else: + return NotImplemented From d8c23e945da1c1f3b211fdfc2e0210bfece4131f Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Sat, 4 Jan 2020 15:40:49 -0600 Subject: [PATCH 18/18] fixup --- pandas/core/ops/dispatch.py | 95 +------------------------------------ 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/pandas/core/ops/dispatch.py b/pandas/core/ops/dispatch.py index f35279378dc65..61a3032c7a02c 100644 --- a/pandas/core/ops/dispatch.py +++ b/pandas/core/ops/dispatch.py @@ -1,12 +1,10 @@ """ Functions for defining unary operations. """ -from typing import Any, Callable, Union +from typing import Any, Union import numpy as np -from pandas._typing import ArrayLike - from pandas.core.dtypes.common import ( is_datetime64_dtype, is_extension_array_dtype, @@ -126,94 +124,3 @@ def dispatch_to_extension_op( # on the ExtensionArray res_values = op(left, right) return res_values - - -def maybe_dispatch_ufunc_to_dunder_op( - self: ArrayLike, ufunc: Callable, method: str, *inputs: ArrayLike, **kwargs: Any -): - """ - Dispatch a ufunc to the equivalent dunder method. - - Parameters - ---------- - self : ArrayLike - The array whose dunder method we dispatch to - ufunc : Callable - A NumPy ufunc - method : {'reduce', 'accumulate', 'reduceat', 'outer', 'at', '__call__'} - inputs : ArrayLike - The input arrays. - kwargs : Any - The additional keyword arguments, e.g. ``out``. - - Returns - ------- - result : Any - The result of applying the ufunc - """ - # special has the ufuncs we dispatch to the dunder op on - special = { - "add", - "sub", - "mul", - "pow", - "mod", - "floordiv", - "truediv", - "divmod", - "eq", - "ne", - "lt", - "gt", - "le", - "ge", - "remainder", - "matmul", - "or", - "xor", - "and", - } - aliases = { - "subtract": "sub", - "multiply": "mul", - "floor_divide": "floordiv", - "true_divide": "truediv", - "power": "pow", - "remainder": "mod", - "divide": "div", - "equal": "eq", - "not_equal": "ne", - "less": "lt", - "less_equal": "le", - "greater": "gt", - "greater_equal": "ge", - "bitwise_or": "or", - "bitwise_and": "and", - "bitwise_xor": "xor", - } - - # For op(., Array) -> Array.__r{op}__ - flipped = { - "lt": "__gt__", - "le": "__ge__", - "gt": "__lt__", - "ge": "__le__", - "eq": "__eq__", - "ne": "__ne__", - } - - op_name = ufunc.__name__ - op_name = aliases.get(op_name, op_name) - - def not_implemented(*args, **kwargs): - return NotImplemented - - if method == "__call__" and op_name in special and kwargs.get("out") is None: - if isinstance(inputs[0], type(self)): - name = f"__{op_name}__" - return getattr(self, name, not_implemented)(inputs[1]) - else: - name = flipped.get(op_name, f"__r{op_name}__") - return getattr(self, name, not_implemented)(inputs[0]) - else: - return NotImplemented