Reputation: 2241
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.
Upvotes: 40
Views: 18931
Reputation: 91017
Try
def ifnone(a, b):
return b if a is None else a
class A:
def __getitem__(self, item):
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 …
Edit 2024:
The easiest way is probably
class A:
def __init__(self, length):
self._length = length
def __len__(self):
return self._length
def __getitem__(self, item):
if isinstance(item, slice):
return range(*item.indices(self._length))
else:
return item
Here you have to provide a length when creating A()
which is then used if the slicing needs it.
Upvotes: 20
Reputation: 127
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]
Upvotes: 6
Reputation: 769
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]
Upvotes: 0
Reputation: 28302
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]
Upvotes: 25
Reputation: 393
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...
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
.
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.
return list(range(item.start if item.start else 0, item.stop, item.step if item.step else 1))
... which is also unnecessarily verbose.
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.
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.
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).
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__,
))
Upvotes: 11
Reputation: 1121744
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.
Upvotes: 1
Reputation: 10740
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.
Upvotes: 0