Repository: dask/dask

Issue:
[FEA] In-place DataFrame.drops() 
### What 
Support of in-place `DataFrame.drops()`

### Motivation
Pandas and cuDF support in-place `DataFrame.drops()`, which is significant faster and less memory intensive than copying the whole DataFrame.

This can double the performance of a distributed merges in some cases: https://github.com/dask/dask/pull/5657

Additionally, a fast `drops()` operation can simplify common workflows: https://github.com/dask/dask/pull/5657#issuecomment-560557840

### Discussion

However as @mrocklin points out, [we cannot support in-place _as is_ because tasks in Dask may run twice or may run out-of-order so we generally cannot modify inputs of a task in-place.](https://github.com/dask/dask/pull/5657#issuecomment-560455949)

As I see it, there is two approaches:
  - Making the drops() operation faster in cuDF, which is hard since the semantic of Pandas requires a full copy but could be done by using some kind of _copy-on-write_ technique. 
  - As suggested by @mrocklin, inject `inplace=True` in drop operations in Dask when it is safe. E.g., it is save when the drop is inside a blockwise operation where it doesn't affect the input of the blockwise operation. @rjzamora is working on this solution.




Discussion:

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -70,6 +70,7 @@
     valid_divisions,
     hash_object_dispatch,
     check_matching_columns,
+    drop_by_shallow_copy,
 )
 
 no_default = "__no_default__"
@@ -3750,12 +3751,7 @@ def drop(self, labels=None, axis=0, columns=None, errors="raise"):
         axis = self._validate_axis(axis)
         if (axis == 1) or (columns is not None):
             return self.map_partitions(
-                M.drop,
-                labels=labels,
-                axis=axis,
-                columns=columns,
-                errors=errors,
-                enforce_metadata=False,
+                drop_by_shallow_copy, columns or labels, errors=errors
             )
         raise NotImplementedError(
             "Drop currently only works for axis=1 or when columns is not None"
diff --git a/dask/dataframe/utils.py b/dask/dataframe/utils.py
--- a/dask/dataframe/utils.py
+++ b/dask/dataframe/utils.py
@@ -952,3 +952,13 @@ def valid_divisions(divisions):
         return False
 
     return True
+
+
+def drop_by_shallow_copy(df, columns, errors="raise"):
+    """ Use shallow copy to drop columns in place
+    """
+    df2 = df.copy(deep=False)
+    if not pd.api.types.is_list_like(columns):
+        columns = [columns]
+    df2.drop(columns=columns, inplace=True, errors=errors)
+    return df2


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -4118,9 +4118,10 @@ def test_pop():
 
 
 def test_simple_map_partitions():
-    df = pd.DataFrame({"x": range(10), "y": range(10)})
+    data = {"col_0": [9, -3, 0, -1, 5], "col_1": [-2, -7, 6, 8, -5]}
+    df = pd.DataFrame(data)
     ddf = dd.from_pandas(df, npartitions=2)
-    ddf = ddf.drop("x", axis=1)
+    ddf = ddf.clip(-4, 6)
     task = ddf.__dask_graph__()[ddf.__dask_keys__()[0]]
     [v] = task[0].dsk.values()
-    assert v[0] == M.drop or v[1] == M.drop
+    assert v[0] == M.clip or v[1] == M.clip