Reputation: 13
When implementing classes in Python that shall be sliceable with standard Python syntax (i.e. negative indices, stepping, etc.) it sometimes can be helpful to convert the slice into a "sane, forward slice" to determine the elements of the slice. How can one write such a function in a concise / elegant form?
With a "sane, forward slice" I mean a slice that is equivalent to the initial slice in the sense that the resulting elements are the same, but has a positive step, no negative indices, and also no indices that are larger than the object's length.
Examples (assuming an array length of 10):
slice(None, 4,None) -> slice( 0, 4, 1)
slice( -7, 12, 1) -> slice( 3,10, 1)
slice(None,None, -1) -> slice( 0,10, 1)
slice( 7, 3, -2) -> slice( 5, 8, 2)
slice( 9, 1, -3) -> slice( 3,10, 3)
It is not extremely difficult to write a functions that performs such a conversion, but I was not able to write it concise. Especially determining the start index when converting a "backwards slice" into an equivalent "forward slice" seems to be quite cumbersome.
Working example:
def to_sane_slice(s, N):
step = s.step if s.step is not None else 1
if step == 0:
ValueError('Slice step cannot be 0!')
# get first index
first = s.start
if first is None:
first = 0 if step > 0 else N-1
elif first < 0:
first = N+first
if first < 0:
if step < 0:
return slice(0,0,1)
first = 0
elif first >= N:
if step > 0:
return slice(0,0,1)
first = N-1
# get stop index
stop = s.stop
if stop is None:
stop = N if step > 0 else -1
elif stop < 0:
stop = max(-1, N+stop)
# check for etmpy slices
if (stop-first)*step <= 0:
return slice(0,0,1)
if step > 0:
return slice(first, min(stop,N), step)
elif step == -1:
return slice(stop+1, first+1, -step)
else:
# calculate the new start -- does not have to be next to old stop, since
# the stepping might lead elsewhere
step = -step
dist = first - max(stop,-1)
last = first - dist / step * step
if dist % step == 0:
last += step
return slice(last, first+1, step)
.
.
.
Edit (final function):
With the use of slice.indices()
to which @doublep kindly pointed me it becomes:
def to_sane_slice(s, N):
# get rid of None's, overly large indices, and negative indices (except -1 for
# backward slices that go down to first element)
start, stop, step = s.indices(N)
# get number of steps & remaining
n, r = divmod(stop - start, step)
if n < 0 or (n==0 and r==0):
return slice(0,0,1)
if r != 0: # its a "stop" index, not an last index
n += 1
if step < 0:
start, stop, step = start+(n-1)*step, start-step, -step
else: # step > 0, step == 0 is not allowed
stop = start+n*step
stop = min(stop, N)
return slice(start, stop, step)
Upvotes: 1
Views: 540
Reputation:
Use slice.indices()
method.
S.indices(len) -> (start, stop, stride)
Assuming a sequence of length len, calculate the start and stop indices, and the stride length of the extended slice described by S. Out of bounds indices are clipped in a manner consistent with the handling of normal slices.
Upvotes: 3