Repository: mars-project/mars

Issue:
[BUG] Tensor from a slice is changed by +=, but original data is not effected
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**

A tensor which got from a slice is changed by `+=`, but the original data is not effected.

**To Reproduce**
To help us reproducing this bug, please provide information below:
1. Your Python version
2. The version of Mars you use
3. Versions of crucial packages, such as numpy, scipy and protobuf
4. Full stack of the error.
5. Minimized code to reproduce the error.

```
In [1]: import mars.tensor as mt                                                

In [2]: a = mt.zeros((10, 2))                                                   

In [3]: b = a[:5]                                                               

In [4]: b += 1                                                                  

In [5]: b.execute()                                                             
Out[5]: 
array([[1., 1.],
       [1., 1.],
       [1., 1.],
       [1., 1.],
       [1., 1.]])

In [6]: a.execute()    # wrong                                                         
Out[6]: 
array([[0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.],
       [0., 0.]])
```

**Expected behavior**

The original data should be effected. I guess the root cause is that we did not implement magic like `__iadd__` which should define `out` argument to let the `iadd` change the data, and then the original data would be effected.



Discussion:

Resolving patch:
diff --git a/mars/tensor/arithmetic/__init__.py b/mars/tensor/arithmetic/__init__.py
--- a/mars/tensor/arithmetic/__init__.py
+++ b/mars/tensor/arithmetic/__init__.py
@@ -114,6 +114,13 @@
 from .setimag import TensorSetImag
 
 
+def _wrap_iop(func):
+    def inner(self, *args, **kwargs):
+        kwargs['out'] = self
+        return func(self, *args, **kwargs)
+    return inner
+
+
 def _install():
     from ..core import TENSOR_TYPE, Tensor, TensorData
     from .add import add, radd
@@ -132,24 +139,34 @@ def _install():
 
     for cls in TENSOR_TYPE:
         setattr(cls, '__add__', add)
+        setattr(cls, '__iadd__', _wrap_iop(add))
         setattr(cls, '__radd__', radd)
         setattr(cls, '__sub__', subtract)
+        setattr(cls, '__isub__', _wrap_iop(subtract))
         setattr(cls, '__rsub__', rsubtract)
         setattr(cls, '__mul__', multiply)
+        setattr(cls, '__imul__', _wrap_iop(multiply))
         setattr(cls, '__rmul__', rmultiply)
         setattr(cls, '__div__', divide)
+        setattr(cls, '__idiv__', _wrap_iop(divide))
         setattr(cls, '__rdiv__', rdivide)
         setattr(cls, '__truediv__', truediv)
+        setattr(cls, '__itruediv__', _wrap_iop(truediv))
         setattr(cls, '__rtruediv__',rtruediv)
         setattr(cls, '__floordiv__', floordiv)
+        setattr(cls, '__ifloordiv__', _wrap_iop(floordiv))
         setattr(cls, '__rfloordiv__', rfloordiv)
         setattr(cls, '__pow__', power)
+        setattr(cls, '__ipow__', _wrap_iop(power))
         setattr(cls, '__rpow__', rpower)
         setattr(cls, '__mod__', mod)
+        setattr(cls, '__imod__', _wrap_iop(mod))
         setattr(cls, '__rmod__', rmod)
         setattr(cls, '__lshift__', lshift)
+        setattr(cls, '__ilshift__', _wrap_iop(lshift))
         setattr(cls, '__rlshift__', rlshift)
         setattr(cls, '__rshift__', rshift)
+        setattr(cls, '__irshift__', _wrap_iop(rshift))
         setattr(cls, '__rrshift__', rrshift)
 
         setattr(cls, '__eq__', equal)
@@ -159,10 +176,13 @@ def _install():
         setattr(cls, '__gt__', greater)
         setattr(cls, '__ge__', greater_equal)
         setattr(cls, '__and__', bitand)
+        setattr(cls, '__iand__', _wrap_iop(bitand))
         setattr(cls, '__rand__', rbitand)
         setattr(cls, '__or__', bitor)
+        setattr(cls, '__ior__', _wrap_iop(bitor))
         setattr(cls, '__ror__', rbitor)
         setattr(cls, '__xor__', bitxor)
+        setattr(cls, '__ixor__', _wrap_iop(bitxor))
         setattr(cls, '__rxor__', rbitxor)
 
         setattr(cls, '__neg__', negative)
diff --git a/mars/tensor/arithmetic/core.py b/mars/tensor/arithmetic/core.py
--- a/mars/tensor/arithmetic/core.py
+++ b/mars/tensor/arithmetic/core.py
@@ -93,6 +93,8 @@ def check_inputs(self, inputs):
     @classmethod
     def _execute_gpu(cls, op, xp, lhs, rhs, **kw):
         func_name = getattr(cls, '_func_name')
+        if kw.get('out') is not None:
+            kw['out'] = xp.asarray(kw['out'])
         r = getattr(xp, func_name)(lhs, rhs, **kw)
         return convert_order(r, op.outputs[0].order.value)
 
@@ -100,6 +102,8 @@ def _execute_gpu(cls, op, xp, lhs, rhs, **kw):
     def _execute_cpu(cls, op, xp, lhs, rhs, **kw):
         kw['order'] = op.order
         func_name = getattr(cls, '_func_name')
+        if kw.get('out') is not None:
+            kw['out'] = np.asarray(kw['out'])
         return getattr(xp, func_name)(lhs, rhs, **kw)
 
     @classmethod
@@ -108,14 +112,14 @@ def execute(cls, ctx, op):
             [ctx[c.key] for c in op.inputs], device=op.device, ret_extra=True)
 
         with device(device_id):
-            kw = {'casting': op.casting} if op.out else {}
+            kw = {'casting': op.casting} if op.out is not None else {}
 
             inputs_iter = iter(inputs)
             lhs = op.lhs if np.isscalar(op.lhs) else next(inputs_iter)
             rhs = op.rhs if np.isscalar(op.rhs) else next(inputs_iter)
-            if op.out:
+            if op.out is not None:
                 kw['out'] = next(inputs_iter).copy()
-            if op.where:
+            if op.where is not None:
                 kw['where'] = next(inputs_iter)
 
             with np.errstate(**op.err):


Test patch:
diff --git a/mars/tensor/tests/test_core_execute.py b/mars/tensor/tests/test_core_execute.py
--- a/mars/tensor/tests/test_core_execute.py
+++ b/mars/tensor/tests/test_core_execute.py
@@ -67,6 +67,18 @@ def testViewDataOnSlice(self):
         np.testing.assert_array_equal(b.execute(), npb)
         np.testing.assert_array_equal(a.execute(), npa)
 
+        data = np.random.rand(10, 20)
+        a = tensor(data, chunk_size=6)
+        b = a[:7]
+        b += 1
+
+        npa = data.copy()
+        npb = npa[:7]
+        npb += 1
+
+        np.testing.assert_array_equal(b.execute(), npb)
+        np.testing.assert_array_equal(a.execute(), npa)
+
     def testSetItemOnView(self):
         a = ones((5, 8), dtype=int)
         b = a[:3]