In Python, how do I iterate over previous, current and next values of a given list?

551 Views Asked by At

I don't even know where to begin to provide an adequate title for this question. Please suggest something better if you can.

EDIT:
Sorry, I need to clarify my question. I'm looking for something that is:
* memory efficient (i.e., using itertools, iterators, or generators) and
* generic can handle results with tuple of any size with any amount of offset.
* reusable without having to duplicate code.

some example use cases:

  1. func(3, -1) -- PREV, CURR, NEXT
  2. func(2, -1) -- PREV, CURR
  3. func(2, 0) -- CURR, NEXT
  4. or even func(5, -2) -- PREV, PREV, CURR, NEXT, NEXT

Here's an example with values for use case #1

for example if my list is: ['abc', 'def', 'ghi', 'jkl']

the results of iterating over it would be:

jkl abc def
abc def ghi
def ghi jkl
ghi jkl abc

So obviously I'm using some wrapping here, i.e., for the first iteration, I use the last value in the list for the previous value. And likewise, for the last iteration, I use the first value in the list for the next value.


My question is, is there a more pythonic way that uses a different itertools function or a better itertools recipe to do what I want.


Here is some new code that I'd like to propose as a solution to my own question that is based off of @maya answer, but is more generic and uses yield instead of print.

def cyclic_n_tuples(seq, n=3, offset=-1):
    seq_len = len(seq)
    offset = seq_len + offset if offset < 0 else offset
    for i in range(offset, offset + seq_len):
        if (start := i % seq_len) < (end := (i + n) % seq_len):
            yield seq[start:end]
        else:
            yield seq[start:] + seq[:end]


seq = "111 222 333 444 555 666 777 888 999 000".split()
for i in cyclic_n_tuples(seq, 5,  -2):
    print(*i)

output from the above

999 000 111 222 333
000 111 222 333 444
111 222 333 444 555
222 333 444 555 666
333 444 555 666 777
444 555 666 777 888
555 666 777 888 999
666 777 888 999 000
777 888 999 000 111
888 999 000 111 222

code posted with original question:


I've come up with 2 versions of a generic function that return a list of tuples per the above requirements.

The first creates multiple slices from the original list and then uses zip to create the list of tuples. Probably not the most memory efficient solution...

The second uses itertools chain & islice functions, to do the same but without creating multiple copies of the array. However, because of the nature of iterators, I have to keep creating fresh copies of the shifted list. That, and it looks confusing and not very pythonic.

both functions take the same parameters
* the_list of values to be operated on
* n number of items in each tuple in the returned value
* offset a give number of positions (either positive or negative) to shift the original input by

The shifting of the input is needed for the use case where we want the tuples to contain the previous value.

from itertools import islice, chain


def wrapit(the_list, n, offset=0):
    shifted_list = the_list[offset:] + the_list[:offset]
    list_of_lists = [shifted_list[i:] + shifted_list[:i] for i in range(n)]
    return zip(*list_of_lists)


def iter_wrapit(the_list, n, offset=0):
    offset = offset if offset >= 0 else len(the_list) + offset
    lst_list = [
        chain(
            islice(chain(islice(the_list, offset, None), islice(the_list, 0, offset)), i, None),
            islice(chain(islice(the_list, offset, None), islice(the_list, 0, offset)), 0, i)
        ) for i in range(n)
    ]
    return zip(*lst_list)


def main():
    a = "abc def ghi jkl".split()
    print(a)
    print("-" * 10)
    for t in wrapit(a, 3, -1):
        print(*t)
    print("-" * 10)
    for t in iter_wrapit(a, 3, -1):
        print(*t)
    print("-" * 10)


if __name__ == "__main__":
    main()
4

There are 4 best solutions below

0
On BEST ANSWER

Ok, this is what I feel is the best answer. It is generic in that it can return tuples of any length and handle both positive and negative offsets. Thanks to everyone who provided input.

def cyclic_n_tuples(seq, n=3, offset=-1):
    seq_len = len(seq)
    offset = seq_len + offset if offset < 0 else offset
    for i in range(offset, offset + seq_len):
        if (start := i % seq_len) < (end := (i + n) % seq_len):
            yield seq[start:end]
        else:
            yield seq[start:] + seq[:end]


seq = "111 222 333 444 555 666 777 888 999 000".split()
for i in cyclic_n_tuples(seq, 5,  -2):
    print(*i)

sample output for the above

999 000 111 222 333
000 111 222 333 444
111 222 333 444 555
222 333 444 555 666
333 444 555 666 777
444 555 666 777 888
555 666 777 888 999
666 777 888 999 000
777 888 999 000 111
888 999 000 111 222
2
On

You could build it on top of a sliding window function:

def cyclic_triples(seq):
    if not seq:
        return

    if len(seq) == 1:
        yield (seq[0], seq[0], seq[0])
        return

    yield (seq[-1], seq[0], seq[1])
    yield from map(tuple, windows(seq, 3))
    yield (seq[-2], seq[-1], seq[0])

where an example implementation of windows is:

from collections import deque

def windows(iterable, size):
    d = deque((), size)
    it = iter(iterable)

    for i in range(size - 1):
        d.append(next(it, None))

    for x in it:
        d.append(x)
        yield d
1
On

try this:

def iter_item(lst: list) -> list:
    for i in range(len(lst)):
        start = len(lst) - 1 if i < 1 else i - 1
        end = (start + 3) % len(lst)
        if end > start:
            print(*lst[start:end])
        else:
            print(*(lst[start:] + lst[:end]))


iter_item(['abc', 'def', 'ghi', 'jkl'])
1
On

Using slices:

def rotate(seq, k):
    return seq[k:] + seq[:k]

l = ['abc', 'def', 'ghi', 'jkl']
for i, j, k in zip(rotate(l, -1), l, rotate(l, 1)):
    print(i, j, k)