phndiaye
phndiaye

Reputation: 329

Propagate latest not None value to rest of items in izip_longest

I'm wondering if there is a way to have the default values of an "unmatched" item in a list set to the latest value in izip_longtest function.

The problem: i have a word which have a position on x, on y, and a rotation. Theses ones can be multiple and each letter of the word must have position/rotation.

More explicitely: i have two lists:

By example, if i have the word "HELLO" and x = [1,2,3,4,5], y = [6,7,8,9,10] and r = [11,12,13,14,15].

The 'H' letter will have x = 1, y = 6, y = 11 The last letter 'O' will have x = 5, y = 10, r = 15

But, if the word is "PROBLEM" and x = [1], only the 'P' letter will match an x value and all the other letters will be set to None with izip_longest.

What i want is to "progagate" the latest or only one x to all the other letters. So, please, If you have any suggestion or solution, tell me. Thanks by advance !

Upvotes: 1

Views: 479

Answers (4)

RT Dean
RT Dean

Reputation: 11

As you've noticed, izip_longest doesn't quite take you all the way there. You can wrap it, however, and take it across the finish line.

def izip_longest_last_value(*args, **kwargs):
    last = (None,) * len(args)
    for x in izip_longest(*args, **kwargs):
        last = tuple(z if z is not None else last[y] for y,z in enumerate(x))
        yield last

izip_longest has a signature of *args, **kwargs, so we repeat that here. We also support an arbitrary number of positional arguments, so you can izip 1, 2, or 46 different lists together.

We initialize last to be a tuple as long as the number of positional args. Last will be updated to contain the values from the last iteration.

for x in izip_longest() is us grabbing the current value from izip_longest, the meat of the wrapper.

last = tuple(...) is us updating the last-tuple, and filling in either the current values from izip or the value from last if izip_longest returned a None for the space, indicating that it had been padded.

Upvotes: 1

Wolph
Wolph

Reputation: 80061

Since the default zip_longest does not work for you here's a slightly smarter version:

import pprint
word = 'PROBLEM'

def zip_longest(*iterators):
    last = {}
    # Make a list of non-empty iterators
    non_empty = dict.fromkeys(range(len(iterators)))

    # Make sure it's an iterator, does no harm on iterators and helps
    # on lists
    iterators = map(iter, iterators)
    while non_empty:
        # Prepare the row
        row = []

        # Walk through the iterators in the given order
        for i, iterator in enumerate(iterators):
            # If there are still values, find the next
            if i in non_empty:
                try:
                    last[i] = iterator.next()
                except StopIteration:
                    # No items anymore, this one is empty
                    del non_empty[i]

            # Add the current (or last if it was already empty) value
            row.append(last.get(i))

        yield row

First/simple test:

x = [1]
y = [2]
pprint.pprint(list(zip_longest(word, x, y)))
[['P', 1, 2],
 ['R', 1, 2],
 ['O', 1, 2],
 ['B', 1, 2],
 ['L', 1, 2],
 ['E', 1, 2],
 ['M', 1, 2],
 ['M', 1, 2]]

Slightly more complicated:

x = range(3)
y = range(6)
pprint.pprint(list(zip_longest(word, x, y)))
[['P', 0, 0],
 ['R', 1, 1],
 ['O', 2, 2],
 ['B', 2, 3],
 ['L', 2, 4],
 ['E', 2, 5],
 ['M', 2, 5],
 ['M', 2, 5]]

With other longer ranges:

x = range(10)
y = range(5)
pprint.pprint(list(zip_longest(word, x, y)))

[['P', 0, 0],
 ['R', 1, 1],
 ['O', 2, 2],
 ['B', 3, 3],
 ['L', 4, 4],
 ['E', 5, 4],
 ['M', 6, 4],
 ['M', 7, 4],
 ['M', 8, 4],
 ['M', 9, 4],
 ['M', 9, 4]]

For completeness:

x = []
y = []
pprint.pprint(list(zip_longest(word, x, y)))

[['P', None, None],
 ['R', None, None],
 ['O', None, None],
 ['B', None, None],
 ['L', None, None],
 ['E', None, None],
 ['M', None, None],
 ['M', None, None]]

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1123420

Use itertools.cycle() for the shorter input and do not use izip_longest():

from itertools import cycle, izip

izip(word, cycle(x), cycle(y), cycle(r))

Now x, y and r will be cycled through to match up with the rest of the characters in word:

>>> list(zip(word, cycle(x), cycle(y), cycle(r)))
[('P', 1, 6, 11), ('R', 2, 7, 12), ('O', 3, 8, 13), ('B', 4, 9, 14), ('L', 5, 10, 15), ('E', 1, 6, 11), ('M', 2, 7, 12)]

Upvotes: 0

Sylvain Leroux
Sylvain Leroux

Reputation: 52030

You will probably require your own custom zip iterator. Mostly based on izip_longest but with some memory of the last not None value.

There is probably room for improvement, but here is the spirit:

import itertools

def myzip(it1, it2):
    last1 = None
    last2 = None
    for i in itertools.izip_longest(it1, it2):
        if i[0] is not None:
            last1 = i[0]
        if i[1] is not None:
            last2 = i[1]
        yield((last1, last2))

And here is the "test case":

>>> lst1 = ('a', 'b')
>>> lst2 = (1,2,3,4,5)

>>> print list(myzip(lst1, lst2))
[('a', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5)]

>>> print list(myzip(lst2, lst1))
[(1, 'a'), (2, 'b'), (3, 'b'), (4, 'b'), (5, 'b')]

Upvotes: 0

Related Questions