diff --git a/doc/source/user_guide/indexing.rst b/doc/source/user_guide/indexing.rst index 7541cf3c8af3c..7b839d62ddde9 100644 --- a/doc/source/user_guide/indexing.rst +++ b/doc/source/user_guide/indexing.rst @@ -62,6 +62,8 @@ of multi-axis indexing. * A boolean array (any ``NA`` values will be treated as ``False``). * A ``callable`` function with one argument (the calling Series or DataFrame) and that returns valid output for indexing (one of the above). + * A tuple of row (and column) indices whose elements are one of the + above inputs. See more at :ref:`Selection by Label `. @@ -78,6 +80,8 @@ of multi-axis indexing. * A boolean array (any ``NA`` values will be treated as ``False``). * A ``callable`` function with one argument (the calling Series or DataFrame) and that returns valid output for indexing (one of the above). + * A tuple of row (and column) indices whose elements are one of the + above inputs. See more at :ref:`Selection by Position `, :ref:`Advanced Indexing ` and :ref:`Advanced @@ -85,6 +89,12 @@ of multi-axis indexing. * ``.loc``, ``.iloc``, and also ``[]`` indexing can accept a ``callable`` as indexer. See more at :ref:`Selection By Callable `. + .. note:: + + Destructuring tuple keys into row (and column) indexes occurs + *before* callables are applied, so you cannot return a tuple from + a callable to index both rows and columns. + Getting values from an object with multi-axes selection uses the following notation (using ``.loc`` as an example, but the following applies to ``.iloc`` as well). Any of the axes accessors may be the null slice ``:``. Axes left out of @@ -450,6 +460,8 @@ The ``.iloc`` attribute is the primary access method. The following are valid in * A slice object with ints ``1:7``. * A boolean array. * A ``callable``, see :ref:`Selection By Callable `. +* A tuple of row (and column) indexes, whose elements are one of the + above types. .. ipython:: python @@ -553,6 +565,12 @@ Selection by callable ``.loc``, ``.iloc``, and also ``[]`` indexing can accept a ``callable`` as indexer. The ``callable`` must be a function with one argument (the calling Series or DataFrame) that returns valid output for indexing. +.. note:: + + For ``.iloc`` indexing, returning a tuple from the callable is + not supported, since tuple destructuring for row and column indexes + occurs *before* applying callables. + .. ipython:: python df1 = pd.DataFrame(np.random.randn(6, 4), diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index d881bc3cd041c..13756dd5a70e4 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -4,6 +4,8 @@ import sys from typing import ( TYPE_CHECKING, + Any, + TypeVar, cast, final, ) @@ -25,6 +27,7 @@ _chained_assignment_msg, ) from pandas.util._decorators import doc +from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.cast import ( can_hold_element, @@ -90,6 +93,7 @@ Series, ) +T = TypeVar("T") # "null slice" _NS = slice(None, None) _one_ellipsis_message = "indexer may only contain one '...' entry" @@ -153,6 +157,10 @@ def iloc(self) -> _iLocIndexer: """ Purely integer-location based indexing for selection by position. + .. deprecated:: 2.2.0 + + Returning a tuple from a callable is deprecated. + ``.iloc[]`` is primarily integer position based (from ``0`` to ``length-1`` of the axis), but may also be used with a boolean array. @@ -166,7 +174,8 @@ def iloc(self) -> _iLocIndexer: - A ``callable`` function with one argument (the calling Series or DataFrame) and that returns valid output for indexing (one of the above). This is useful in method chains, when you don't have a reference to the - calling object, but would like to base your selection on some value. + calling object, but would like to base your selection on + some value. - A tuple of row and column indexes. The tuple elements consist of one of the above inputs, e.g. ``(0, 1)``. @@ -878,7 +887,8 @@ def __setitem__(self, key, value) -> None: key = tuple(list(x) if is_iterator(x) else x for x in key) key = tuple(com.apply_if_callable(x, self.obj) for x in key) else: - key = com.apply_if_callable(key, self.obj) + maybe_callable = com.apply_if_callable(key, self.obj) + key = self._check_deprecated_callable_usage(key, maybe_callable) indexer = self._get_setitem_indexer(key) self._has_valid_setitem_indexer(key) @@ -1137,6 +1147,17 @@ def _contains_slice(x: object) -> bool: def _convert_to_indexer(self, key, axis: AxisInt): raise AbstractMethodError(self) + def _check_deprecated_callable_usage(self, key: Any, maybe_callable: T) -> T: + # GH53533 + if self.name == "iloc" and callable(key) and isinstance(maybe_callable, tuple): + warnings.warn( + "Returning a tuple from a callable with iloc " + "is deprecated and will be removed in a future version", + FutureWarning, + stacklevel=find_stack_level(), + ) + return maybe_callable + @final def __getitem__(self, key): check_dict_or_set_indexers(key) @@ -1151,6 +1172,7 @@ def __getitem__(self, key): axis = self.axis or 0 maybe_callable = com.apply_if_callable(key, self.obj) + maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable) return self._getitem_axis(maybe_callable, axis=axis) def _is_scalar_access(self, key: tuple): diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 4bad0838f7af8..765671fdd7f25 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -1017,6 +1017,15 @@ def test_single_element_ix_dont_upcast(self, float_frame): result = df.loc[[0], "b"] tm.assert_series_equal(result, expected) + def test_iloc_callable_tuple_return_value(self): + # GH53769 + df = DataFrame(np.arange(40).reshape(10, 4), index=range(0, 20, 2)) + msg = "callable with iloc" + with tm.assert_produces_warning(FutureWarning, match=msg): + df.iloc[lambda _: (0,)] + with tm.assert_produces_warning(FutureWarning, match=msg): + df.iloc[lambda _: (0,)] = 1 + def test_iloc_row(self): df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), index=range(0, 20, 2)