3

I'm looking for a general method on how to reverse a slice in Python. I read this comprehensive post, which has several nice explanations about how slicing works: Understanding Python's slice notation

Yet I cannot figure out a generalized rule on how to calculate a reversed slice which addresses exactly the same elements in reverse order. I was actually surprised not to find a builtin method doing this.

What I'm looking for is a method reversed_slice that works like this with arbitrary start, stop and step values including negative values:

>>> import numpy as np
>>> a = np.arange(30)
>>> s = np.s_[10:20:2]
>>> a[s]
array([10, 12, 14, 16, 18])
>>> a[reversed_slice(s,len(a))]
array([18, 16, 14, 12, 10])

What I've tried but doesn't work is this:

def reversed_slice(slice_, len_):
    """
    Reverses a slice (selection in array of length len_), 
    addressing the same elements in reverse order.
    """
    assert isinstance(slice_, slice)
    instart, instop, instep = slice_.indices(len_)
    if instep > 0:
        start, stop, step = instop - 1, instart - 1, -instep
    else:
        start, stop, step = instop + 1, instart + 1, -instep
    return slice(start, stop, step)

This works fine for step of 1 and when the last addressed element coincides with stop-1. For other cases it does not:

>>> import numpy as np
>>> a = np.arange(30)
>>> s = np.s_[10:20:2]
>>> a[s]
array([10, 12, 14, 16, 18])
>>> a[reversed_slice(s,len(a))]
array([19, 17, 15, 13, 11])

So it seems like I'm missing some relation like (stop - start) % step. Any help on how to write a general method is greatly appreciated.

Notes:

  • I do know that there a other possibilities to get a sequence with the same elements reversed, like calling reversed(a[s]). This is not an option here, as I need to reverse the slice itself. The reason is that I work on h5py datasets which do not allow negative step values in slices.

  • An easy but not very elegant way would be the use of coordinate lists, i.e. a[list(reversed(range(*s.indices(len(a)))))]. This is also not an option due to the h5py requirement that indices in the list must be given in increasing order.

eaglesear
  • 480
  • 4
  • 12

5 Answers5

3

You can specify negative values for step.

>>> s = np.s_[20-2:10-2:-2]
>>> a[s]
array([18, 16, 14, 12, 10])

So you can build the reversed_slice function as follows

>>> def reversed_slice(s):
...     """
...     Reverses a slice 
...     """
...     m = (s.stop-s.start) % s.step or s.step
...     return slice(s.stop-m, s.start-m, -s.step)
... 
>>> a = np.arange(30)
>>> s = np.s_[10:20:2]
>>> a[reversed_slice(s)]
array([18, 16, 14, 12, 10])
>>> 
>>> a[reversed_slice(reversed_slice(s))]
array([10, 12, 14, 16, 18])
>>> 
Sunitha
  • 11,777
  • 2
  • 20
  • 23
  • Thanks for your answer. The `-2` in start and stop is the core of it. It works in this case, where `(20-10)%2 == 0`. Yet in other cases it doesn't: `a[11:20:2]` will give `array([11, 13, 15, 17, 19])` and `a[20-2:11-2:-2]` will give `array([18, 16, 14, 12, 10])`. – eaglesear Jul 06 '18 at 12:04
  • @eaglesear Thanks for pointing it out. I have updated the answer – Sunitha Jul 06 '18 at 12:16
  • @Sunitha Thanks again. This still doesn't check out. With your updated version `reversed_slice(np.s_[10:20:2])` gives `slice(20, 10, -2)` which gives `array([20, 18, 16, 14, 12])`. – eaglesear Jul 06 '18 at 12:33
  • @eaglesear... Aahhh... Updated again – Sunitha Jul 06 '18 at 12:41
  • @Sunitha Thanks, this is a big step further, but I still found a case where it gives a wrong result: For `s = np.s_[-2:10:-2]`, `a[s]` yields `array([28, 26, 24, 22, 20, 18, 16, 14, 12])`, but `reversed_slice(s)` yields `slice(12, 0, 2)` and and empty array. – eaglesear Jul 06 '18 at 13:04
1

I just wanted to use the answer to this question but while testing found that there are still some cases that would silently give the wrong result -

The following definition of the reversed_slice function developed from the other answers seems to cover those cases correctly -

def reversed_slice(s, len_):
    """
    Reverses a slice selection on a sequence of length len_, 
    addressing the same elements in reverse order.
    """
    assert isinstance(s, slice)
    instart, instop, instep = s.indices(len_)

    if (instop < instart and instep > 0) or (instop > instart and instep < 0) \
      or (instop == 0 and instart == 0) :
        return slice(0,0,None)

    overstep = abs(instop-instart) % abs(instep)

    if overstep == 0 :
        overstep = abs(instep)

    if instep > 0:
        start = instop - overstep
        stop = instart - 1
    else :
        start = instop + overstep
        stop = instart + 1

    if stop < 0 :
        stop = None

    return slice(start, stop, -instep)
Warwick
  • 26
  • 2
  • Oh, interesting... Can you give me an example for a slice that works with yours, but not with mine? I'd like to see where the problem is. – eaglesear Mar 18 '19 at 11:50
  • In initial testing of instart, instop and instep as returned by s.indices. Using as a test list [x for x in range(0,10,1)]. With no testing of the values returned by s.indices or testing (instart==0) or testing (instart==0 and instop==0) slice[5:0:1] fails. Testing only (instop==0) slice[5:0:-1] fails. The problems I found were all cases where the result should be [] but wasn't. – Warwick Mar 19 '19 at 15:46
  • Thank you! I tested it and you're right. I accepted your answer as this is the one containing no breaking cases that I know of for now. – eaglesear Mar 21 '19 at 09:59
0

You made some mistakes with start/stop math:

overstep = abs(instop-instart) % abs(instep)
if overstep == 0 :
    overstep = abs(instep)

if instep > 0:
    start = instop - overstep
    stop = instart - 1
else :
    start = instop + overstep
    stop = instart + 1

step = -instep

Once you put this in your code, everything should work just fine.

lenik
  • 23,228
  • 4
  • 34
  • 43
  • Thanks for your answer. This is a step further, but still gives wrong results in some cases. With your answer: `reversed_slice(numpy.s_[20:10:-2],len(a))` gives `slice(8, 21, 2)` which corresponds to `array([ 8, 10, 12, 14, 16, 18, 20])`. – eaglesear Jul 06 '18 at 12:42
  • @eaglesear you might have missed something when copying code, (20,10,-2) gives an answer (12,21,2) -- seems right to me. I have checked a few combinations, basically, this `reverse_indices()` function gives the same result when applied twice, like `reverse_indices( reverse_indices( ...some values... ))` will give back those `... some values ...` – lenik Jul 06 '18 at 13:26
  • @eaglesear well.... there are cases, when `(20,10,-2) -> (12,21,2) -> (20,11,-2) -> (12,21,2)` -- here `(20,11,2)` is just the same as `(20,10,2)`, though looks a bit different, but generates the same slice. – lenik Jul 06 '18 at 13:32
  • yeah you're right in this case. Sorry. It's other cases that break. Examples: `(10, 21, 1) -> (21, 9, -1)` , `(0, 15, 2) -> (14, -1, -2)` , `(16, None, 2) -> (30, 15, -2)` – eaglesear Jul 06 '18 at 14:52
  • @eaglesear ok, i see the problem, here's the fixed version, I don't think you'll be able to find any faulty combination for this one =) – lenik Jul 06 '18 at 15:50
  • Sorry, but I found one :) `(0, 10, 1)` -> `(9, -1, -1)`. This issue happens on the boundaries of the sequence. I excluded it separately, see my answer. Thanks for all your help! – eaglesear Jul 06 '18 at 16:31
  • @eaglesear (0,10,1) and (9,-1,-1) is the same sequence [0,1,2...7,8,9] backward and forward, isn't it? – lenik Jul 06 '18 at 17:36
  • No, it's not, because the stop -1 addresses the **last** element. Because of this `(9,-1,-1)` yields an empty array. The only way to go down to zero with negative step is a stop value of `None`, so the correct slice would be `(9,None,-1)`. – eaglesear Jul 09 '18 at 08:17
  • @eaglesear this is a corner case, easy to fix with a simple `if` statement – lenik Jul 09 '18 at 08:46
0

So far I also didn't find any builtin, but what worked even for negative steps:

def invert_slice(start, stop, step=1):
    distance = stop - start
    step_distance = distance // step
    expected_distance = step_distance * step

    if expected_distance != distance:
        expected_distance += step

    new_start = start + expected_distance - step
    new_stop = start - step

    return slice(new_start, new_stop, -step)

This gives you

>>> import numpy as np
>>> a = np.arange(30)
>>> s = np.s_[24:10:-1]
>>> expected = list(reversed(a[s]))

[18, 16, 14, 12, 10]

>>> # resulting slice
>>> result = invert_slice(s.start, s.stop, s.step)

slice(18, 8, -2)

>>> assert np.allclose(expected, a[result]), "Invalid Slice %s" % result
>>> a[result]

[18 16 14 12 10] They are equal ;-)

tlausch
  • 1,000
  • 1
  • 7
  • 5
  • Thanks! This works for quite a number of cases, even for negative starts and stops. As far as tested, it gives the same slices as Sunitha's answer. So the case where it breaks is also this one: For `s = np.s_[-2:10:-2]`, `a[s]` yields `array([28, 26, 24, 22, 20, 18, 16, 14, 12])`, but `reversed_slice(s)` yields `slice(12, 0, 2)` and and empty array. – eaglesear Jul 06 '18 at 13:13
  • :D For negative start and stop you will have to create a new slicing class, since they depend on array length. When subclassing overwrite the indices(length) method to return expected ranges. – tlausch Jul 06 '18 at 16:15
0

I found a working solution based on Sunitha's answer (EDIT: Also implemented Warwick's answer):

def reversed_slice(s, len_):
    """
    Reverses a slice selection on a sequence of length len_, 
    addressing the same elements in reverse order.
    """
    assert isinstance(s, slice)
    instart, instop, instep = s.indices(len_)

    if (instop < instart and instep > 0) or (instop > instart and instep < 0) \
            or (instop == 0 and instart == 0):
        return slice(0, 0, None)

    m = (instop - instart) % instep or instep

    if instep > 0 and instart - m < 0:
        outstop = None
    else:
        outstop = instart - m
    if instep < 0 and instop - m > len_:
        outstart = None
    else:
        outstart = instop - m

    return slice(outstart, outstop, -instep)

It uses the slice.indices(len) method to expand the functionality so it can also be used with None entries in the slice, e.g. with [::-1]. Problems at the boundaries are avoided with the if clauses.

This solution only works when providing the length of the sequence to address. I don't think there's a way to get around this. If there is, or if there is an easier way, I'm open for suggestions!

eaglesear
  • 480
  • 4
  • 12