38

I'm using Python 3.3. I want to get a slice object and use it to make a new range object.

It goes something like that:

>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step))

>>> a = A()
>>> a[1:5:2] # works fine
[1, 3]
>>> a[1:5] # won't work :(
Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    a[1:5] # won't work :(
  File "<pyshell#9>", line 4, in __getitem__
    return list(range(item.start, item.stop, item.step))
TypeError: 'NoneType' object cannot be interpreted as an integer

Well, the problem is obvious here - range doesn't accept None as a value:

>>> range(1, 5, None)
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    range(1, 5, None)
TypeError: 'NoneType' object cannot be interpreted as an integer

But what is not obvious (to me) is the solution. How will I call range so it will work in every case? I'm searching for a nice pythonic way to do it.

Georgy
  • 12,464
  • 7
  • 65
  • 73
slallum
  • 2,161
  • 4
  • 24
  • 24
  • 3
    Does it help that in Python 3 you can slice a `range` object to get a new `range` object? – asmeurer Jan 31 '15 at 19:12
  • 4
    For those looking for a simple and more general answer, from [Labrys Knossos](https://stackoverflow.com/users/7510791/labrys-knossos)'s answer below: `range(item.start or 0, item.stop or len(self), item.step or 1)`. Replace `len(self)` as needed if not in a class with `__len__` defined. – lehiester Dec 21 '18 at 18:31

7 Answers7

25

There's an easier way to do this (at least in 3.4, I don't have 3.3 at the moment, and I don't see it in the changelog).

Assuming your class already has a known length you can just slice a range of that size:

>>> range(10)[1:5:2]
range(1, 5, 2)
>>> list(range(10)[1:5:2])
[1, 3]

If you don't know the length a priori you'll have to do:

>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.stop)[item])
>>> a = A()
>>> a[1:5:2]
[1, 3]
>>> a[1:5]
[1, 2, 3, 4]
CrazyCasta
  • 26,917
  • 4
  • 45
  • 72
  • 2
    This is fantastic. One thing I want to note is that `list(range(10)[0:20])` does not throw an error, which could be problematic! – taper Jun 09 '18 at 21:56
  • 3
    Fails in the case of `slice(0, None, 1)` with `TypeError: 'NoneType' object cannot be interpreted as an integer`. You should check for None and hanle the case. glglgl proposes using `itertools.count()`. – Mathieu CAROFF Dec 15 '18 at 17:25
  • @MathieuCAROFF Dude, the original person asking the question said he wanted a range, not some other iterable. He never expressed a wish for the ability to handle slices without an endpoint. If that's what you want, then go ask your own question instead of asking for it in a comment. – CrazyCasta Jan 04 '19 at 16:57
  • Sorry, it's just that it is in my oppinion the prettiest solution, and this is the only downside. – Mathieu CAROFF Jan 06 '19 at 19:22
  • Also, all other answers are so more much wrong (as of 2019-01-06). – Mathieu CAROFF Jan 06 '19 at 19:25
  • A really nice answer :) – Vaidøtas I. Jan 07 '21 at 21:35
18

Try

class A:
    def __getitem__(self, item):
        ifnone = lambda a, b: b if a is None else a
        if isinstance(item, slice):
            if item.stop is None:
                # do something with itertools.count()
            else:
                return list(range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)))
        else:
            return item

This will reinterpret .start and .step appropriately if they are None.


Another option could be the .indices() method of a slice. It is called with the number of entries and reinterprets None to the appropriate values and wraps negative values around the given length parameter:

>>> a=slice(None, None, None)
>>> a.indices(1)
(0, 1, 1)
>>> a.indices(10)
(0, 10, 1)
>>> a=slice(None, -5, None)
>>> a.indices(100)
(0, 95, 1)

It depends what you intend to do with negative indices...

glglgl
  • 89,107
  • 13
  • 149
  • 217
  • 2
    Can't you also just do: ```python3 list(range(item.start or 0, item.stop, item.step or 1)) ``` – LoveToCode May 23 '20 at 18:07
  • 2
    @LoveToCode I'd assume it's slightly different and maybe bad practice. ifnone test specifically the value none, whereas your way replace anything that is interpreted as false. For example, if you put 0 in the step, with ifnone, it will be left untouched and range raise an error as probably should. With item.step or 1, it will replace 0 as item.step by 1 – 4xel Jul 20 '20 at 07:45
  • slice's `.indices()` method is exactly what I was looking for. Thanks! – Arty Aug 30 '21 at 14:22
  • If `item.stop` is None why shouldn't set stop to the length ? – Ger Apr 27 '23 at 16:37
11

The problem:

A slice consists of start, stop, and step parameters and can be created with either slice notation or using the slice built-in. Any (or all) of the start, stop, and step parameters can be None.

# valid
sliceable[None:None:None]

# also valid
cut = slice(None, None, None)
sliceable[cut]

However, as pointed out in the original question, the range function does not accept None arguments. You can get around this in various ways...

The solutions

With conditional logic:

if item.start None:
    return list(range(item.start, item.stop))
return list(range(item.start, item.stop, item.step))

...which can get unnecessarily complex since any or all of the parameters may be None.

With conditional variables:

start = item.start if item.start is None else 0
step = item.step if item.step is None else 1
return list(range(item.start, item.stop, item.step))

... which is explicit, but a little verbose.

With conditionals directly in the statement:

return list(range(item.start if item.start else 0, item.stop, item.step if item.step else 1))

... which is also unnecessarily verbose.

With a function or lambda statement:

ifnone = lambda a, b: b if a is None else a
range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)

...which can be difficult to understand.

With 'or':

return list(range(item.start or 0, item.stop or len(self), item.step or 1))

I find using or to assign sensible default values the simplest. It's explicit, simple, clear, and concise.

Rounding out the implementation

To complete the implementation you should also handle integer indexes (int, long, etc) by checking isinstance(item, numbers.Integral) (see int vs numbers.Integral).

Define __len__ to allow for using len(self) for a default stop value.

Finally raise an appropriate TypeError for invalid indexes (e.g. strings, etc).

TL;DR;

class A:
    def __len__(self):
        return 0

    def __getitem__(self, item):
        if isinstance(item, numbers.Integral):  # item is an integer
            return item
        if isinstance(item, slice):  # item is a slice
            return list(range(item.start or 0, item.stop or len(self), item.step or 1))
        else:  # invalid index type
            raise TypeError('{cls} indices must be integers or slices, not {idx}'.format(
                cls=type(self).__name__,
                idx=type(item).__name__,
            ))
Community
  • 1
  • 1
Labrys Knossos
  • 393
  • 2
  • 7
  • 1
    /!\ *or* in case item.step is 0. – Mathieu CAROFF Jan 06 '19 at 19:24
  • 1
    "[or] is explicit". Not so much, a handful of values are implicitely interpreted as False, which can be problematic if you really only want to test None. In that example, having the slice step equal to 0 should probably raise an error and it doesn't. Worse even, you can't use 0 as a value for item.stop, it'll get overwritten by length self. – 4xel Jul 20 '20 at 07:57
  • Great answer thanks. It doesn't work with negative slices though. does it? – Carol Eisen Mar 15 '22 at 17:38
  • The "With conditional variables:" answer is wrong. It should be `item.start if item.start is not None else 0` – Ger Apr 27 '23 at 16:01
5

All of the other answers here are completely missing it.

You simply cannot convert a slice into a range in the general case.

There isn't enough information. You need to know the length of the list (or other sequence type) that you are trying to slice.

Once you have that, you can create a range easily in Python 3 using slice.indices()

Following the example you provided:

class A:
    def __init__(self, mylist):
        self.mylist = mylist

    def __getitem__(self, item):
        if isinstance(item, slice):
            mylen = len(self.mylist)
            return list(range(*item.indices(mylen)))

mylist = [1, 2, 'abc', 'def', 3, 4, None, -1]
a = A(mylist)
a[1:5]  # produces [1, 2, 3, 4]
Mike
  • 105
  • 2
  • 5
1

I would special-case the item.step is None branch:

def __getitem__(self, item):
    if isinstance(item, slice):
        if item.step is None:
            return list(range(item.start, item.stop))
        return list(range(item.start, item.stop, item.step))

and you'll handle ranges that need to count down correctly.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    That's cool, but `item.start` can be `None` too, and I think that too many `if`s in the code can make it look pretty ugly, so i used in the `indices` method that suggested in another answer. – slallum Dec 13 '12 at 08:28
  • @slallum: The `start = item.start if item.start is not None else 0` conditional expression might work well enough for that (then use `range(start, item.stop ..`). – Martijn Pieters Dec 13 '12 at 08:49
0

In your last example a[1:5], item.step == None and you are trying to do range(1, 5, None), which of course causes the error. Fast way to fix:

class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step if item.step else 1)) #Changed line!

But it is just to show you your problem. It is not the best approach.

alexvassel
  • 10,600
  • 2
  • 29
  • 31
  • 1
    What if `stop` is lower than `start`? Then step should be `-1`! – Martijn Pieters Dec 13 '12 at 08:11
  • 1
    @MartijnPieters - then it wouldn't return anything, as expected, like slice works in lists. – slallum Dec 13 '12 at 08:35
  • @slallum: but this is a range. Lists may *interpret* the slice as an empty list, but in a range a stop lower than start results in an automatic `-1` step. – Martijn Pieters Dec 13 '12 at 08:47
  • 1
    @MartijnPieters - Are you sure? I have python3.3 and python2.7 and in both of them `list(range(7,2))` returns an empty list.. – slallum Dec 13 '12 at 08:49
  • 2
    @slallum: golly, not sure how I got the idea it'd automatically deal with the right `step` there then. Shows you how often I used a negative range.. – Martijn Pieters Dec 13 '12 at 08:58
0

What about something like this?

>>> class A:
def __getitem__(self, item):
    if isinstance(item, slice):
        return list(range(item.start, item.stop, item.step if item.step else 1))

>>> a = A()
>>> a[1:5:2] # works fine
[1, 3]
>>> a[1:5] # works as well :)
[1, 2, 3, 4]
Wolf Elkan
  • 749
  • 6
  • 2