python-3.xsequencesubclassing

subclass tuple in Python to simulate infinite repeating sequence?


I have a sequence of 16 elements (dealer and vulnerability of bridge boards, to be specific) that repeat endlessly in theory (in practice it almost never gets past 128, and rarely past 36).

That is, the information on board 17, 33, 49, 65,... is the same as that on board 1. (Arguably, there is no board zero, but I'm happy if it pretends to be board 16, too).

I would like to create a tuple-like object that will return the correct information for arbitrary index: board_info[129] for example. It would be very nice if I can slice it as a normal tuple would: board_info[15:22] to get a len-7 tuple for the 7 boards starting with 15.

I know I can create a function that will do the right thing for one board, like:

# BoardInfo is the object containing board-specific information
# instantiate DEALVUL as "BoardInfo for boards 0-15" here

def board_info(board: int) -> BoardInfo:
    return DEALVUL[board % 16]

But something "native" that allows me to treat this as an immutable sequence with all the features of a tuple (slice, get the next one, ...) would be much more useful.

I have tried something like (note, I know this doesn't work)

class BInfo(tuple):
    """Infinite list of board information."""

    def __new__(cls):
        return tuple.__new__(*DEALVUL)

    def __getitem__(self, board_number: int) -> BoardInfo:
        return self[board_number % 16]

    def __iter__(self):
        return iter(self)

    def __next__(self):
        try:
            return next(self)
        except StopIteration:
           # reset the iterator to zero and
            return next(self) 

It's clear I do not understand the magic methods.


Solution

  • You can create a sliceable view over the given sequence that when sliced, returns an itertools.islice of an itertools.cycle over the given sequence that begins from the slice's starting index's modulo of the size of the sequence:

    from itertools import cycle, islice
    
    class CycleView:
        def __init__(self, seq):
            self.seq = seq
    
        def __getitem__(self, index):
            if isinstance(index, slice):
                start = (index.start or 0) % len(self.seq)
                if (stop := index.stop) is not None:
                    stop = start + stop - (index.start or 0)
                return islice(cycle(self.seq), start, stop)
            return self.seq[index % len(self.seq)]
    

    or maps indices between the start and the stop of the slice to the __getitem__ method of view itself, which handles out-of-boundary indices by taking a modulo of the size of the sequence:

    from itertools import count
    
    class CycleView:
        def __init__(self, seq):
            self.seq = seq
    
        def __getitem__(self, index):
            if isinstance(index, slice):
                return map(
                    self.__getitem__,
                    count(index.start) if index.stop is None
                    else range(index.start or 0, index.stop)
                )
            return self.seq[index % len(self.seq)]
    

    so that as an example:

    DEADVUL = 'x', 'y', 'z'
    print(*CycleView(DEADVUL)[:10])
    print(*CycleView(DEADVUL)[2000000000:2000000010])
    

    outputs:

    x y z x y z x y z x
    z x y z x y z x y z
    

    Demo 1: https://ideone.com/I2WamY

    Demo 2: https://ideone.com/HBhJfc