Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I handle inclusive ranges in Python?

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.

like image 581
user200783 Avatar asked Apr 12 '15 23:04

user200783


People also ask

Is range () inclusive in Python?

Python range is inclusive because it starts with the first argument of the range() method, but it does not end with the second argument of the range() method; it ends with the end – 1 index.

How do you represent an inclusive range?

In the text form of a range, an inclusive lower bound is represented by “ [ ” while an exclusive lower bound is represented by “ ( ”. Likewise, an inclusive upper bound is represented by “ ] ”, while an exclusive upper bound is represented by “ ) ”.

Does range in Python include 0?

All parameters can be positive or negative. range() (and Python in general) is 0-index based, meaning list indexes start at 0, not 1. eg. The syntax to access the first element of a list is mylist[0] .

What does inclusive in Python mean?

… the word “inclusive” means that the value 50 should be included in the range. So, the ending value of the range is set to 51 (exclusive) in the Python statement, meaning that 51 is not included in the range.


2 Answers

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.

like image 67
L3viathan Avatar answered Oct 14 '22 16:10

L3viathan


Since in Python, the ending index is always exclusive, it's worth considering to always use the "Python-convention" values internally. This way, you will save yourself from mixing up the two in your code.

Only ever deal with the "external representation" through dedicated conversion subroutines:

def text2range(text):     m = re.match(r"from (\d+) to (\d+)",text)     start,end = int(m.groups(1)),int(m.groups(2))+1  def range2text(start,end):     print "from %d to %d"%(start,end-1) 

Alternatively, you can mark the variables holding the "unusual" representation with the true Hungarian notation.

like image 38
ivan_pozdeev Avatar answered Oct 14 '22 15:10

ivan_pozdeev