I am working in a domain in which ranges are conventionally described inclusively. I have human-readable descriptions such as from A to B
, which represent rang
Maybe the inclusive package proves helpful.
If you don't want to specify the step size but rather the number of steps, there is the option to use numpy.linspace
which includes the starting and ending point
import numpy as np
np.linspace(0,5,4)
# array([ 0. , 1.66666667, 3.33333333, 5. ])
Focusing on your request for best syntax, what about targeting:
l[1:UpThrough(5):2]
You can achieve this using the __index__ method:
class UpThrough(object):
def __init__(self, stop):
self.stop = stop
def __index__(self):
return self.stop + 1
class DownThrough(object):
def __init__(self, stop):
self.stop = stop
def __index__(self):
return self.stop - 1
Now you don't even need a specialized list class (and don't need to modify global definition either):
>>> l = [1,2,3,4]
>>> l[1:UpThrough(2)]
[2,3]
If you use a lot you could use shorter names upIncl
, downIncl
or even
In
and InRev
.
You can also build out these classes so that, other than use in slice, they act like the actual index:
def __int__(self):
return self.stop
I believe that the standard answer is to just use +1 or -1 everywhere it is needed.
You don't want to globally change the way slices are understood (that will break plenty of code), but another solution would be to build a class hierarchy for the objects for which you wish the slices to be inclusive. For example, for a list
:
class InclusiveList(list):
def __getitem__(self, index):
if isinstance(index, slice):
start, stop, step = index.start, index.stop, index.step
if index.stop is not None:
if index.step is None:
stop += 1
else:
if index.step >= 0:
stop += 1
else:
if stop == 0:
stop = None # going from [4:0:-1] to [4::-1] since [4:-1:-1] wouldn't work
else:
stop -= 1
return super().__getitem__(slice(start, stop, step))
else:
return super().__getitem__(index)
>>> a = InclusiveList([1, 2, 4, 8, 16, 32])
>>> a
[1, 2, 4, 8, 16, 32]
>>> a[4]
16
>>> a[2:4]
[4, 8, 16]
>>> a[3:0:-1]
[8, 4, 2, 1]
>>> a[3::-1]
[8, 4, 2, 1]
>>> a[5:1:-2]
[32, 8, 2]
Of course, you want to do the same with __setitem__
and __delitem__
.
(I used a list
but that works for any Sequence or MutableSequence
.)
Write an additional function for inclusive slice, and use that instead of slicing. While it would be possible to e.g. subclass list and implement a __getitem__
reacting to a slice object, I would advise against it, since your code will behave contrary to expectation for anyone but you — and probably to you, too, in a year.
inclusive_slice
could look like this:
def inclusive_slice(myList, slice_from=None, slice_to=None, step=1):
if slice_to is not None:
slice_to += 1 if step > 0 else -1
if slice_to == 0:
slice_to = None
return myList[slice_from:slice_to:step]
What I would do personally, is just use the "complete" solution you mentioned (range(A, B + 1)
, l[A:B+1]
) and comment well.
Without writing your own class, the function seems to be the way to go. What i can think of at most is not storing actual lists, just returning generators for the range you care about. Since we're now talking about usage syntax - here is what you could do
def closed_range(slices):
slice_parts = slices.split(':')
[start, stop, step] = map(int, slice_parts)
num = start
if start <= stop and step > 0:
while num <= stop:
yield num
num += step
# if negative step
elif step < 0:
while num >= stop:
yield num
num += step
And then use as:
list(closed_range('1:5:2'))
[1,3,5]
Of course you'll need to also check for other forms of bad input if anyone else is going to use this function.