3

How can I implement a circular range object in Python?

e.g.

Let S is a circular space modulo 2^3 (range [0, 2^3)). I want to generate a range object like this:

crange(3, 7, 2 ** 3)  # => a range object [3, 4, 5, 6]
crange(7, 3, 2 ** 3)  # => a range object [7, 0, 1, 2]

I tried this:

def crange(start, stop, modulo):
    if start > stop:
        return range(start, modulo) or range(stop)
    else:
        return range(start, stop)

But I can't input bigint to crange e.g. crange(8, 2, 2 ** 160).

OverflowError: Python int too large to convert to C ssize_t

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
sira
  • 281
  • 3
  • 10
  • 2
    What have you tried already? post it so we can help. SO is not a code writing service – kmaork Dec 05 '16 at 08:50
  • Show your Minimum Viable Code. – Abhijeet Kasurde Dec 05 '16 at 08:50
  • 2
    What result do you want for `crange(0, 8)` in the example you give? – Mark Dickinson Dec 05 '16 at 08:50
  • Sorry for the lack of explanation. I add detail information. – sira Dec 05 '16 at 08:54
  • 1
    related: [`xrange(2**100)` -> OverflowError: long int too large to convert to int](http://stackoverflow.com/q/1482480/4279). In particular, [`lrange()`](https://github.com/zed/lrange/) – jfs Dec 05 '16 at 08:56
  • `crange(8, 2, 2 ** 160)`. Um. Wouldn't this be asking for a list with almost 2**160 elements? If not, what sort of answer do you want here? – Mark Dickinson Dec 05 '16 at 08:56
  • @MarkDickinson In Python 3, range generates a range object. I don't want a list with 2**160 elements. – sira Dec 05 '16 at 09:03
  • Ah, you should probably clarify your question, then. Your desired outputs look very much like plain old lists. – Mark Dickinson Dec 05 '16 at 09:05
  • @MarkDickinson Sorry. I fixed. – sira Dec 05 '16 at 09:21
  • @J.F.Sebastian Thank you, I'll try it. – sira Dec 05 '16 at 09:22
  • 2
    Interestingly, your code is raising `OverflowError` because evaluating the truthiness of a large `range` apparently raises `OverflowError` (even though the range itself is representable). That's arguably a bug. I've opened http://bugs.python.org/issue28876 – Mark Dickinson Dec 05 '16 at 09:44
  • 1
    @sira: it is related but it is a different issue (lrange() won't help unless it will define `__bool__` method—it is easy but it will break the compatibility with the range() from Python 3). No range() object would generate `7, 0, 1, 2`. The specific error is due to [bool(range())](https://docs.python.org/3/reference/datamodel.html#object.__bool__) being implemented as len(range()) and due to the implementation history len() is limited to C ssize_t size. Here's what [Guido said about it in 2008](http://bugs.python.org/msg70525) – jfs Dec 05 '16 at 09:49
  • If you are just trying to iterate over the values, one simple approach is to just have [a circular iterator](https://stackoverflow.com/questions/23416381/circular-list-iterator-in-python) over an ordinary range, starting at the appropriate point. – Karl Knechtel Jan 13 '23 at 01:38

7 Answers7

3

Try this:

def crange(start, stop, modulo):
    result = []
    index = start
    while index != stop:
        result.append(index)
        index = (index + 1) % modulo
    return result

If you know that your list can be too long, you can use a generator instead that generates the necessage sequence:

def crange(start, stop, modulo):
    index = start
    while index != stop:
        yield index
        index = (index + 1) % modulo
Fomalhaut
  • 8,590
  • 8
  • 51
  • 95
3

I implemented crange which I want (in reference to @Ni and @J.F.Sebastian).

import math


class crange:
    def __init__(self, start, stop, step=None, modulo=None):
        if step == 0:
            raise ValueError('crange() arg 3 must not be zero')

        if step is None and modulo is None:
            self.start = 0
            self.stop = start
            self.step = 1
            self.modulo = stop
        else:
            self.start = start
            self.stop = stop
            if modulo is None:
                self.step = 1
                self.modulo = step
            else:
                self.step = step
                self.modulo = modulo

    def __iter__(self):
        n = self.start
        if n > self.stop:
            while n < self.modulo:
                yield n
                n += 1
            n = 0
        while n < self.stop:
            yield n
            n += 1

    def __contains__(self, n):
        if self.start >= self.stop:
            return self.start <= n < self.modulo or 0 <= n < self.stop
        else:
            return self.start <= n < self.stop

I got the following output:

>>> print(list(crange(start=7, stop=3, modulo=2 ** 4)))
[7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2]
>>> print(3 in crange(start=7, stop=3, modulo=2 ** 4))
False
>>> print(7 in crange(start=7, stop=3, modulo=2 ** 4))
True
>>> print(list(crange(start=3, stop=7, modulo=2 ** 4)))
[3, 4, 5, 6]
>>> print(3 in crange(start=3, stop=7, modulo=2 ** 4))
True
>>> print(7 in crange(start=3, stop=7, modulo=2 ** 4))
False
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
sira
  • 281
  • 3
  • 10
  • In one of the branches, there is `self.modulo = step` which is super suspicious. It was probably intended to be `self.modulo = stop` instead. But actually, `self.modulo = stop` can be problematic if any of `start` or `stop` is negative. You probably want to use `self.modulo = max(abs(start), abs(stop), abs(stop - start))` instead, to make absolutely sure the modulo is not too small. – Stef Jan 23 '23 at 15:01
  • Nevermind my previous comment, I now understand that the if/else branches were meant to bypass python's way of handling optional parameters. You should probably add a comment just before the class or just before `__init__` to explain all the different ways `crange(...)` can be called. – Stef Jan 23 '23 at 15:09
  • Actually, `list(crange(12,8))` and `list(crange(0,12,1,8))` both result in `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]` so there is a definitely a bug somewhere. The modulo 8 is simply ignored. – Stef Jan 23 '23 at 15:11
2

You can avoid the use of range and the storage of a huge list in memory by creating your own generator:

def crange(start, end, modulo):
    if start > end:
        while start < modulo:
            yield start
            start += 1
        start = 0

    while start < end:
        yield start
        start += 1

print list(crange(3, 7, 2 ** 3))
print list(crange(7, 3, 2 ** 3))
print next(crange(8, 2, 2 ** 160))

This code outputs:

[3, 4, 5, 6]
[7, 0, 1, 2]
8
kmaork
  • 5,722
  • 2
  • 23
  • 40
  • `crange(2, 2 ** 100, 2 ** 160)` take up so much memory. How can I fix it? – sira Dec 05 '16 at 09:10
  • It doesn't. The generator doesn't store anything in memory except for the local variables. – kmaork Dec 05 '16 at 09:11
  • I understood. Thank you. – sira Dec 05 '16 at 09:13
  • This solution is wrong for the general case, because it assumes that `end-start` won't be greater than `modulo*2`. @Fomalhaut 's solution does work in this case, and so does mine which I think is even simpler, although they assume `start<=stop`, that is, `crange(7, 3, 2 ** 3)` would need to be coded as `crange(7, 3+2**3, 2 ** 3)` – Pepe Mandioca Jun 28 '22 at 19:56
1

An equivalent solution to that of @Fomalhaut but simpler:

def crange(start, end, modulo):
    for i in range(start,end):
        yield i % modulo
Pepe Mandioca
  • 334
  • 2
  • 10
0

Added no reset to start and subscriptability. original copied from answer by @sira

class Crange:
    def __init__(self, start, stop, step=None, modulo=None, no_reset=True):
        if step == 0:
            raise ValueError('crange() arg 3 must not be zero')
        self.no_reset = no_reset
        if step is None and modulo is None:
            self.start = 0
            self.stop = start
            self.step = 1
            self.modulo = stop
        else:
            self.start = start
            self.stop = stop
            if modulo is None:
                self.step = 1
                self.modulo = step
            else:
                self.step = step
                self.modulo = modulo

    def __iter__(self):
        n = self.start
        if n > self.stop:
            while n < self.modulo:
                yield n
                n += self.step
            if n > self.modulo: 
                n = n-self.modulo if self.no_reset else 0
            else: n=0
        while n < self.stop:
            yield n
            n += self.step
            
    def __getitem__(self, __name):
        return [*self][__name]
        
    def __contains__(self, n):
        if self.start >= self.stop:
            return self.start <= n < self.modulo or 0 <= n < self.stop
        else:
            return self.start <= n < self.stop
    
Sadern Alwis
  • 104
  • 1
  • 4
  • 17
0

I made my own version that can slice forwards and backwards. What it can't do in this state is use negative integers and different step sizes than one though, so that would need to be implemented to cover the whole gamut of slicing options in python. I bet that's an easy addition though.

l = list(range(10))

def crange(l,a,b):
  
  assert a >= 0
  assert b >= 0
  
  d = b-a 
  
  if d == 0:
    return
  elif d > 0:
    increment = 1
  elif d < 0:
    increment = -1
  
  start = a%len(l)
  counter = 0
  
  while counter <= abs(d):
    index = abs(start + counter*increment)%len(l)
    yield l[index]
    counter += 1
    
print(list(l))
print(list(crange(l,0,6)))
print(list(crange(l,6,10)))
print(list(crange(l,10,25)))
print(list(crange(l,9,0)))
print(list(crange(l,10,5)))

J.Doe
  • 224
  • 1
  • 4
  • 19
0

Python 3.x's range objects provides more functionality than just iteration and membership testing: subscripting (indexing), reverse iteration, length testing (which implies boolean coercion), the .count and .index methods (all of these collectively being the Sequence protocol), plus __str__ and __repr__.

To emulate these, we can wrap a range object, delegating to its methods with modified arguments. We can also leverage collections.abc in order to avoid the need to delegate everything, relying on mixins from the Sequence ABC instead.

There's also an issue here in that the input specification is tricky to interpret in corner cases, and inflexible (for example, we can't represent an iterator over [3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6]). The simple way around this is to redefine: a crange(start, stop, step, modulo) iterates over the values start % modulo, (start + step) % modulo ... (start + n * step) % modulo, such that (start + n * step) is limited to stop. (That is: the same values as would be in the range, but applying the specified modulo value to each of them.)

This is fairly straightforward to implement, as we need only __getitem__ and __len__:

from collections.abc import Sequence
class crange(Sequence):
    def __init__(self, start, stop, *, step=1, modulo=None):
        self._modulo, self._impl = modulo, range(start, stop, step)
    def __getitem__(self, index):
        result = self._impl[index]
        if self._modulo is not None:
            result %= self._modulo
        return result
    def __len__(self):
        return len(self._impl)
    def __repr__(self):
        return f'c{self._impl}'

Now we can do fun things like:

>>> list(reversed(crange(7, 19, step=3, modulo=8)))
[0, 5, 2, 7]
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153