Monday, March 7, 2011

Extended slice that goes to beginning of sequence with negative stride

Bear with me while I explain my question. Skip down to the bold heading if you already understand extended slice list indexing.

In python, you can index lists using slice notation. Here's an example:

>>> A = list(range(10))
>>> A[0:5]
[0, 1, 2, 3, 4]

You can also include a stride, which acts like a "step":

>>> A[0:5:2]
[0, 2, 4]

The stride is also allowed to be negative, meaning the elements are retrieved in reverse order:

>>> A[5:0:-1]
[5, 4, 3, 2, 1]

But wait! I wanted to see [4, 3, 2, 1, 0]. Oh, I see, I need to decrement the start and end indices:

>>> A[4:-1:-1]
[]

What happened? It's interpreting -1 as being at the end of the array, not the beginning. I know you can achieve this as follows:

>>> A[4::-1]
[4, 3, 2, 1, 0]

But you can't use this in all cases. For example, in a method that's been passed indices.

My question is:

Is there any good pythonic way of using extended slices with negative strides and explicit start and end indices that include the first element of a sequence?

This is what I've come up with so far, but it seems unsatisfying.

>>> A[0:5][::-1]
[4, 3, 2, 1, 0]
From stackoverflow
  • a[4::-1]
    

    Example:

    Python 2.6 (r26:66714, Dec  4 2008, 11:34:15) 
    [GCC 4.0.1 (Apple Inc. build 5488)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>> a = list(range(10))
    >>> a
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    >>> a[4:0:-1]
    [4, 3, 2, 1]
    >>> a[4::-1]
    [4, 3, 2, 1, 0]
    >>>
    

    The reason is that the second term is interpreted as "while not index ==". Leaving it out is "while index in range".

    recursive : I mentioned that in the question. But you can't use that if you are storing your indices in variables for example.
    Charlie Martin : Oh. I knew I needed a nap.
    recursive : Yeah, I knew that too. I mean me. I need a nap.
  • I believe that the following doesn't satisfy you:

    def getReversedList(aList, end, start, step):
        if step < 0 and start == 0:
             return aList[end::step]
        return aList[end:start:step]
    

    or does it? :-)

    recursive : I had considered that, requiring two separate cases seems like it should be unnecessary. But you've given me an idea...
  • Ok, I think this is probably as good as I will get it. Thanks to Abgan for sparking the idea. This relies on the fact that None in a slice is treated as if it were a missing parameter. Anyone got anything better?

    def getReversedList(aList, end, start, step):
        return aList[end:start if start!=-1 else None:step]
    

    edit: check for start==-1, not 0

    This is still not ideal, because you're clobbering the usual behavior of -1. It seems the problem here is two overlapping definitions of what's supposed to happen. Whoever wins takes away otherwise valid invocations looking for the other intention.

    Abgan : Hah, haven't thought about that. Nice solution, worth remembering :-)
    J.F. Sebastian : You could replace `None` by `-len(a) + start`. See http://stackoverflow.com/questions/399067/extended-slice-that-goes-to-beginning-of-sequence-with-negative-stride#400118
  • [ A[b] for b in range(end,start,stride) ]
    

    Slower, however you can use negative indices, so this should work:

    [ A[b] for b in range(9, -1, -1) ]
    

    I realize this isn't using slices, but thought I'd offer the solution anyway if using slices specifically for getting the result isn't a priority.

  • But you can't use that if you are storing your indices in variables for example.

    Is this satisfactory?

    >>> a = range(10)
    >>> start = 0
    >>> end = 4
    >>> a[4:start-1 if start > 0 else None:-1]
    [4, 3, 2, 1, 0]
    
  • It is error-prone to change the semantics of start and stop. Use None or -(len(a) + 1) instead of 0 or -1. The semantics is not arbitrary. See Edsger W. Dijkstra's article "Why numbering should start at zero".

    >>> a = range(10)
    >>> start, stop, step = 4, None, -1
    

    Or

    >>> start, stop, step = 4, -(len(a) + 1), -1
    >>> a[start:stop:step]
    [4, 3, 2, 1, 0]
    

    Or

    >>> s = slice(start, stop, step)
    >>> a[s]
    [4, 3, 2, 1, 0]
    
  • As you say very few people fully understand everything that you can do with extended slicing, so unless you really need the extra performance I'd do it the "obvious" way:

    rev_subset = reversed(data[start:stop])

0 comments:

Post a Comment