Python 3.x Shifting Ranges
Question:
suppose I have a range like this:
x = range(10)
which would have the following values as a list:
list(x) # Prints [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
I would like to shift this range (possibly multiple times)
and iterate over the results e.g.
# [7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
Creating an equivalent list is not a problem. But I’d like
to know if it is possible to create something like this as
a range to save some space in memory and of course it would
be nice if the solution could be about as performant as:
for i in range(1000000)
Answers:
You can wrap the range in a generator expression, applying the shift and modulo on the fly:
def shifted_range(rangeob, shift):
size, shift = rangeob.stop, shift * rangeob.step
return ((i + shift) % size for i in rangeob)
Demo:
>>> def shifted_range(rangeob, shift):
... size, shift = rangeob.stop, shift * rangeobj.step
... return ((i + shift) % size for i in rangeob)
...
>>> range_10 = range(10)
>>> list(shifted_range(range_10, 3))
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
>>> list(shifted_range(range_10, 7))
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
>>> range_10_2 = range(0, 10, 2)
>>> list(shifted_range(range_10_2, 4))
[8, 0, 2, 4, 6]
You could make this a wrapper object as well:
class RangeShift:
def __init__(self, rangeob, shift):
self._range = rangeob
self.shift = shift
@property
def start(self):
r = self._range
return (r.start + self.shift * r.step) % r.stop
@property
def stop(self):
r = self._range
return (r.stop + self.shift * r.step) % r.stop
def index(self, value):
idx = self._range.index(value)
return (idx - self.shift) % len(self._range)
def __getattr__(self, attr):
return getattr(self._range, attr)
def __getitem__(self, index):
r = self._range
return (r[index] + self.shift * r.step) % r.stop
def __len__(self):
return len(self._range)
def __iter__(self):
size, shift = self._range.stop, self.shift * self._range.step
return ((i + shift) % size for i in self._range)
This will behave just like the original range, but applying a shift to all values produced. It even lets you alter the shift!
Demo:
>>> range_10 = range(10)
>>> shifted = RangeShift(range_10, 7)
>>> len(shifted)
10
>>> shifted.start
7
>>> shifted.stop
7
>>> shifted.step
1
>>> shifted[3]
0
>>> shifted[8]
5
>>> list(shifted)
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
>>> shifted.shift = 3
>>> list(shifted)
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
>>> range_10_2 = range(0, 10, 2)
>>> shifted_10_2 = RangeShift(range_10_2, 4)
>>> list(shifted_10_2)
[8, 0, 2, 4, 6]
Best trick this wrapper now supports: reversing the shifted range:
>>> list(reversed(shifted))
[2, 1, 0, 9, 8, 7, 6, 5, 4, 3]
>>> list(reversed(shifted_10_2))
[6, 4, 2, 0, 8]
I guess the simplest way is to chain
two ranges:
from itertools import chain
shifted = chain(range(7, 10), range(7))
for x in shifted:
print(x)
You can use itertools to chain two ranges. This code works even if the ranges have step > 1.
import itertools
def shift_range(r, s):
return itertools.chain(range(r.start + s*r.step, r.stop, r.step),
range(r.start, r.start + s*r.step, r.step))
Testing:
>>> list(shift_range(range(10), 5))
[5, 6, 7, 8, 9, 0, 1, 2, 3, 4]
>>> list(shift_range(range(3, 30, 3), 5))
[18, 21, 24, 27, 3, 6, 9, 12, 15]
It’s an old question, but I just stumbled across it.
My solution:
offset = 7
shifted_range = range(x.start + offset, x.stop + offset, x.step)
suppose I have a range like this:
x = range(10)
which would have the following values as a list:
list(x) # Prints [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
I would like to shift this range (possibly multiple times)
and iterate over the results e.g.
# [7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
Creating an equivalent list is not a problem. But I’d like
to know if it is possible to create something like this as
a range to save some space in memory and of course it would
be nice if the solution could be about as performant as:
for i in range(1000000)
You can wrap the range in a generator expression, applying the shift and modulo on the fly:
def shifted_range(rangeob, shift):
size, shift = rangeob.stop, shift * rangeob.step
return ((i + shift) % size for i in rangeob)
Demo:
>>> def shifted_range(rangeob, shift):
... size, shift = rangeob.stop, shift * rangeobj.step
... return ((i + shift) % size for i in rangeob)
...
>>> range_10 = range(10)
>>> list(shifted_range(range_10, 3))
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
>>> list(shifted_range(range_10, 7))
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
>>> range_10_2 = range(0, 10, 2)
>>> list(shifted_range(range_10_2, 4))
[8, 0, 2, 4, 6]
You could make this a wrapper object as well:
class RangeShift:
def __init__(self, rangeob, shift):
self._range = rangeob
self.shift = shift
@property
def start(self):
r = self._range
return (r.start + self.shift * r.step) % r.stop
@property
def stop(self):
r = self._range
return (r.stop + self.shift * r.step) % r.stop
def index(self, value):
idx = self._range.index(value)
return (idx - self.shift) % len(self._range)
def __getattr__(self, attr):
return getattr(self._range, attr)
def __getitem__(self, index):
r = self._range
return (r[index] + self.shift * r.step) % r.stop
def __len__(self):
return len(self._range)
def __iter__(self):
size, shift = self._range.stop, self.shift * self._range.step
return ((i + shift) % size for i in self._range)
This will behave just like the original range, but applying a shift to all values produced. It even lets you alter the shift!
Demo:
>>> range_10 = range(10)
>>> shifted = RangeShift(range_10, 7)
>>> len(shifted)
10
>>> shifted.start
7
>>> shifted.stop
7
>>> shifted.step
1
>>> shifted[3]
0
>>> shifted[8]
5
>>> list(shifted)
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
>>> shifted.shift = 3
>>> list(shifted)
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
>>> range_10_2 = range(0, 10, 2)
>>> shifted_10_2 = RangeShift(range_10_2, 4)
>>> list(shifted_10_2)
[8, 0, 2, 4, 6]
Best trick this wrapper now supports: reversing the shifted range:
>>> list(reversed(shifted))
[2, 1, 0, 9, 8, 7, 6, 5, 4, 3]
>>> list(reversed(shifted_10_2))
[6, 4, 2, 0, 8]
I guess the simplest way is to chain
two ranges:
from itertools import chain
shifted = chain(range(7, 10), range(7))
for x in shifted:
print(x)
You can use itertools to chain two ranges. This code works even if the ranges have step > 1.
import itertools
def shift_range(r, s):
return itertools.chain(range(r.start + s*r.step, r.stop, r.step),
range(r.start, r.start + s*r.step, r.step))
Testing:
>>> list(shift_range(range(10), 5))
[5, 6, 7, 8, 9, 0, 1, 2, 3, 4]
>>> list(shift_range(range(3, 30, 3), 5))
[18, 21, 24, 27, 3, 6, 9, 12, 15]
It’s an old question, but I just stumbled across it.
My solution:
offset = 7
shifted_range = range(x.start + offset, x.stop + offset, x.step)