Reputation: 4007
The Python Documentation states that
slice indices are silently truncated to fall in the allowed range
and therefor no IndexErrors
are risen when slicing a list, regardless what start
or stop
parameters are used:
>>> egg = [1, "foo", list()]
>>> egg[5:10]
[]
Since the list egg
does not contain any indices greater then 2
, a egg[5]
or egg[10]
call would raise an IndexError
:
>> egg[5]
Traceback (most recent call last):
IndexError: list index out of range
The question is now, how can we raise an IndexError
, when both given slice indices are out of range?
Upvotes: 8
Views: 21043
Reputation: 1123440
There is no silver bullet here; you'll have to test both boundaries:
def slice_out_of_bounds(sequence, start=None, stop=None, step=1):
length = len(sequence)
if start is None: # default depends on step direction
start = length - 1 if step < 0 else 0
elif start < 0: # relative to length
start += length
if stop is None: # default depends on step direction
stop = -1 if step < 0 else length
elif stop < 0: # relative to length
stop += length
# stop can range [0 .. length] for positive steps or
# [-1 .. (length - 1)] for negative steps (both bounds inclusive).
# adjust stop for negative steps to make the bounds check easier.
if step < 0:
stop += 1
if not (0 <= start < length and 0 <= stop <= length):
raise IndexError("Container slice indices out of bounds")
Note the way we need to handle stop
specially. The end value in slicing is exclusive, so is allowed to range up to length
. For negative strides, valid stop
values lie in the range [-1 .. length)
(or [-1 .. (length - 1)]
with inclusive bounds), but adding 1 to the stop
value makes it possible to re-use the same 0 <= stop <= length
test.
If you create your own container class with an __getitem__()
method, then slicing the container gives you a slice()
instance. Don't use the slice.indices()
method in that case, it adjusts the bounds for you rather than raise an IndexError
. Instead, us the .start
, .stop
and .step
attributes. Each of those can be None
, including .step
, so make sure to handle that too.
Here's a __getitem__
version of the above, with a few added checks for other edge cases to replicate how list
slicing works:
class SomeContainer:
...
def __getitem__(self, key):
length = len(self)
if isinstance(key, int) and not (0 <= key < length):
raise IndexError("Container index out of range")
elif isinstance(key, slice):
if key.step == 0:
raise ValueError("Slice step cannot be zero")
start, stop, step = key.start, key.stop, key.step or 1
if start is None: # default depends on step direction
start = length - 1 if step < 0 else 0
elif start < 0: # relative to length
start += length
if end is None: # default depends on step direction
end = -1 if step < 0 else length
elif end < 0: # relative to length
end += length
# end can range [0 .. length] for positive steps or
# [-1 .. (length - 1)] for negative steps (both bounds inclusive).
# adjust end for negitive steps to make the bounds check easier.
# Don't do this if you also wanted to calculate the slice length!
if step < 0:
end += 1
if not (0 <= start < length and 0 <= end <= length):
raise IndexError("Container slice indices out of bounds")
else:
raise TypeError(f"list indices must be integers or slices, not {type(key)}")
Upvotes: 3
Reputation: 471
In Python 2 you can override __getslice__
method by this way:
class MyList(list):
def __getslice__(self, i, j):
len_ = len(self)
if i > len_ or j > len_:
raise IndexError('list index out of range')
return super(MyList, self).__getslice__(i, j)
Then use your class instead of list
:
>>> egg = [1, "foo", list()]
>>> egg = MyList(egg)
>>> egg[5:10]
Traceback (most recent call last):
IndexError: list index out of range
Upvotes: 4