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]