From d1004ebf08813b269318f75760151841a993b199 Mon Sep 17 00:00:00 2001 From: gfyoung Date: Tue, 20 Jun 2017 21:53:06 -0700 Subject: [PATCH] MAINT/BUG: Default inplace to False in pd.eval Deprecated in 0.18.0. xref gh-11149. Also patches bug where we were improperly handling the inplace=False condition, as we were assuming that target input was non-None when that wasn't necessarily enforced. --- doc/source/whatsnew/v0.21.0.txt | 2 + pandas/core/computation/eval.py | 58 ++++++++++++++++----------- pandas/core/frame.py | 13 +++--- pandas/tests/computation/test_eval.py | 8 ---- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 45c92717b60f09..16b6ab302cfc76 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -76,6 +76,7 @@ Removal of prior version deprecations/changes - ``pd.read_excel()`` has dropped the ``has_index_names`` parameter (:issue:`10967`) - ``Categorical`` has dropped the ``.order()`` and ``.sort()`` methods in favor of ``.sort_values()`` (:issue:`12882`) +- ``pd.eval`` and ``DataFrame.eval`` have changed the default of ``inplace`` from ``None`` to ``False`` (:issue:`11149`) .. _whatsnew_0210.performance: @@ -138,3 +139,4 @@ Categorical Other ^^^^^ +- Bug in ``pd.eval()`` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`) diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py index 22e376306280ac..9d01fec9329bca 100644 --- a/pandas/core/computation/eval.py +++ b/pandas/core/computation/eval.py @@ -3,7 +3,6 @@ """Top level ``eval`` module. """ -import warnings import tokenize from pandas.io.formats.printing import pprint_thing from pandas.core.computation import _NUMEXPR_INSTALLED @@ -148,7 +147,7 @@ def _check_for_locals(expr, stack_level, parser): def eval(expr, parser='pandas', engine=None, truediv=True, local_dict=None, global_dict=None, resolvers=(), level=0, - target=None, inplace=None): + target=None, inplace=False): """Evaluate a Python expression as a string using various backends. The following arithmetic operations are supported: ``+``, ``-``, ``*``, @@ -207,18 +206,24 @@ def eval(expr, parser='pandas', engine=None, truediv=True, scope. Most users will **not** need to change this parameter. target : a target object for assignment, optional, default is None essentially this is a passed in resolver - inplace : bool, default True - If expression mutates, whether to modify object inplace or return - copy with mutation. + inplace : bool, default False + If `target` is provided, and the expression mutates `target`, whether + to modify `target` inplace. Otherwise, return a copy of `target` with + the mutation. - WARNING: inplace=None currently falls back to to True, but - in a future version, will default to False. Use inplace=True - explicitly rather than relying on the default. + If `inplace=True`, but `target` cannot be modified inplace, a + ValueError will be raised. Examples of targets that cannot be + modified inplace are integers and strings. Examples of targets + that can be modified inplace are lists and class instances. Returns ------- ndarray, numeric scalar, DataFrame, Series + Raises + ------ + ValueError : `inplace=True`, but the provided `target` could not be + modified inplace. Notes ----- The ``dtype`` of any objects involved in an arithmetic ``%`` operation are @@ -232,8 +237,13 @@ def eval(expr, parser='pandas', engine=None, truediv=True, pandas.DataFrame.query pandas.DataFrame.eval """ - inplace = validate_bool_kwarg(inplace, 'inplace') - first_expr = True + + inplace = validate_bool_kwarg(inplace, "inplace") + modifiable = hasattr(target, "__setitem__") + + if inplace and not modifiable: + raise ValueError("Cannot modify the provided target inplace") + if isinstance(expr, string_types): _check_expression(expr) exprs = [e.strip() for e in expr.splitlines() if e.strip() != ''] @@ -245,7 +255,10 @@ def eval(expr, parser='pandas', engine=None, truediv=True, raise ValueError("multi-line expressions are only valid in the " "context of data, use DataFrame.eval") + ret = None first_expr = True + target_modified = False + for expr in exprs: expr = _convert_expression(expr) engine = _check_engine(engine) @@ -269,21 +282,21 @@ def eval(expr, parser='pandas', engine=None, truediv=True, if parsed_expr.assigner is None and multi_line: raise ValueError("Multi-line expressions are only valid" " if all expressions contain an assignment") + elif not (parsed_expr.assigner is None + or target is None or modifiable): + raise ValueError("Cannot assign expression output to target") # assign if needed if env.target is not None and parsed_expr.assigner is not None: - if inplace is None: - warnings.warn( - "eval expressions containing an assignment currently" - "default to operating inplace.\nThis will change in " - "a future version of pandas, use inplace=True to " - "avoid this warning.", - FutureWarning, stacklevel=3) - inplace = True + target_modified = True + # Cannot assign to the target if it is not assignable. # if returning a copy, copy only on the first assignment if not inplace and first_expr: - target = env.target.copy() + try: + target = env.target.copy() + except AttributeError: + raise ValueError("Cannot return a copy of the target") else: target = env.target @@ -304,7 +317,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True, ret = None first_expr = False - if not inplace and inplace is not None: - return target - - return ret + # We want to exclude `inplace=None` as being False. + if inplace is False: + return target if target_modified else ret diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 2b2e7be62427b4..80cdebc24c39df 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -2224,7 +2224,7 @@ def query(self, expr, inplace=False, **kwargs): else: return new_data - def eval(self, expr, inplace=None, **kwargs): + def eval(self, expr, inplace=False, **kwargs): """Evaluate an expression in the context of the calling DataFrame instance. @@ -2232,13 +2232,10 @@ def eval(self, expr, inplace=None, **kwargs): ---------- expr : string The expression string to evaluate. - inplace : bool - If the expression contains an assignment, whether to return a new - DataFrame or mutate the existing. - - WARNING: inplace=None currently falls back to to True, but - in a future version, will default to False. Use inplace=True - explicitly rather than relying on the default. + inplace : bool, default False + If the expression contains an assignment, whether to perform the + operation inplace and mutate the existing DataFrame. Otherwise, + a new DataFrame is returned. .. versionadded:: 0.18.0 diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py index 89ab4531877a4e..c214f7c68efe83 100644 --- a/pandas/tests/computation/test_eval.py +++ b/pandas/tests/computation/test_eval.py @@ -1311,14 +1311,6 @@ def assignment_not_inplace(self): expected['c'] = expected['a'] + expected['b'] tm.assert_frame_equal(df, expected) - # Default for inplace will change - with tm.assert_produces_warnings(FutureWarning): - df.eval('c = a + b') - - # but don't warn without assignment - with tm.assert_produces_warnings(None): - df.eval('a + b') - def test_multi_line_expression(self): # GH 11149 df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})