20
votes

In Python 2, a common (old, legacy) idiom is to use map to join iterators of uneven length using the form map(None,iter,iter,...) like so:

>>> map(None,xrange(5),xrange(10,12))
[(0, 10), (1, 11), (2, None), (3, None), (4, None)]

In Python 2, it is extended so that the longest iterator is the length of the returned list and if one is shorter than the other it is padded with None.

In Python 3, this is different. First, you cannot use None as an argument for the callable in position 1:

>>> list(map(None, range(5),range(10,12)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

OK -- I can fix that like so:

>>> def f(*x): return x    
... 
>>> list(map(f, *(range(5),range(10,12))))
[(0, 10), (1, 11)]

But now, I have a different problem: map returns the shortest iterator's length -- no longer padded with None.

As I port Python 2 code to Python 3, this is not a terrible rare idiom and I have not figured out an easy in place solution.

Unfortunately, the 2to3 tools does not pick this up -- unhelpfully suggesting:

-map(None,xrange(5),xrange(10,18))
+list(map(None,list(range(5)),list(range(10,18)))) 

Suggestions?


Edit

There is some discussion of how common this idiom is. See this SO post.

I am updating legacy code written when I was still in high school. Look at the 2003 Python tutorials being written and discussed by Raymond Hettinger with this specific behavior of map being pointed out...

4
map(None,*(iter,iter)) is not a common idiom. Have you heard of itertools.izip_longest()? Does exactly what you described, but without gimmicks. - Tadeck
@Tadeck: Since the behavior map described is straight from the Python 2 documents, yeah -- it is not uncommon... - dawg
@drewk: I am familiar with how map() works, but using map(None, *(iter, iter)) in this case seems to be very unpythonic for me. Also, I also wonder, why map(None,*(xrange(5),xrange(10,12))) and not more direct map(None, xrange(5), xrange(10,12))? Does exactly the same. Any reference that could prove it is pythonic? Guido's confirmation would be enough, or Raymond Hettinger's, or part of Python's docs would do. - Tadeck
I did not state it was Pythonic or even good practice; I said it was common because that is the behavior that is documented in map in Python 2. Google 'python join list of different lengths' Some of the older answers use map. - dawg
Well, "common" and "supported" are different things; I've certainly never seen it before. In any case, this functionality has been supplanted by zip and zip_longest, which are far more understandable IMO. - Danica

4 Answers

19
votes

itertools.zip_longest does what you want, with a more comprehensible name. :)

2
votes

I'll answer my own question this time.

With Python 3x, you can use itertools.zip_longest like so:

>>> list(map(lambda *a: a,*zip(*itertools.zip_longest(range(5),range(10,17)))))
[(0, 10), (1, 11), (2, 12), (3, 13), (4, 14), (None, 15), (None, 16)]

You can also roll ur own I suppose:

>>> def oldMapNone(*ells):
...     '''replace for map(None, ....), invalid in 3.0 :-( '''
...     lgst = max([len(e) for e in ells])
...     return list(zip(* [list(e) + [None] * (lgst - len(e)) for e in ells]))
... 
>>> oldMapNone(range(5),range(10,12),range(30,38))
[(0, 10, 30), (1, 11, 31), (2, None, 32), (3, None, 33), (4, None, 34), (None, None, 35), (None, None, 36), (None, None, 37)]
0
votes

One way if you need some obsolete functionality in from the Python 2 then one way - write it yourself. Of course, it's not built-in functionality, but at least it is something.

The code snippet below takes 27 lines

#!/usr/bin/env python3

def fetch(sequence, index):
  return None if len(sequence) <= index else sequence[index]

def mymap(f, *args):
  max_len = 0
  for i in range(len(args)): 
      max_len = max(max_len, len(args[i]))
  out = []
  for i in range(max_len):
      t = []
      for j in range(len(args)): 
          t.append(fetch(args[j],i))      

      if f != None:
          # Use * for unpack arguments from Arbitarily argument list
          # Use ** for unpack arguments from Keyword argument list
          out.append(f(*t))
      else:
          out.append(tuple(t))
  return out 

if __name__ == '__main__':
    print(mymap(None, [1,2,3,4,5],[2,1,3,4],[3,4]))
    print(mymap(None,range(5),range(10,12)))
-2
votes

you can solve the problem like this: list(map(lambda x, y: (x, y),[1, 2, 3 ,4, 5], [6, 7, 8, 9, 10]))