new_here
new_here

Reputation: 41

How to slice a circular list

Suppose I have list as follow:

lst = [0,10,20,30,40,50,60,70]

I want elements from lst from index = 5 to index = 2 in cyclic order.

lst[5:2] yields [] I want lst[5:2] = [50,60,70,0,10]. Is there any simple library function to do this?

Upvotes: 3

Views: 659

Answers (5)

Brian61354270
Brian61354270

Reputation: 14423

For something that allows you to still use the native slicing syntax and that maintains static typing compatibility, you can use a light wrapper class around your sequence:

from typing import Generic, Protocol, TypeVar


S = TypeVar('S', bound="ConcatSequence")


class CircularView(Generic[S]):

    def __init__(self, seq: S) -> None:
        self.seq = seq

    def __getitem__(self, s: slice) -> S:
        if s.start <= s.stop:
            return self.seq[s]
        else:
            wrap = len(self.seq) % s.step if s.step else 0
            return self.seq[s.start::s.step] + self.seq[wrap:s.stop:s.step]


lst = [0, 10, 20, 30, 40, 50, 60, 70]

print(CircularView(lst)[2:5])    # [20, 30, 40]
print(CircularView(lst)[5:2])    # [50, 60, 70, 0, 10]
print(CircularView(lst)[5:2:2])  # [50, 70, 0]
print(CircularView(lst)[5:3:2])  # [50, 70, 0, 20]
print(CircularView(lst)[4:3:3])  # [40, 70, 20]

with the optional protocol for static typing

class ConcatSequence(Protocol):
    """
    A sequence that implements concatenation via '__add__'.

    This protocol is required instead of using 
    'collections.abc.Sequence' since not all sequence types
    implement '__add__' (for example, 'range').
    """

    def __add__(self, other):
        ...

    def __getitem__(self, item):
        ...

    def __len__(self):
        ...

This method passes type checking with mypy.

Upvotes: 1

mozway
mozway

Reputation: 260790

Using a deque as suggested in comments:

from collections import deque

d = deque(lst)

a,b = 5,2
d.rotate(-a)
list(d)[:len(lst)-a+b]

NB. I find it not very practical as it requires to make a copy of the list to create the deque, and another copy to slice

Upvotes: 1

BurgeoningApe
BurgeoningApe

Reputation: 384

There are two fast/easy solutions to this problem.

The first, and more complicated method, would be to overwrite the default python library implementation of the python list.__getitem__ method, which has been referenced in other places on StackOverflow.

This would allow you to reference the slicing as you would normally, i.e. list[5:3], and it would, in theory, behave as you define. This would be a "local expansion" of the default library.

Transversely, you could implement your own function that will iterate over your list in a circular manner, that meets your own criterion. Some pseudo-code:

def foo(left_idx, right_idx):
    if right_idx < left_idx:
        wrap index when right bound has been reached
    else:
        iterate normally

Upvotes: 0

CozyCode
CozyCode

Reputation: 504

You could use a function like this:

def circular_indexing(list_, start_index, end_index) -> list:
    return [*list_[start_index:len(list_)], *list_[0:end_index]]

For example:

list1 = [0, 1, 2, 3]

def circular_indexing(list_, start_index, end_index) -> list:
    return [*list_[start_index:len(list_)], *list_[0:end_index]]

print(circular_indexing(list1, 2, 1))

Output: [2, 3, 0]

Upvotes: 0

mozway
mozway

Reputation: 260790

Simply split the slicing in two if the second term is smaller than the first:

lst = [0,10,20,30,40,50,60,70]


def circslice(l, a, b):
    if b>=a:
        return l[a:b]
    else:
        return l[a:]+l[:b]
    
circslice(lst, 5, 2)

output: [50, 60, 70, 0, 10]

Upvotes: 5

Related Questions