12

Is there a Python function an "outer-zip", which is a extension of zip with different default values for each iterable?

a = [1, 2, 3]   # associate a default value 0
b = [4, 5, 6, 7] # associate b default value 1

zip(a,b)  # [(1, 4), (2, 5), (3, 6)]

outerzip((a, 0), (b, 1)) = [(1, 4), (2, 5), (3, 6), (0, 7)]
outerzip((b, 0), (a, 1)) = [(4, 1), (5, 2), (6, 3), (7, 1)]

I can almost replicate this outerzip function using map, but with None as the only default:

map(None, a, b) # [(1, 4), (2, 5), (3, 6), (None, 7)]

Note1: The built-in zip function takes an arbitrary number of iterables, and so should an outerzip function. (e.g. one should be able to calculate outerzip((a,0),(a,0),(b,1)) similarly to zip(a,a,b) and map(None, a, a, b).)

Note2: I say "outer-zip", in the style of this haskell question, but perhaps this is not correct terminology.

Community
  • 1
  • 1
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • 1
    should one be able to calculate `outerzip(a,0,a,2,b,1)`? – SilentGhost Oct 26 '12 at 11:41
  • That's a good question! I'm not sure whether this would make sense (personally it doesn't happen for me since a list's elements are always of a particular type and so have an implicit default value), but I'm not sure in a general `outerzip` function... – Andy Hayden Oct 26 '12 at 11:45
  • 1
    useful trick with `map`. – n611x007 Nov 07 '13 at 17:38
  • IMHO your question would be better titled something like "zip_longest() with multiple fill-values?" since it's tagged "Python". – martineau Feb 16 '14 at 11:46
  • 1
    @martineau re-titled, kept both (though I guess zip_longest is a more pythonic name!) – Andy Hayden Feb 17 '14 at 19:57

3 Answers3

13

It's called izip_longest (zip_longest in python-3.x):

>>> from itertools import zip_longest
>>> a = [1,2,3]
>>> b = [4,5,6,7]
>>> list(zip_longest(a, b, fillvalue=0))
[(1, 4), (2, 5), (3, 6), (0, 7)]
Jules G.M.
  • 3,624
  • 1
  • 21
  • 35
SilentGhost
  • 307,395
  • 66
  • 306
  • 293
5

You could modify zip_longest to support your use case for general iterables.

from itertools import chain, repeat

class OuterZipStopIteration(Exception):
    pass

def outer_zip(*args):
    count = len(args) - 1

    def sentinel(default):
        nonlocal count
        if not count:
            raise OuterZipStopIteration
        count -= 1
        yield default

    iters = [chain(p, sentinel(default), repeat(default)) for p, default in args]
    try:
        while iters:
            yield tuple(map(next, iters))
    except OuterZipStopIteration:
        pass


print(list(outer_zip( ("abcd", '!'), 
                      ("ef", '@'), 
                      (map(int, '345'), '$') )))
kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • Your use of `nonlocal` makes this a Python 3-only solution -- although a Python 2.x version could probably also easily be written. – martineau Feb 16 '14 at 11:28
  • @martineau: See http://stackoverflow.com/questions/3190706/nonlocal-keyword-in-python-2-x for how to substitute `nonlocal` in Python 2. – kennytm Feb 16 '14 at 11:51
  • Yes, I'm aware there are workarounds. As a matter of fact in the [2.x documentation](http://docs.python.org/2/library/itertools.html?highlight=izip_longest#itertools.izip_longest) for `izip_longest()` one is used for the `counter` variable in the pure Python code it says is equivalent -- which is why I said creating a 2.x version would probably be a relatively easy thing to do. I made my comment because the OP's question is not tagged Python-3.x. – martineau Feb 16 '14 at 13:34
  • This function is/was *very* clever, and deserves a bounty! One little question looking back, was there a reason to call the sub-function "sentinel"? – Andy Hayden Feb 18 '14 at 05:14
  • @AndyHayden: Probably to convey that it's a "sentinel value". – kennytm Feb 18 '14 at 07:25
  • @KennyTM ooooh... it seems I did not know the precise meaning of that word! Thanks again! :) – Andy Hayden Feb 18 '14 at 07:28
3

This function can be defined by extending each inputted list and zipping:

def outerzip(*args):
    # args = (a, default_a), (b, default_b), ...
    max_length = max( map( lambda s: len(s[0]), args))
    extended_args = [ s[0] + [s[1]]*(max_length-len(s[0])) for s in args ]
    return zip(*extended_args)

outerzip((a, 0), (b, 1)) # [(1, 4), (2, 5), (3, 6), (0, 7)]
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535