1

I am looking to take as input a list and then create another list which contains tuples (or sub-lists) of adjacent elements from the original list, wrapping around for the beginning and ending elements. The input/output would look like this:

l_in  = [0, 1, 2, 3]
l_out = [(3, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 0)]

My question is closely related to another titled getting successive adjacent elements of a list, but this other question does not take into account wrapping around for the end elements and only handles pairs of elements rather than triplets.

I have a somewhat longer approach to do this involving rotating deques and zipping them together:

from collections import deque
l_in = [0, 1, 2, 3]
deq = deque(l_in)
deq.rotate(1)
deq_prev = deque(deq)
deq.rotate(-2)
deq_next = deque(deq)
deq.rotate(1)
l_out = list(zip(deq_prev, deq, deq_next))
# l_out is [(3, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 0)]

However, I feel like there is probably a more elegant (and/or efficient) way to do this using other built-in Python functionality. If, for instance, the rotate() function of deque returned the rotated list instead of modifying it in place, this could be a one- or two-liner (though this approach of zipping together rotated lists is perhaps not the most efficient). How can I accomplish this more elegantly and/or efficiently?

Grayscale
  • 1,114
  • 1
  • 6
  • 17

4 Answers4

1

This can be done with slices:

l_in  = [0, 1, 2, 3]

l_in = [l_in[-1]] + l_in + [l_in[0]]
l_out = [l_in[i:i+3] for i in range(len(l_in)-2)]

Well, or such a perversion:

div = len(l_in)
n = 3
l_out = [l_in[i % div: i % div + 3]
         if len(l_in[i % div: i % div + 3]) == 3
         else l_in[i % div: i % div + 3] + l_in[:3 - len(l_in[i % div: i % div + 3])]
         for i in range(3, len(l_in) + 3 * n + 2)]

You can specify the number of iterations.

1

One approach may be to use itertools combined with more_itertools.windowed:

import itertools as it

import more_itertools as mit


l_in  = [0, 1, 2, 3]
n = len(l_in)
list(it.islice(mit.windowed(it.cycle(l_in), 3), n-1, 2*n-1))
# [(3, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 0)]

Here we generated an infinite cycle of sliding windows and sliced the desired subset.


FWIW, here is an abstraction of the latter code for a general, flexible solution given any iterable input e.g. range(5), "abcde", iter([0, 1, 2, 3]), etc.:

def get_windows(iterable, size=3, offset=-1):
    """Return an iterable of windows including an optional offset."""
    it1, it2 = it.tee(iterable)
    n = mit.ilen(it1)
    return it.islice(mit.windowed(it.cycle(it2), size), n+offset, 2*n+offset)


list(get_windows(l_in))
# [(3, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 0)]

list(get_windows("abc", size=2))
# [('c', 'a'), ('a', 'b'), ('b', 'c')]

list(get_windows(range(5), size=2, offset=-2))
# [(3, 4), (4, 0), (0, 1), (1, 2), (2, 3)]

Note: more-itertools is a separate library, easily installed via:

> pip install more_itertools
pylang
  • 28,402
  • 9
  • 97
  • 94
  • The use of `more_itertools.windowed` seems like a great alternate way to do this that was made for this kind of problem. It should be noted though that the package `more_itertools` is not built-in (though it can be easily installed with `pip3 install more-itertools`). – Grayscale Aug 18 '17 at 14:09
  • I think this can actually be simplified down to `list(mit.windowed(l_in[-1:] + l_in + l_in[:1], 3))`, which is probably the cleanest way of doing it I have seen so far. – Grayscale Aug 18 '17 at 14:16
  • Updated. Thank you. Yes, windowing is indeed a useful concept. You are welcome to apply any technique you wish with it. Your suggestion is concise, clear and will certainly work in this case, but it relies on concatenating lists (or tuples). I attempted to give general solution that should work with any size iterable and window. For example, your suggestion potentially gives an unexpected answer for `range(5)`, window size of 2. – pylang Aug 19 '17 at 00:26
0

Well I figured out a better solution as I was writing the question, but I already went through the work of writing it, so here goes. This solution is at least much more concise:

l_out = list(zip(l_in[-1:] + l_in[:-1], l_in, l_in[1:] + l_in[:1]))

See this post for different answers on how to rotate lists in Python.

The one-line solution above should be at least as efficient as the solution in the question (based on my understanding) since the slicing should not be more expensive than the rotating and copying of the deques (see https://wiki.python.org/moin/TimeComplexity).

Other answers with more efficient (or elegant) solutions are still welcome though.

Grayscale
  • 1,114
  • 1
  • 6
  • 17
0

as you found there is a list rotation slicing based idiom lst[i:] + lst[:i]

using it inside a comprehension taking a variable n for the number of adjacent elements wanted is more general [lst[i:] + lst[:i] for i in range(n)]

so everything can be parameterized, the number of adjacent elements n in the cyclic rotation and the 'phase' p, the starting point if not the 'natural' 0 base index, although the default p=-1 is set to -1 to fit the apparant desired output

tst = list(range(4))

def rot(lst, n, p=-1):
    return list(zip(*([lst[i+p:] + lst[:i+p] for i in range(n)])))

rot(tst, 3)
Out[2]: [(3, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 0)]    

showing the shortend code as per the comment

f5r5e5d
  • 3,266
  • 3
  • 10
  • 18
  • I think the outer list comprehension is redundant. You should be able to simplify what is being returned down to `list(zip(*[lst[i+p:] + lst[:i+p] for i in range(n)]))`. – Grayscale Aug 18 '17 at 14:48
  • It also seems like unexpected behavior that specifying an `n` such that `n > len(lst)` will result in the first element being used as a filler value at the end of each tuple (as seen in the last case of `rot(txt, 6, 0)` that you show) rather than continuing to cycle through `lst`. – Grayscale Aug 18 '17 at 15:48