From 4a92688be132d19675536fdd73e230f993b36440 Mon Sep 17 00:00:00 2001 From: David Kamm Date: Tue, 19 Sep 2017 10:18:52 -0400 Subject: [PATCH] BUG: wrap all supported inplace methods to avoid making a copy (#12962) --- doc/source/whatsnew/v0.21.0.txt | 1 + pandas/core/ops.py | 15 ++++++++++++--- pandas/tests/frame/test_operators.py | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index c808babeee5d9..254d4c010f1e2 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -585,6 +585,7 @@ PyPy Other ^^^^^ +- Bug where some inplace operators were not being wrapped and produced a copy when invoked (:issue:`12962`) - Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`) - Several ``NaT`` method docstrings (e.g. :func:`NaT.ctime`) were incorrect (:issue:`17327`) - The documentation has had references to versions < v0.17 removed and cleaned up (:issue:`17442`, :issue:`17442`, :issue:`17404` & :issue:`17504`) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 221f6ff8b92c6..d37acf48ed9c2 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -186,8 +186,10 @@ def add_special_arithmetic_methods(cls, arith_method=None, arith_method : function (optional) factory for special arithmetic methods, with op string: f(op, name, str_rep, default_axis=None, fill_zeros=None, **eval_kwargs) - comp_method : function, optional, + comp_method : function (optional) factory for rich comparison - signature: f(op, name, str_rep) + bool_method : function (optional) + factory for boolean methods - signature: f(op, name, str_rep) use_numexpr : bool, default True whether to accelerate with numexpr, defaults to True force : bool, default False @@ -234,9 +236,16 @@ def f(self, other): __isub__=_wrap_inplace_method(new_methods["__sub__"]), __imul__=_wrap_inplace_method(new_methods["__mul__"]), __itruediv__=_wrap_inplace_method(new_methods["__truediv__"]), - __ipow__=_wrap_inplace_method(new_methods["__pow__"]), )) + __ifloordiv__=_wrap_inplace_method(new_methods["__floordiv__"]), + __imod__=_wrap_inplace_method(new_methods["__mod__"]), + __ipow__=_wrap_inplace_method(new_methods["__pow__"]))) if not compat.PY3: - new_methods["__idiv__"] = new_methods["__div__"] + new_methods["__idiv__"] = _wrap_inplace_method(new_methods["__div__"]) + if bool_method: + new_methods.update( + dict(__iand__=_wrap_inplace_method(new_methods["__and__"]), + __ior__=_wrap_inplace_method(new_methods["__or__"]), + __ixor__=_wrap_inplace_method(new_methods["__xor__"]))) add_methods(cls, new_methods=new_methods, force=force, select=select, exclude=exclude) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 5052bef24e95a..53118ad79713a 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -1161,6 +1161,33 @@ def test_inplace_ops_identity(self): assert_frame_equal(df2, expected) assert df._data is df2._data + @pytest.mark.parametrize('op', ['add', 'and', 'div', 'floordiv', 'mod', + 'mul', 'or', 'pow', 'sub', 'truediv', + 'xor']) + def test_inplace_ops_identity2(self, op): + + if compat.PY3 and op == 'div': + return + + df = DataFrame({'a': [1., 2., 3.], + 'b': [1, 2, 3]}) + + operand = 2 + if op in ('and', 'or', 'xor'): + # cannot use floats for boolean ops + df['a'] = [True, False, True] + + df_copy = df.copy() + iop = '__i{}__'.format(op) + op = '__{}__'.format(op) + + # no id change and value is correct + getattr(df, iop)(operand) + expected = getattr(df_copy, op)(operand) + assert_frame_equal(df, expected) + expected = id(df) + assert id(df) == expected + def test_alignment_non_pandas(self): index = ['A', 'B', 'C'] columns = ['X', 'Y', 'Z']