Repository: pydata/xarray

Issue:
Unnecessary copy when indexing to obtain a 0d array
#### Code Sample
```python
>>> import numpy as np
>>> import xarray as xr
>>> da = xr.DataArray(np.arange(3))
>>> da
<xarray.DataArray (dim_0: 3)>
array([0, 1, 2])
Dimensions without coordinates: dim_0
>>> da[0].values.fill(99)
>>> da
<xarray.DataArray (dim_0: 3)>
array([0, 1, 2])
Dimensions without coordinates: dim_0
```
#### Problem description
Indexing into xarray objects creates a view of the underlying data if possible. A surprising exception is when all dimensions are indexed out and the resulting object is 0d. Xarray insists on returning a 0d array rather than a scalar, which suggests (at least to me) that this is also a view whenever possible; however, it is always a copy, and modifying it will never affect the original array.

(The example above is a little contrived, since one could always call `da[0] = 99`. In my actual use case I am indexing into a Dataset in a way that creates views for all variables except the one that happens to collapse to 0d, and thus I'm unable to use the indexed Dataset to modify that variable in the original Dataset.) 

The copy happens because, internally, the 0d array is created by retrieving a scalar from the underlying numpy array and then wrapping a new array around it. However, in numpy a 0d view can be created directly by indexing with `Ellipsis`/`...`, as follows:
```python
>>> import numpy as np
>>> arr = np.arange(3)
>>> arr[0, ...]
array(0)
```
Thus, a fix that solves my immediate issues and passes all current tests is to modify the following method:
https://github.com/pydata/xarray/blob/778ffc49135d6f97e17b37b48304995fca72f1e0/xarray/core/indexing.py#L1154-L1163
to always append an ellipsis for basic and outer indexing:
```python
    def _indexing_array_and_key(self, key):
        if isinstance(key, OuterIndexer):
            array = self.array
>           key = _outer_to_numpy_indexer(key, self.array.shape) + (Ellipsis,)
        elif isinstance(key, VectorizedIndexer):
            array = nputils.NumpyVIndexAdapter(self.array)
            key = key.tuple
        elif isinstance(key, BasicIndexer):
            array = self.array
>           key = key.tuple + (Ellipsis,)
```
I'm not familiar enough with all the indexing variants in xarray to know if this covers all cases of 0d arrays that are currently copies but could be views. If someone wants to share some insight (e.g., some more advanced test cases), I could try and put together a pull request.

#### Expected Output
```python
>>> da[0].values.fill(99)
>>> da
<xarray.DataArray (dim_0: 3)>
array([99, 1, 2])
Dimensions without coordinates: dim_0
```
#### Output of ``xr.show_versions()``

<details>
/home/daniel/local/anaconda3/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.5.final.0
python-bits: 64
OS: Linux
OS-release: 4.15.0-42-lowlatency
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

xarray: 0.11.0
pandas: 0.23.0
numpy: 1.14.3
scipy: 1.1.0
netCDF4: 1.4.0
h5netcdf: 0.6.2
h5py: 2.7.1
Nio: None
zarr: None
cftime: 1.0.0b1
PseudonetCDF: None
rasterio: None
iris: None
bottleneck: 1.2.1
cyordereddict: None
dask: 0.17.5
distributed: 1.21.8
matplotlib: 2.2.2
cartopy: None
seaborn: 0.8.1
setuptools: 39.1.0
pip: 10.0.1
conda: 4.5.12
pytest: 3.5.1
IPython: 6.4.0
sphinx: 1.7.4
</details>


Discussion:
pep8speaks: Hello @danielwe! Thanks for submitting the PR.

 - In the file [`xarray/core/indexing.py`](https://github.com/danielwe/xarray/blob/21bb69b6f396115a8baf11aac3b631ac36af2d43/xarray/core/indexing.py), following                are the PEP8 issues :
> [Line 1163:80](https://github.com/danielwe/xarray/blob/21bb69b6f396115a8baf11aac3b631ac36af2d43/xarray/core/indexing.py#L1163): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (80 > 79 characters)


 - There are no PEP8 issues in the file [`xarray/tests/test_variable.py`](https://github.com/danielwe/xarray/blob/21bb69b6f396115a8baf11aac3b631ac36af2d43/xarray/tests/test_variable.py) !



Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 26894e2a..34654a85 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -56,6 +56,9 @@ Enhancements
 - :py:meth:`DataArray.resample` and :py:meth:`Dataset.resample` now supports the
   ``loffset`` kwarg just like Pandas.
   By `Deepak Cherian <https://github.com/dcherian>`_
+- 0d slices of ndarrays are now obtained directly through indexing, rather than
+  extracting and wrapping a scalar, avoiding unnecessary copying. By `Daniel
+  Wennberg <https://github.com/danielwe>`_.
 
 Bug fixes
 ~~~~~~~~~
diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py
index d51da471..02f2644d 100644
--- a/xarray/core/indexing.py
+++ b/xarray/core/indexing.py
@@ -1142,15 +1142,6 @@ class NumpyIndexingAdapter(ExplicitlyIndexedNDArrayMixin):
                             'Trying to wrap {}'.format(type(array)))
         self.array = array
 
-    def _ensure_ndarray(self, value):
-        # We always want the result of indexing to be a NumPy array. If it's
-        # not, then it really should be a 0d array. Doing the coercion here
-        # instead of inside variable.as_compatible_data makes it less error
-        # prone.
-        if not isinstance(value, np.ndarray):
-            value = utils.to_0d_array(value)
-        return value
-
     def _indexing_array_and_key(self, key):
         if isinstance(key, OuterIndexer):
             array = self.array
@@ -1160,7 +1151,10 @@ class NumpyIndexingAdapter(ExplicitlyIndexedNDArrayMixin):
             key = key.tuple
         elif isinstance(key, BasicIndexer):
             array = self.array
-            key = key.tuple
+            # We want 0d slices rather than scalars. This is achieved by
+            # appending an ellipsis (see
+            # https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html#detailed-notes).  # noqa
+            key = key.tuple + (Ellipsis,)
         else:
             raise TypeError('unexpected key type: {}'.format(type(key)))
 
@@ -1171,7 +1165,7 @@ class NumpyIndexingAdapter(ExplicitlyIndexedNDArrayMixin):
 
     def __getitem__(self, key):
         array, key = self._indexing_array_and_key(key)
-        return self._ensure_ndarray(array[key])
+        return array[key]
 
     def __setitem__(self, key, value):
         array, key = self._indexing_array_and_key(key)


Test patch:
diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py
index 84813f6c..d98783fe 100644
--- a/xarray/tests/test_variable.py
+++ b/xarray/tests/test_variable.py
@@ -1147,6 +1147,11 @@ class TestVariable(VariableSubclassobjects):
         assert v_new.dims == ('x', )
         assert_array_equal(v_new, v._data[:, 1])
 
+        # test that we obtain a modifiable view when taking a 0d slice
+        v_new = v[0, 0]
+        v_new[...] += 99
+        assert_array_equal(v_new, v._data[0, 0])
+
     def test_getitem_with_mask_2d_input(self):
         v = Variable(('x', 'y'), [[0, 1, 2], [3, 4, 5]])
         assert_identical(v._getitem_with_mask(([-1, 0], [1, -1])),