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 ranges that include both end points - e.g. from 2 to 4
means 2, 3, 4
.
What is the best way to work with these ranges in Python code? The following code works to generate inclusive ranges of integers, but I also need to perform inclusive slice operations:
def inclusive_range(start, stop, step):
return range(start, (stop + 1) if step >= 0 else (stop - 1), step)
The only complete solution I see is to explicitly use + 1
(or - 1
) every time I use range
or slice notation (e.g. range(A, B + 1)
, l[A:B+1]
, range(B, A - 1, -1)
). Is this repetition really the best way to work with inclusive ranges?
Edit: Thanks to L3viathan for answering. Writing an inclusive_slice
function to complement inclusive_range
is certainly an option, although I would probably write it as follows:
def inclusive_slice(start, stop, step):
...
return slice(start, (stop + 1) if step >= 0 else (stop - 1), step)
...
here represents code to handle negative indices, which are not straightforward when used with slices - note, for example, that L3viathan's function gives incorrect results if slice_to == -1
.
However, it seems that an inclusive_slice
function would be awkward to use - is l[inclusive_slice(A, B)]
really any better than l[A:B+1]
?
Is there any better way to handle inclusive ranges?
Edit 2: Thank you for the new answers. I agree with Francis and Corley that changing the meaning of slice operations, either globally or for certain classes, would lead to significant confusion. I am therefore now leaning towards writing an inclusive_slice
function.
To answer my own question from the previous edit, I have come to the conclusion that using such a function (e.g. l[inclusive_slice(A, B)]
) would be better than manually adding/subtracting 1 (e.g. l[A:B+1]
), since it would allow edge cases (such as B == -1
and B == None
) to be handled in a single place. Can we reduce the awkwardness in using the function?
Edit 3: I have been thinking about how to improve the usage syntax, which currently looks like l[inclusive_slice(1, 5, 2)]
. In particular, it would be good if the creation of an inclusive slice resembled standard slice syntax. In order to allow this, instead of inclusive_slice(start, stop, step)
, there could be a function inclusive
that takes a slice as a parameter. The ideal usage syntax for inclusive
would be line 1
:
l[inclusive(1:5:2)] # 1
l[inclusive(slice(1, 5, 2))] # 2
l[inclusive(s_[1:5:2])] # 3
l[inclusive[1:5:2]] # 4
l[1:inclusive(5):2] # 5
Unfortunately this is not permitted by Python, which only allows the use of :
syntax within []
. inclusive
would therefore have to be called using either syntax 2
or 3
(where s_
acts like the version provided by numpy).
Other possibilities are to make inclusive
into an object with __getitem__
, permitting syntax 4
, or to apply inclusive
only to the stop
parameter of the slice, as in syntax 5
. Unfortunately I do not believe the latter can be made to work since inclusive
requires knowledge of the step
value.
Of the workable syntaxes (the original l[inclusive_slice(1, 5, 2)]
, plus 2
, 3
and 4
), which would be the best to use? Or is there another, better option?
Final Edit: Thank you all for the replies and comments, this has been very interesting. I have always been a fan of Python's "one way to do it" philosophy, but this issue has been caused by a conflict between Python's "one way" and the "one way" proscribed by the problem domain. I have definitely gained some appreciation for TIMTOWTDI in language design.
For giving the first and highest-voted answer, I award the bounty to L3viathan.
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.