1

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()
gskluzacek
  • 113
  • 7
  • Why not just manage it with an index and `enumerate`? – Nick Bailey Mar 04 '22 at 05:24
  • I have been doing that up until now, but I have several places in my code where I'm doing this and was looking for something pythonic that would just return a list of tuples of Previous, current and next for each item in the list... – gskluzacek Mar 04 '22 at 05:29
  • If you are working with lists, why not just `[[data[i-1], data[i], data[(i+1)%len(data)]] for i in range(len(data))]` – juanpa.arrivillaga Mar 04 '22 at 05:56
  • @juanpa.arrivillaga, ya that definitely works. I was using similar code in many places. however, I'm looking for a solutions that is generic. I should have been more explicit in my question. I'm looking for something that takes parameters for: how many items should be in the tuple values returned (my example gave 3) and what the offset should be (my example gave -1). I want this because I was having use cases where I also need just 2 items in the tuples, for prev & curr, or curr & next: eg f(2, -1) and f(2, 0), or even f(5, -2). It should limit copies of the list to be memory efficient too. – gskluzacek Mar 06 '22 at 18:39

4 Answers4

1

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'])
maya
  • 1,029
  • 1
  • 2
  • 7
  • I've upvoted your solutions, but haven't accepted it as then answer. But I have update my original question with a proposed solution based off yours. Please have a look and I welcome your comments. The reason I didn't accept this as the answer is that 1) it doesn't return anything and 2) its not generic where it can create results of tuples of any size using any offset. I've updated my original question to explicitly call this out too. – gskluzacek Mar 06 '22 at 19:16
1

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)
Maximus
  • 1,244
  • 10
  • 12
  • I wasn't explicit enough in my original question, but... though I like the part of your solution where you factor out the rotation of the list into a separate function. As I mentioned in another comment of mine, I was looking for code that was generic, memory efficient and reusable in multiple places. So I'm looking for code that can do for example, prev, curr or curr, next or even prev, prev, curr, next, next, next. I was hoping to rely on iterators or generators as some of the list would be really long and I didn't want to make copies of the original data. – gskluzacek Mar 06 '22 at 18:50
0

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
Ry-
  • 218,210
  • 55
  • 464
  • 476
  • wow I really appreciate the thought and detail you put into your solution... the only downside I can see, though I haven't tried it yet, is that the code is kind of complicated looking and I'm having a hard time understanding it. Maybe if you could add some comments to your code it would help with my understanding. – gskluzacek Mar 06 '22 at 18:55
  • Also, I know my original question wasn't super explicit, but I was looking for something that kept the number of copies of the original data down to a minimum as the input sequence may be quite long - hence I was trying to find a solutions that relied on itertools i.e., be memory efficient). And additionally, I want something that would be generic and could handle more than just triples: For example: func(2, -1) - prev, curr 0r func(2, 0) - curr, next or even func(5, -2) prev, prev, curr, next, next. – gskluzacek Mar 06 '22 at 19:00
0

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
gskluzacek
  • 113
  • 7