Repository: numpy/numpy

Issue:
np.swapaxes doesn't always return a view
Related somehow to the discussion in #5258. The docs for `np.swapaxes` state that it returns a view if the first argument is an ndarray, but it has a fast path for `axis1 == axis2`, where it actually returns the original array incref'ed:

```
>>> a = np.arange(60).reshape(3, 4, 5).copy()
>>> a.base is None
True
>>> a.swapaxes(0, 1).base is None
False
>>> a.swapaxes(0, 0).base is None
True
>>> a.swapaxes(20, 20).base is None  # that this works is bad!!!
True
>>> a.transpose(range(3)).base is None
False
```

I prefer that it always returned a view, even if it is of the whole array unchanged, as the docs say, and as e.g. `np.transpose` does, even if the array is unchanged, and will be more than happy to submit a PR. If there is any good reason why this should not be the case, then at least the docs should be updated, and the check for `axis1 == axis2` be moved to after they have both been checked for in-boundness.



Discussion:
I agree that the lack of bounds checking is a bug. I'm not sure it matters
whether we return a view or the original array -- I guess it affects things
like .resize? -- but if you want to fix it then I can't see any problem
with that either.

On Tue, Nov 4, 2014 at 5:44 PM, Jaime notifications@github.com wrote:

> Related somehow to the discussion in #5258
> https://github.com/numpy/numpy/pull/5258. The docs for np.swapaxes
> state that it returns a view if the first argument is an ndarray, but it
> has a fast path for axis1 == axis2, where it actually returns the
> original array incref'ed:
> 
> > > > a = np.arange(60).reshape(3, 4, 5).copy()
> > > > a.base is None
> > > > True
> > > > a.swapaxes(0, 1).base is None
> > > > False
> > > > a.swapaxes(0, 0).base is None
> > > > True
> > > > a.swapaxes(20, 20).base is None  # that this works is bad!!!
> > > > True
> > > > a.transpose(range(3)).base is None
> > > > False
> 
> I prefer that it always returned a view, even if it is of the whole array
> unchanged, as the docs say, and as e.g. np.transpose does, even if the
> array is unchanged, and will be more than happy to submit a PR. If there is
> any good reason why this should not be the case, then at least the docs
> should be updated, and the check for axis1 == axis2 be moved to after
> they have both been checked for in-boundness.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/numpy/numpy/issues/5260.

## 

Nathaniel J. Smith
Postdoctoral researcher - Informatics - University of Edinburgh
http://vorpus.org

Sounds reasonable. I guess ideally returning the array or returning a view does not really matter, because `ndarray` is almost immutable (the container part). Resize and `arr.shape = (1, 2, 3)`/`arr.strides = (1, 2)` and even `arr.dtype` are notable exception. Except shape, none of these are probably used, but then view creation should be fast enough...

There is actually the exact same issue with `rollaxis`, see [here](https://github.com/numpy/numpy/blob/master/numpy/core/numeric.py#L1425). I had some old code that tripped on this, but I can't fully recall what the exact issue was.

Always returning a view sounds right to me on first thought.

Is there agreement on always returning a view?

My work schedule has been crazy this past few weeks, but I will make a pull request later tonight. Will also ping the list when it is ready.

Everyone seems lukewarmly in favor :-)


Resolving patch:
diff --git a/numpy/core/fromnumeric.py b/numpy/core/fromnumeric.py
--- a/numpy/core/fromnumeric.py
+++ b/numpy/core/fromnumeric.py
@@ -464,8 +464,10 @@ def swapaxes(a, axis1, axis2):
     Returns
     -------
     a_swapped : ndarray
-        If `a` is an ndarray, then a view of `a` is returned; otherwise
-        a new array is created.
+        For Numpy >= 1.10, if `a` is an ndarray, then a view of `a` is
+        returned; otherwise a new array is created. For earlier Numpy
+        versions a view of `a` is returned only if the order of the
+        axes is changed, otherwise the input array is returned.
 
     Examples
     --------


Test patch:
diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -1887,6 +1887,38 @@ def test_ravel(self):
         assert_equal(a.ravel('A'), [0, 2, 4, 6, 8, 10, 12, 14])
         assert_equal(a.ravel('F'), [0, 8, 4, 12, 2, 10, 6, 14])
 
+    def test_swapaxes(self):
+        a = np.arange(1*2*3*4).reshape(1, 2, 3, 4).copy()
+        idx = np.indices(a.shape)
+        assert_(a.flags['OWNDATA'])
+        b = a.copy()
+        # check exceptions
+        assert_raises(ValueError, a.swapaxes, -5, 0)
+        assert_raises(ValueError, a.swapaxes, 4, 0)
+        assert_raises(ValueError, a.swapaxes, 0, -5)
+        assert_raises(ValueError, a.swapaxes, 0, 4)
+
+        for i in range(-4, 4):
+            for j in range(-4, 4):
+                for k, src in enumerate((a, b)):
+                    c = src.swapaxes(i, j)
+                    # check shape
+                    shape = list(src.shape)
+                    shape[i] = src.shape[j]
+                    shape[j] = src.shape[i]
+                    assert_equal(c.shape, shape, str((i, j, k)))
+                    # check array contents
+                    i0, i1, i2, i3 = [dim-1 for dim in c.shape]
+                    j0, j1, j2, j3 = [dim-1 for dim in src.shape]
+                    assert_equal(src[idx[j0], idx[j1], idx[j2], idx[j3]],
+                                 c[idx[i0], idx[i1], idx[i2], idx[i3]],
+                                 str((i, j, k)))
+                    # check a view is always returned, gh-5260
+                    assert_(not c.flags['OWNDATA'], str((i, j, k)))
+                    # check on non-contiguous input array
+                    if k == 1:
+                        b = c
+
     def test_conjugate(self):
         a = np.array([1-1j, 1+1j, 23+23.0j])
         ac = a.conj()