Repository: ipython/ipython

Issue:
Multiprocessing semantics in parallel.view.map
I have a script that is executed using either
- the builtin map
- using multiprocessing map
- IPython view.map.

This is very convenient because I can easily debug and test scripts, as well as run the same script unchanged in different installations. 

However, the semantics of map is different. While in multiprocessing a generator is accepted, in IPython is not (fails when it tries to take the length of the generator). Insider multiprocessing, the following code converts a generator to a list:

```
if not hasattr(iterable, '__len__'):
    iterable = list(iterable)
```

I do not really like this behavior (`Explicit is better than implicit`), but having the same semantics is convenient. 

So, my questions are:
1.- Is it possible to have a `map` that does not require to know in advance the number of tasks?
2.- If not, what do you think about using the same semantics that `multiprocessing`?



Discussion:
Matching multiprocessing should be trivial, failing to cast to a list might be tricky, but I will have a look.


Resolving patch:
diff --git a/IPython/parallel/client/client.py b/IPython/parallel/client/client.py
--- a/IPython/parallel/client/client.py
+++ b/IPython/parallel/client/client.py
@@ -853,6 +853,7 @@ def _flush_iopub(self, sock):
             # ignore IOPub messages with no parent.
             # Caused by print statements or warnings from before the first execution.
             if not parent:
+                idents,msg = self.session.recv(sock, mode=zmq.NOBLOCK)
                 continue
             msg_id = parent['msg_id']
             content = msg['content']
diff --git a/IPython/parallel/client/map.py b/IPython/parallel/client/map.py
--- a/IPython/parallel/client/map.py
+++ b/IPython/parallel/client/map.py
@@ -59,17 +59,19 @@
 class Map(object):
     """A class for partitioning a sequence using a map."""
     
-    def getPartition(self, seq, p, q):
-        """Returns the pth partition of q partitions of seq."""
+    def getPartition(self, seq, p, q, n=None):
+        """Returns the pth partition of q partitions of seq.
         
+        The length can be specified as `n`,
+        otherwise it is the value of `len(seq)`
+        """
+        n = len(seq) if n is None else n
         # Test for error conditions here
         if p<0 or p>=q:
-          print "No partition exists."
-          return
+          raise ValueError("must have 0 <= p <= q, but have p=%s,q=%s" % (p, q))
         
-        N = len(seq)
-        remainder = N % q
-        basesize = N // q
+        remainder = n % q
+        basesize = n // q
         
         if p < remainder:
             low = p * (basesize + 1)
@@ -104,19 +106,14 @@ def concatenate(self, listOfPartitions):
         return listOfPartitions
 
 class RoundRobinMap(Map):
-    """Partitions a sequence in a roun robin fashion.
+    """Partitions a sequence in a round robin fashion.
     
     This currently does not work!
     """
 
-    def getPartition(self, seq, p, q):
-        # if not isinstance(seq,(list,tuple)):
-        #     raise NotImplementedError("cannot RR partition type %s"%type(seq))
-        return seq[p:len(seq):q]
-        #result = []
-        #for i in range(p,len(seq),q):
-        #    result.append(seq[i])
-        #return result
+    def getPartition(self, seq, p, q, n=None):
+        n = len(seq) if n is None else n
+        return seq[p:n:q]
 
     def joinPartitions(self, listOfPartitions):
         testObject = listOfPartitions[0]
diff --git a/IPython/parallel/client/remotefunction.py b/IPython/parallel/client/remotefunction.py
--- a/IPython/parallel/client/remotefunction.py
+++ b/IPython/parallel/client/remotefunction.py
@@ -161,13 +161,15 @@ class ParallelFunction(RemoteFunction):
     chunksize : int or None
         The size of chunk to use when breaking up sequences in a load-balanced manner
     ordered : bool [default: True]
-        Whether 
+        Whether the result should be kept in order. If False,
+        results become available as they arrive, regardless of submission order.
     **flags : remaining kwargs are passed to View.temp_flags
     """
 
-    chunksize=None
-    ordered=None
-    mapObject=None
+    chunksize = None
+    ordered = None
+    mapObject = None
+    _mapping = False
 
     def __init__(self, view, f, dist='b', block=None, chunksize=None, ordered=True, **flags):
         super(ParallelFunction, self).__init__(view, f, block=block, **flags)
@@ -176,23 +178,40 @@ def __init__(self, view, f, dist='b', block=None, chunksize=None, ordered=True,
 
         mapClass = Map.dists[dist]
         self.mapObject = mapClass()
-
+    
     @sync_view_results
     def __call__(self, *sequences):
         client = self.view.client
         
+        lens = []
+        maxlen = minlen = -1
+        for i, seq in enumerate(sequences):
+            try:
+                n = len(seq)
+            except Exception:
+                seq = list(seq)
+                if isinstance(sequences, tuple):
+                    # can't alter a tuple
+                    sequences = list(sequences)
+                sequences[i] = seq
+                n = len(seq)
+            if n > maxlen:
+                maxlen = n
+            if minlen == -1 or n < minlen:
+                minlen = n
+            lens.append(n)
+        
         # check that the length of sequences match
-        len_0 = len(sequences[0])
-        for s in sequences:
-            if len(s)!=len_0:
-                msg = 'all sequences must have equal length, but %i!=%i'%(len_0,len(s))
-                raise ValueError(msg)
+        if not self._mapping and minlen != maxlen:
+            msg = 'all sequences must have equal length, but have %s' % lens
+            raise ValueError(msg)
+        
         balanced = 'Balanced' in self.view.__class__.__name__
         if balanced:
             if self.chunksize:
-                nparts = len_0//self.chunksize + int(len_0%self.chunksize > 0)
+                nparts = maxlen // self.chunksize + int(maxlen % self.chunksize > 0)
             else:
-                nparts = len_0
+                nparts = maxlen
             targets = [None]*nparts
         else:
             if self.chunksize:
@@ -211,21 +230,17 @@ def __call__(self, *sequences):
         for index, t in enumerate(targets):
             args = []
             for seq in sequences:
-                part = self.mapObject.getPartition(seq, index, nparts)
-                if len(part) == 0:
-                    continue
-                else:
-                    args.append(part)
-            if not args:
+                part = self.mapObject.getPartition(seq, index, nparts, maxlen)
+                args.append(part)
+            if not any(args):
                 continue
 
-            # print (args)
-            if hasattr(self, '_map'):
+            if self._mapping:
                 if sys.version_info[0] >= 3:
                     f = lambda f, *sequences: list(map(f, *sequences))
                 else:
                     f = map
-                args = [self.func]+args
+                args = [self.func] + args
             else:
                 f=self.func
 
@@ -233,9 +248,9 @@ def __call__(self, *sequences):
             with view.temp_flags(block=False, **self.flags):
                 ar = view.apply(f, *args)
 
-            msg_ids.append(ar.msg_ids[0])
+            msg_ids.extend(ar.msg_ids)
 
-        r = AsyncMapResult(self.view.client, msg_ids, self.mapObject, 
+        r = AsyncMapResult(self.view.client, msg_ids, self.mapObject,
                             fname=getname(self.func),
                             ordered=self.ordered
                         )
@@ -249,16 +264,19 @@ def __call__(self, *sequences):
             return r
 
     def map(self, *sequences):
-        """call a function on each element of a sequence remotely.
+        """call a function on each element of one or more sequence(s) remotely.
         This should behave very much like the builtin map, but return an AsyncMapResult
         if self.block is False.
+        
+        That means it can take generators (will be cast to lists locally),
+        and mismatched sequence lengths will be padded with None.
         """
-        # set _map as a flag for use inside self.__call__
-        self._map = True
+        # set _mapping as a flag for use inside self.__call__
+        self._mapping = True
         try:
-            ret = self.__call__(*sequences)
+            ret = self(*sequences)
         finally:
-            del self._map
+            self._mapping = False
         return ret
 
 __all__ = ['remote', 'parallel', 'RemoteFunction', 'ParallelFunction']


Test patch:
diff --git a/IPython/parallel/tests/test_lbview.py b/IPython/parallel/tests/test_lbview.py
--- a/IPython/parallel/tests/test_lbview.py
+++ b/IPython/parallel/tests/test_lbview.py
@@ -58,6 +58,40 @@ def f(x):
         data = range(16)
         r = self.view.map_sync(f, data)
         self.assertEqual(r, map(f, data))
+    
+    def test_map_generator(self):
+        def f(x):
+            return x**2
+        
+        data = range(16)
+        r = self.view.map_sync(f, iter(data))
+        self.assertEqual(r, map(f, iter(data)))
+    
+    def test_map_short_first(self):
+        def f(x,y):
+            if y is None:
+                return y
+            if x is None:
+                return x
+            return x*y
+        data = range(10)
+        data2 = range(4)
+        
+        r = self.view.map_sync(f, data, data2)
+        self.assertEqual(r, map(f, data, data2))
+
+    def test_map_short_last(self):
+        def f(x,y):
+            if y is None:
+                return y
+            if x is None:
+                return x
+            return x*y
+        data = range(4)
+        data2 = range(10)
+        
+        r = self.view.map_sync(f, data, data2)
+        self.assertEqual(r, map(f, data, data2))
 
     def test_map_unordered(self):
         def f(x):