How should I handle inclusive ranges in Python?

后端 未结 12 2214
天涯浪人
天涯浪人 2020-12-14 05:56

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

相关标签:
12条回答
  • 2020-12-14 06:01

    Maybe the inclusive package proves helpful.

    0 讨论(0)
  • 2020-12-14 06:04

    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.        ])
    
    0 讨论(0)
  • 2020-12-14 06:05

    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
    
    0 讨论(0)
  • 2020-12-14 06:11

    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.)

    0 讨论(0)
  • 2020-12-14 06:13

    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.

    0 讨论(0)
  • 2020-12-14 06:14

    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.

    0 讨论(0)
提交回复
热议问题