Repository: OpenMined/PySyft

Issue:
Iterate over wrapper of tensor pointer failed
**Describe the bug**
I would like to iterate over a torch tensor, which is send to a websocketclientworker. But it is non-iterator. 

**To Reproduce**
```
import torch
import syft as sy
import copy
hook = sy.TorchHook(torch)
from torch import nn, optim

bob = sy.VirtualWorker(hook, id="bob")
data = torch.tensor([[0,0],[0,1],[1,0],[1,1.]], requires_grad=True)
bob_data = data.send(bob)

for i in data:
    print(i)
for i in bob_data:
    print(i)
```

**Expected behavior**
It should list the tensors like the following:

> tensor([0., 0.], grad_fn=\<SelectBackward\>)
> tensor([0., 1.], grad_fn=\<SelectBackward\>)
> tensor([1., 0.], grad_fn=\<SelectBackward\>)
> tensor([1., 1.], grad_fn=\<SelectBackward\>)



**Screenshots**
However, it report the following error:

> TypeError: iter() returned non-iterator of type 'Tensor'

**Desktop (please complete the following information):**
 - OS: Windows 10 and Ubuntu 18.04


Discussion:
Hmm, looks like this happens because our tensor types like `PointerTensor` don't implement the iterable interface for Python classes. That might be a missing feature, but if we implemented it, I don't think it would have the behavior you're expecting, since the resulting tensors would still be on the worker `bob` and not local.

As written, it would probably print something like:
```
[PointerTensor | me:12345 -> bob:23456]
[PointerTensor | me:34567 -> bob:45678]
[PointerTensor | me:56789 -> bob:67890]
[PointerTensor | me:78901 -> bob:89012]
```
To get the output you expect above, you'd need to fetch the results from `bob` to your local worker with something like:
```
for i in bob_data:
    print(i.get())
```

@gmuraru, what do you think about this? Is this something we can do with the current implementation of `PointerTensor`? Are there things I'm not thinking of that would make this difficult?
> Hmm, looks like this happens because our tensor types like `PointerTensor` don't implement the iterable interface for Python classes. That might be a missing feature, but if we implemented it, I don't think it would have the behavior you're expecting, since the resulting tensors would still be on the worker `bob` and not local.
> 
> As written, it would probably print something like:
> 
> ```
> [PointerTensor | me:12345 -> bob:23456]
> [PointerTensor | me:34567 -> bob:45678]
> [PointerTensor | me:56789 -> bob:67890]
> [PointerTensor | me:78901 -> bob:89012]
> ```
> 
> To get the output you expect above, you'd need to fetch the results from `bob` to your local worker with something like:
> 
> ```
> for i in bob_data:
>     print(i.get())
> ```
> 
> @gmuraru, what do you think about this? Is this something we can do with the current implementation of `PointerTensor`? Are there things I'm not thinking of that would make this difficult?

Currently, we allow indexing. We can do a ```bob_data[0]``` and it will create a ```PointerTensor``` on our machine that will point to the data on ```bob```'s machine. (like you specified @karlhigley).

What we can do, to implement the for: simply get the ```shape``` of the ```PointerTensor``` and then use the functionality we have for indexing.

To get a fast implementation:
```
shape = tensor_ptr.shape
for i in range(shape[0]):
    print(bob_data[i].get()) #
```

What can be a use-case for doing an iteration on a ```PointerTensor```? (you can replace the behavior with a ```get``` and then iterate over it)
Could we implement the iterable interface on `PointerTensor` to build that behavior in?
Sure. Started doing it :D

Resolving patch:
diff --git a/syft/generic/frameworks/hook/hook_args.py b/syft/generic/frameworks/hook/hook_args.py
--- a/syft/generic/frameworks/hook/hook_args.py
+++ b/syft/generic/frameworks/hook/hook_args.py
@@ -216,7 +216,7 @@ def hook_response(attr, response, wrap_type, wrap_args={}, new_self=None):
     """
 
     # inline methods should just return new_self
-    if "__i" == attr[0:3]:
+    if "__i" == attr[0:3] and attr != "__iter__":
         return new_self
 
     # TODO: Why do we need to cast it in a tuple? this is a (small) time waste
diff --git a/syft/generic/object_storage.py b/syft/generic/object_storage.py
--- a/syft/generic/object_storage.py
+++ b/syft/generic/object_storage.py
@@ -161,3 +161,9 @@ def register_tags(self, obj):
 
         for tag in obj.tags:
             self._tag_to_object_ids[tag].add(obj.id)
+
+    def __len__(self):
+        """
+        Return the number of objects in the store
+        """
+        return len(self._objects)
diff --git a/syft/generic/pointers/pointer_tensor.py b/syft/generic/pointers/pointer_tensor.py
--- a/syft/generic/pointers/pointer_tensor.py
+++ b/syft/generic/pointers/pointer_tensor.py
@@ -407,6 +407,9 @@ def item(self) -> None:
     def __eq__(self, other):
         return self.eq(other)
 
+    def __iter__(self):
+        return (self[idx] for idx in range(self.shape[0]))
+
     @staticmethod
     def simplify(worker: AbstractWorker, ptr: "PointerTensor") -> tuple:
         """


Test patch:
diff --git a/test/generic/pointers/test_pointer_tensor.py b/test/generic/pointers/test_pointer_tensor.py
--- a/test/generic/pointers/test_pointer_tensor.py
+++ b/test/generic/pointers/test_pointer_tensor.py
@@ -585,3 +585,36 @@ def test_inplace_ops_on_remote_long_tensor(workers):
     p.get_()
 
     assert p == torch.LongTensor([4])
+
+
+def test_iterable_pointer(workers):
+    alice = workers["alice"]
+
+    t = torch.Tensor([[1, 2], [4, 5], [7, 8]])
+
+    p = t.send(alice)
+
+    assert len(alice.object_store) == 1
+    for idx, tensor in enumerate(p):
+        assert len(alice.object_store) == 2
+        assert isinstance(tensor, PointerTensor)
+        assert torch.all(tensor.get() == t[idx])
+
+    assert len(alice.object_store) == 1
+
+    l = []
+    for idx, tensor in enumerate(p):
+        l.append(tensor)
+
+    assert len(alice.object_store) == 4
+
+    del l
+    del tensor
+
+    assert len(alice.object_store) == 1
+    for idx, tensor in enumerate(p[:, 1]):
+
+        # Should be 3 because p[:, 1] will create another tensor on alice side
+        assert len(alice.object_store) == 3
+        assert isinstance(tensor, PointerTensor)
+        assert torch.all(tensor.get() == t[:, 1][idx])