Skip to content

Commit d5963dc

Browse files
jakevdpjreback
authored andcommitted
API: update NDFrame __setattr__ to match behavior of __getattr__ (GH8994)
1 parent e51eb9e commit d5963dc

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
lines changed

doc/source/whatsnew/v0.15.2.txt

+29
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,35 @@ API changes
6262

6363
- Allow equality comparisons of Series with a categorical dtype and object dtype; previously these would raise ``TypeError`` (:issue:`8938`)
6464

65+
- Bug in ``NDFrame``: conflicting attribute/column names now behave consistently between getting and setting. Previously, when both a column and attribute named ``y`` existed, ``data.y`` would return the attribute, while ``data.y = z`` would update the column (:issue:`8994`)
66+
67+
.. ipython:: python
68+
69+
data = pd.DataFrame({'x':[1, 2, 3]})
70+
data.y = 2
71+
data['y'] = [2, 4, 6]
72+
data
73+
74+
# this assignment was inconsistent
75+
data.y = 5
76+
77+
Old behavior:
78+
79+
.. code-block:: python
80+
81+
In [6]: data.y
82+
Out[6]: 2
83+
84+
In [7]: data['y'].values
85+
Out[7]: array([5, 5, 5])
86+
87+
New behavior:
88+
89+
.. ipython:: python
90+
91+
data.y
92+
data['y'].values
93+
6594
.. _whatsnew_0152.enhancements:
6695

6796
Enhancements

pandas/core/generic.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -1929,11 +1929,12 @@ def __finalize__(self, other, method=None, **kwargs):
19291929
return self
19301930

19311931
def __getattr__(self, name):
1932-
"""After regular attribute access, try looking up the name of a the
1933-
info.
1934-
1932+
"""After regular attribute access, try looking up the name
19351933
This allows simpler access to columns for interactive use.
19361934
"""
1935+
# Note: obj.x will always call obj.__getattribute__('x') prior to
1936+
# calling obj.__getattr__('x').
1937+
19371938
if name in self._internal_names_set:
19381939
return object.__getattribute__(self, name)
19391940
elif name in self._metadata:
@@ -1945,12 +1946,24 @@ def __getattr__(self, name):
19451946
(type(self).__name__, name))
19461947

19471948
def __setattr__(self, name, value):
1948-
"""After regular attribute access, try looking up the name of the info
1949+
"""After regular attribute access, try setting the name
19491950
This allows simpler access to columns for interactive use."""
1951+
# first try regular attribute access via __getattribute__, so that
1952+
# e.g. ``obj.x`` and ``obj.x = 4`` will always reference/modify
1953+
# the same attribute.
1954+
1955+
try:
1956+
object.__getattribute__(self, name)
1957+
return object.__setattr__(self, name, value)
1958+
except AttributeError:
1959+
pass
1960+
1961+
# if this fails, go on to more involved attribute setting
1962+
# (note that this matches __getattr__, above).
19501963
if name in self._internal_names_set:
19511964
object.__setattr__(self, name, value)
19521965
elif name in self._metadata:
1953-
return object.__setattr__(self, name, value)
1966+
object.__setattr__(self, name, value)
19541967
else:
19551968
try:
19561969
existing = getattr(self, name)

pandas/tests/test_generic.py

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
assert_frame_equal,
2020
assert_panel_equal,
2121
assert_almost_equal,
22+
assert_equal,
2223
ensure_clean)
2324
import pandas.util.testing as tm
2425

@@ -1316,6 +1317,18 @@ def test_tz_convert_and_localize(self):
13161317
df = DataFrame(index=l0)
13171318
df = getattr(df, fn)('US/Pacific', level=1)
13181319

1320+
def test_set_attribute(self):
1321+
# Test for consistent setattr behavior when an attribute and a column
1322+
# have the same name (Issue #8994)
1323+
df = DataFrame({'x':[1, 2, 3]})
1324+
1325+
df.y = 2
1326+
df['y'] = [2, 4, 6]
1327+
df.y = 5
1328+
1329+
assert_equal(df.y, 5)
1330+
assert_series_equal(df['y'], Series([2, 4, 6]))
1331+
13191332

13201333
class TestPanel(tm.TestCase, Generic):
13211334
_typ = Panel

0 commit comments

Comments
 (0)