Repository: pandas-dev/pandas

Issue:
.iloc[:] and .loc[:] return the original object
Each of these asserts should fail:

``` python
# on pandas 0.18.1
import pandas as pd

df = pd.DataFrame({'x': range(3)})
assert df.iloc[:] is df
assert df.loc[:] is df

series = df.x
assert series.iloc[:] is series
assert series.loc[:] is series
```

Instead, for the sake of predictability, these should always return a new DataFrame/Series, even if they reuse the same data. See [here](https://github.com/pydata/pandas/issues/13842#issuecomment-236685599) for an example of how this resulted in a user facing bug within pandas.

Thanks @mborysow for pointing this out.



Discussion:
Hurrah.  My first remotely important contribution to an open source project. ;)

@mborysow even better is a PR!

contributing guide is [here](http://pandas.pydata.org/pandas-docs/stable/contributing.html)

note that you have selected a pretty non-trivial area :)

@jreback I'll take a stab at it tonight.  =)

@jreback, @shoyer 
Do I care about all these cases?  

All except the last one evaluates True.

``` python
df2.loc[:] is df2
df2.loc[:, :] is df2
df2.loc[pd.IndexSlice[:, :]] is df2
df2.loc[pd.IndexSlice[:, :, :], :] is df2
```

@mborysow  Indexing should always return a new object. So all of those should be False.

@shoyer 
Barring pep8 and other such issues, is this a terrible way to approach the problem?  First if and elif statements in **getitem** are mine.  I can refactor if necessary.  Not sure about overhead yet, they are all pretty cheap comparisons.  Will try to actually put this in a pull request and do all the other stuff requested in the contrib guidelines.  Just wanted to check if what I was doing was hideous. ;)

``` python
class _LocationIndexer(_NDFrameIndexer):
    _exception = Exception
    def __getitem__(self, key):
        # if being called as .loc[:]/.iloc[:] return a shallow copy
        if isinstance(key, slice) and all([key.start is None, key.stop is None, key.step is None]):
            return self.obj.copy(deep=False)
        # if being called with a key that is a tuple containing only any number of slice(None, None, None) objects
        elif (isinstance(key, tuple) and all([isinstance(subkey, slice) for subkey in key]) and
             all([all([subkey.start is None, subkey.stop is None, subkey.step is None]) for subkey in key])):
            return self.obj.copy(deep=False)

        if isinstance(key, tuple):
            key = tuple(com._apply_if_callable(x, self.obj) for x in key)
        else:
            # scalar callable may return tuple
            key = com._apply_if_callable(key, self.obj)

        if type(key) is tuple:
            return self._getitem_tuple(key)
        else:
            return self._getitem_axis(key, axis=0)
```

It gives the correct behavior for the assertion mentioned earlier as well as fixing my side effect issue.  It does cause one existing unit test to fail, but it's possible it's a bad test so I'll take a look (pasted output below).

Had to explicitly test the slice start, stop, and step since a unittest failed trying to compare Timestamp to NoneType when doing key is slice(None, None, None).  This covers the case of a single slice(None, None, None) as well as a tuple of any length of them. 

```
Single failing test:
FAIL: test_sort_values (pandas.tests.series.test_sorting.TestSeriesSorting)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/net/brainy/gobs/repositories/pandas/pandas/tests/series/test_sorting.py", line 82, in test_sort_values
    self.assertRaises(ValueError, f)
  File "/net/brainy/gobs/repositories/pandas/pandas/util/testing.py", line 2326, in assertRaises
    _callable(*args, **kwargs)
  File "/net/brainy/gobs/repositories/pandas/pandas/util/testing.py", line 2396, in __exit__
    raise AssertionError("{0} not raised.".format(name))
AssertionError: ValueError not raised.
```

@mborysow I think [this](https://github.com/pydata/pandas/blob/d4f95fdebb74db5edb09c743b48c4b03e0940591/pandas/core/indexing.py#L1245) is closer to the source of the problem. Take note of the two other locations in that file calling `need_slice` as well.

The right solution here would probably be _either_ removing that special case or replacing the return value with a shallow copy instead.

Going to take a stab at this (PyCon sprints)

Resolving patch:
diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py
--- a/pandas/core/indexing.py
+++ b/pandas/core/indexing.py
@@ -988,6 +988,10 @@ def _getitem_lowerdim(self, tup):
                     if len(new_key) == 1:
                         new_key, = new_key
 
+                # Slices should return views, but calling iloc/loc with a null
+                # slice returns a new object.
+                if is_null_slice(new_key):
+                    return section
                 # This is an elided recursive call to iloc/loc/etc'
                 return getattr(section, self.name)[new_key]
 
@@ -1250,7 +1254,7 @@ def _get_slice_axis(self, slice_obj, axis=0):
         obj = self.obj
 
         if not need_slice(slice_obj):
-            return obj
+            return obj.copy(deep=False)
         indexer = self._convert_slice_indexer(slice_obj, axis)
 
         if isinstance(indexer, slice):
@@ -1349,7 +1353,7 @@ def _get_slice_axis(self, slice_obj, axis=0):
         """ this is pretty simple as we just have to deal with labels """
         obj = self.obj
         if not need_slice(slice_obj):
-            return obj
+            return obj.copy(deep=False)
 
         labels = obj._get_axis(axis)
         indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop,
@@ -1690,7 +1694,7 @@ def _get_slice_axis(self, slice_obj, axis=0):
         obj = self.obj
 
         if not need_slice(slice_obj):
-            return obj
+            return obj.copy(deep=False)
 
         slice_obj = self._convert_slice_indexer(slice_obj, axis)
         if isinstance(slice_obj, slice):


Test patch:
diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py
--- a/pandas/tests/indexing/test_iloc.py
+++ b/pandas/tests/indexing/test_iloc.py
@@ -591,3 +591,21 @@ def test_iloc_empty_list_indexer_is_ok(self):
         tm.assert_frame_equal(df.iloc[[]], df.iloc[:0, :],
                               check_index_type=True,
                               check_column_type=True)
+
+    def test_identity_slice_returns_new_object(self):
+        # GH13873
+        original_df = DataFrame({'a': [1, 2, 3]})
+        sliced_df = original_df.iloc[:]
+        assert sliced_df is not original_df
+
+        # should be a shallow copy
+        original_df['a'] = [4, 4, 4]
+        assert (sliced_df['a'] == 4).all()
+
+        original_series = Series([1, 2, 3, 4, 5, 6])
+        sliced_series = original_series.iloc[:]
+        assert sliced_series is not original_series
+
+        # should also be a shallow copy
+        original_series[:3] = [7, 8, 9]
+        assert all(sliced_series[:3] == [7, 8, 9])
diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py
--- a/pandas/tests/indexing/test_loc.py
+++ b/pandas/tests/indexing/test_loc.py
@@ -630,3 +630,28 @@ def test_loc_empty_list_indexer_is_ok(self):
         tm.assert_frame_equal(df.loc[[]], df.iloc[:0, :],
                               check_index_type=True,
                               check_column_type=True)
+
+    def test_identity_slice_returns_new_object(self):
+        # GH13873
+        original_df = DataFrame({'a': [1, 2, 3]})
+        sliced_df = original_df.loc[:]
+        assert sliced_df is not original_df
+        assert original_df[:] is not original_df
+
+        # should be a shallow copy
+        original_df['a'] = [4, 4, 4]
+        assert (sliced_df['a'] == 4).all()
+
+        # These should not return copies
+        assert original_df is original_df.loc[:, :]
+        df = DataFrame(np.random.randn(10, 4))
+        assert df[0] is df.loc[:, 0]
+
+        # Same tests for Series
+        original_series = Series([1, 2, 3, 4, 5, 6])
+        sliced_series = original_series.loc[:]
+        assert sliced_series is not original_series
+        assert original_series[:] is not original_series
+
+        original_series[:3] = [7, 8, 9]
+        assert all(sliced_series[:3] == [7, 8, 9])