Skip to content

Commit a01644c

Browse files
evanpwjorisvandenbossche
authored andcommitted
BUG: Can't store callables using __setitem__ (#13516)
* BUG: Can't store callables using __setitem__ * Use an internal method instead of a new parameter * Add a docstring, fix pep8 complaints * Move whatsnew entry to API section
1 parent e8e0aae commit a01644c

File tree

6 files changed

+79
-49
lines changed

6 files changed

+79
-49
lines changed

Diff for: doc/source/whatsnew/v0.18.2.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ Other API changes
421421
- ``TimedeltaIndex.astype(int)`` and ``DatetimeIndex.astype(int)`` will now return ``Int64Index`` instead of ``np.array`` (:issue:`13209`)
422422
- ``.filter()`` enforces mutual exclusion of the keyword arguments. (:issue:`12399`)
423423
- ``PeridIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`)
424+
- ``__setitem__`` will no longer apply a callable rhs as a function instead of storing it. Call ``where`` directly to get the previous behavior. (:issue:`13299`)
424425

425426
.. _whatsnew_0182.deprecations:
426427

@@ -482,7 +483,6 @@ Bug Fixes
482483
- Bug in ``pd.read_hdf()`` where attempting to load an HDF file with a single dataset, that had one or more categorical columns, failed unless the key argument was set to the name of the dataset. (:issue:`13231`)
483484
- Bug in ``.rolling()`` that allowed a negative integer window in contruction of the ``Rolling()`` object, but would later fail on aggregation (:issue:`13383`)
484485

485-
486486
- Bug in various index types, which did not propagate the name of passed index (:issue:`12309`)
487487
- Bug in ``DatetimeIndex``, which did not honour the ``copy=True`` (:issue:`13205`)
488488
- Bug in ``DatetimeIndex.is_normalized`` returns incorrectly for normalized date_range in case of local timezones (:issue:`13459`)

Diff for: pandas/core/frame.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2395,7 +2395,7 @@ def _setitem_frame(self, key, value):
23952395

23962396
self._check_inplace_setting(value)
23972397
self._check_setitem_copy()
2398-
self.where(-key, value, inplace=True)
2398+
self._where(-key, value, inplace=True)
23992399

24002400
def _ensure_valid_index(self, value):
24012401
"""

Diff for: pandas/core/generic.py

+56-46
Original file line numberDiff line numberDiff line change
@@ -4424,54 +4424,14 @@ def _align_series(self, other, join='outer', axis=None, level=None,
44244424
right = right.fillna(fill_value, method=method, limit=limit)
44254425
return left.__finalize__(self), right.__finalize__(other)
44264426

4427-
_shared_docs['where'] = ("""
4428-
Return an object of same shape as self and whose corresponding
4429-
entries are from self where cond is %(cond)s and otherwise are from
4430-
other.
4431-
4432-
Parameters
4433-
----------
4434-
cond : boolean %(klass)s, array or callable
4435-
If cond is callable, it is computed on the %(klass)s and
4436-
should return boolean %(klass)s or array.
4437-
The callable must not change input %(klass)s
4438-
(though pandas doesn't check it).
4439-
4440-
.. versionadded:: 0.18.1
4441-
4442-
A callable can be used as cond.
4443-
4444-
other : scalar, %(klass)s, or callable
4445-
If other is callable, it is computed on the %(klass)s and
4446-
should return scalar or %(klass)s.
4447-
The callable must not change input %(klass)s
4448-
(though pandas doesn't check it).
4449-
4450-
.. versionadded:: 0.18.1
4451-
4452-
A callable can be used as other.
4453-
4454-
inplace : boolean, default False
4455-
Whether to perform the operation in place on the data
4456-
axis : alignment axis if needed, default None
4457-
level : alignment level if needed, default None
4458-
try_cast : boolean, default False
4459-
try to cast the result back to the input type (if possible),
4460-
raise_on_error : boolean, default True
4461-
Whether to raise on invalid data types (e.g. trying to where on
4462-
strings)
4463-
4464-
Returns
4465-
-------
4466-
wh : same type as caller
4467-
""")
4468-
4469-
@Appender(_shared_docs['where'] % dict(_shared_doc_kwargs, cond="True"))
4470-
def where(self, cond, other=np.nan, inplace=False, axis=None, level=None,
4471-
try_cast=False, raise_on_error=True):
4427+
def _where(self, cond, other=np.nan, inplace=False, axis=None, level=None,
4428+
try_cast=False, raise_on_error=True):
4429+
"""
4430+
Equivalent to public method `where`, except that `other` is not
4431+
applied as a function even if callable. Used in __setitem__.
4432+
"""
44724433

44734434
cond = com._apply_if_callable(cond, self)
4474-
other = com._apply_if_callable(other, self)
44754435

44764436
if isinstance(cond, NDFrame):
44774437
cond, _ = cond.align(self, join='right', broadcast_axis=1)
@@ -4627,6 +4587,56 @@ def where(self, cond, other=np.nan, inplace=False, axis=None, level=None,
46274587

46284588
return self._constructor(new_data).__finalize__(self)
46294589

4590+
_shared_docs['where'] = ("""
4591+
Return an object of same shape as self and whose corresponding
4592+
entries are from self where cond is %(cond)s and otherwise are from
4593+
other.
4594+
4595+
Parameters
4596+
----------
4597+
cond : boolean %(klass)s, array or callable
4598+
If cond is callable, it is computed on the %(klass)s and
4599+
should return boolean %(klass)s or array.
4600+
The callable must not change input %(klass)s
4601+
(though pandas doesn't check it).
4602+
4603+
.. versionadded:: 0.18.1
4604+
4605+
A callable can be used as cond.
4606+
4607+
other : scalar, %(klass)s, or callable
4608+
If other is callable, it is computed on the %(klass)s and
4609+
should return scalar or %(klass)s.
4610+
The callable must not change input %(klass)s
4611+
(though pandas doesn't check it).
4612+
4613+
.. versionadded:: 0.18.1
4614+
4615+
A callable can be used as other.
4616+
4617+
inplace : boolean, default False
4618+
Whether to perform the operation in place on the data
4619+
axis : alignment axis if needed, default None
4620+
level : alignment level if needed, default None
4621+
try_cast : boolean, default False
4622+
try to cast the result back to the input type (if possible),
4623+
raise_on_error : boolean, default True
4624+
Whether to raise on invalid data types (e.g. trying to where on
4625+
strings)
4626+
4627+
Returns
4628+
-------
4629+
wh : same type as caller
4630+
""")
4631+
4632+
@Appender(_shared_docs['where'] % dict(_shared_doc_kwargs, cond="True"))
4633+
def where(self, cond, other=np.nan, inplace=False, axis=None, level=None,
4634+
try_cast=False, raise_on_error=True):
4635+
4636+
other = com._apply_if_callable(other, self)
4637+
return self._where(cond, other, inplace, axis, level, try_cast,
4638+
raise_on_error)
4639+
46304640
@Appender(_shared_docs['where'] % dict(_shared_doc_kwargs, cond="False"))
46314641
def mask(self, cond, other=np.nan, inplace=False, axis=None, level=None,
46324642
try_cast=False, raise_on_error=True):

Diff for: pandas/core/series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ def setitem(key, value):
738738
if is_bool_indexer(key):
739739
key = check_bool_indexer(self.index, key)
740740
try:
741-
self.where(~key, value, inplace=True)
741+
self._where(~key, value, inplace=True)
742742
return
743743
except InvalidIndexError:
744744
pass

Diff for: pandas/tests/frame/test_indexing.py

+10
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ def test_setitem_callable(self):
207207
exp = pd.DataFrame({'A': [11, 12, 13, 14], 'B': [5, 6, 7, 8]})
208208
tm.assert_frame_equal(df, exp)
209209

210+
def test_setitem_other_callable(self):
211+
# GH 13299
212+
inc = lambda x: x + 1
213+
214+
df = pd.DataFrame([[-1, 1], [1, -1]])
215+
df[df > 0] = inc
216+
217+
expected = pd.DataFrame([[-1, inc], [inc, -1]])
218+
tm.assert_frame_equal(df, expected)
219+
210220
def test_getitem_boolean(self):
211221
# boolean indexing
212222
d = self.tsframe.index[10]

Diff for: pandas/tests/series/test_indexing.py

+10
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,16 @@ def test_setitem_callable(self):
441441
s[lambda x: 'A'] = -1
442442
tm.assert_series_equal(s, pd.Series([-1, 2, 3, 4], index=list('ABCD')))
443443

444+
def test_setitem_other_callable(self):
445+
# GH 13299
446+
inc = lambda x: x + 1
447+
448+
s = pd.Series([1, 2, -1, 4])
449+
s[s < 0] = inc
450+
451+
expected = pd.Series([1, 2, inc, 4])
452+
tm.assert_series_equal(s, expected)
453+
444454
def test_slice(self):
445455
numSlice = self.series[10:20]
446456
numSliceEnd = self.series[-10:]

0 commit comments

Comments
 (0)