Skip to content

ENH: __pandas_priority__ #48347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9832203
ENH: __pandas_priority__
jbrockmendel Sep 1, 2022
6005a32
Merge branch 'main' into enh-pandas_priority
jbrockmendel Jan 7, 2023
0176f0e
doc, test
jbrockmendel Jan 7, 2023
03038d2
Merge branch 'main' into enh-pandas_priority
jbrockmendel Feb 22, 2023
794b7e8
Update doc/source/development/extending.rst
jbrockmendel Feb 22, 2023
33d2d8c
Merge branch 'enh-pandas_priority' of github.com:jbrockmendel/pandas …
jbrockmendel Feb 22, 2023
a0ace58
Whatsnew
jbrockmendel Feb 22, 2023
8a3bdae
GH ref
jbrockmendel Feb 22, 2023
8de5162
Merge branch 'main' into enh-pandas_priority
jbrockmendel Feb 24, 2023
c86e10b
suggested doc edit
jbrockmendel Feb 24, 2023
05894a5
Update doc/source/development/extending.rst
jbrockmendel Feb 25, 2023
8448791
Update doc/source/development/extending.rst
jbrockmendel Feb 25, 2023
2d3d883
Update doc/source/development/extending.rst
jbrockmendel Feb 25, 2023
2605097
Merge branch 'main' into enh-pandas_priority
jbrockmendel Feb 27, 2023
008e046
lint fixup
jbrockmendel Feb 27, 2023
a7b28e5
Merge branch 'enh-pandas_priority' of github.com:jbrockmendel/pandas …
jbrockmendel Feb 27, 2023
bde9b8c
Merge branch 'main' into enh-pandas_priority
jbrockmendel Mar 3, 2023
1e2e50a
Merge branch 'main' into enh-pandas_priority
jbrockmendel Mar 6, 2023
be8458d
suggested edits
jbrockmendel Mar 6, 2023
78581f1
Merge branch 'main' into enh-pandas_priority
jbrockmendel Mar 8, 2023
a81121c
Merge branch 'main' into enh-pandas_priority
jbrockmendel Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions doc/source/development/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,47 @@ registers the default "matplotlib" backend as follows.

More information on how to implement a third-party plotting backend can be found at
https://github.com/pandas-dev/pandas/blob/main/pandas/plotting/__init__.py#L1.

.. _extending.pandas_priority:

Arithmetic with 3rd party types
-------------------------------

In order to control how arithmetic works between a custom type and a pandas type,
implement ``__pandas_priority__``. Similar to numpy's ``__array_priority__``
semantics, arithmetic methods on :class:`DataFrame`, :class:`Series`, and :class:`Index`
objects will delegate to ``other``, if it has an attribute ``__array_priority__`` with a higher value.

By default, pandas objects try to operate with other objects, even if they are not types known to pandas:

.. code-block:: python

>>> pandas.Series([1, 2]) + [10, 20]
0 11
1 22
dtype: int64

In the example above, if `[10, 20]` was a custom type that can be understood as a list, pandas objects will still operate with it in the same way.

In some cases, it is useful to delegate to the other type the operation. For example, consider I implement a
custom list object, and I want the result of adding my custom list with a pandas `Series` to be an instance of my list
and not a `Series` as seen in the previous example. This is now possible by defining the `__pandas_priority__` attribute
of my custom list, and setting it to a higher value, than the priority of the pandas objects I want to operate with.

.. code-block:: python

class CustomList(list):
__pandas_priority__ = 5000

def __radd__(self, other):
# return `self` and not the addition for simplicity
return self

custom = CustomList()
series = pd.Series([1, 2, 3])

# Series refuses to add custom, since it's an unknown type with higher priority
assert series.__add__(custom) is NotImplemented

# This will cause the custom class `__radd__` being used instead
assert series + custom is custom
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ enhancement2

Other enhancements
^^^^^^^^^^^^^^^^^^
-
- Implemented ``__pandas_priority__`` to allow custom types to take precedence over :class:`DataFrame`, :class:`Series`, :class:`Index`, or :class:`ExtensionArray` for arithmetic operations, :ref:`see the developer guide <extending.pandas_priority>` (:issue:`48347`)
-

.. ---------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions pandas/core/arrays/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ class ExtensionArray:
# Don't override this.
_typ = "extension"

# similar to __array_priority__, positions ExtensionArray after Index,
# Series, and DataFrame. EA subclasses may override to choose which EA
# subclass takes priority. If overriding, the value should always be
# strictly less than 2000 to be below Index.__pandas_priority__.
__pandas_priority__ = 1000

# ------------------------------------------------------------------------
# Constructors
# ------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ class DataFrame(NDFrame, OpsMixin):
_hidden_attrs: frozenset[str] = NDFrame._hidden_attrs | frozenset([])
_mgr: BlockManager | ArrayManager

# similar to __array_priority__, positions DataFrame before Series, Index,
# and ExtensionArray. Should NOT be overridden by subclasses.
__pandas_priority__ = 4000

@property
def _constructor(self) -> Callable[..., DataFrame]:
return DataFrame
Expand Down
4 changes: 4 additions & 0 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ class Index(IndexOpsMixin, PandasObject):
# To hand over control to subclasses
_join_precedence = 1

# similar to __array_priority__, positions Index after Series and DataFrame
# but before ExtensionArray. Should NOT be overridden by subclasses.
__pandas_priority__ = 2000

# Cython methods; see github.com/cython/cython/issues/2647
# for why we need to wrap these instead of making them class attributes
# Moreover, cython will choose the appropriate-dtyped sub-function
Expand Down
9 changes: 4 additions & 5 deletions pandas/core/ops/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from pandas._typing import F

from pandas.core.dtypes.generic import (
ABCDataFrame,
ABCIndex,
ABCSeries,
)
Expand Down Expand Up @@ -70,10 +69,10 @@ def new_method(self, other):
# For comparison ops, Index does *not* defer to Series
pass
else:
for cls in [ABCDataFrame, ABCSeries, ABCIndex]:
if isinstance(self, cls):
break
if isinstance(other, cls):
prio = getattr(other, "__pandas_priority__", None)
if prio is not None:
if prio > self.__pandas_priority__:
# e.g. other is DataFrame while self is Index/Series/EA
return NotImplemented

other = item_from_zerodim(other)
Expand Down
4 changes: 4 additions & 0 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ class Series(base.IndexOpsMixin, NDFrame): # type: ignore[misc]
base.IndexOpsMixin._hidden_attrs | NDFrame._hidden_attrs | frozenset([])
)

# similar to __array_priority__, positions Series after DataFrame
# but before Index and ExtensionArray. Should NOT be overridden by subclasses.
__pandas_priority__ = 3000

# Override cache_readonly bc Series is mutable
# error: Incompatible types in assignment (expression has type "property",
# base class "IndexOpsMixin" defined the type as "Callable[[IndexOpsMixin], bool]")
Expand Down
16 changes: 16 additions & 0 deletions pandas/tests/test_downstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,19 @@ def test_frame_setitem_dask_array_into_new_col():
tm.assert_frame_equal(result, expected)
finally:
pd.set_option("compute.use_numexpr", olduse)


def test_pandas_priority():
# GH#48347

class MyClass:
__pandas_priority__ = 5000

def __radd__(self, other):
return self

left = MyClass()
right = Series(range(3))

assert right.__add__(left) is NotImplemented
assert right + left is left