[Python-ideas] Where did we go wrong with negative stride?

Nick Coghlan ncoghlan at gmail.com
Tue Oct 29 14:32:20 CET 2013


On 29 October 2013 20:23, Paul Moore <p.f.moore at gmail.com> wrote:
> On 28 October 2013 22:41, Guido van Rossum <guido at python.org> wrote:
>> I'm not sure I like new syntax. We'd still have to find a way to represent
>> this with slice() and also with range().
>
> It's a shame there isn't an indexing syntax where you can supply an
> iterator that produces the set of indexes you want and returns the
> subsequence - then we could experiment with alternative semantics in
> user code.
>
> So, for example (silly example, because I don't have the time right
> now to define an indexing function that matches any of the proposed
> solutions):
>
>     >>> def PrimeSlice():
>     >>>    yield 2
>     >>>    yield 3
>     >>>    yield 5
>     >>>    yield 7
>
>     >>> 'abcdefgh'[[PrimeSlice()]]
>     'bceg'
>
> But of course, to make this user-definable needs new syntax in the
> first place :-(

Tangent: I thought of a list comprehension based syntax for that a
while ago, but decided it wasn't particularly interesting since it's
too hard to provide sensible fallback behaviour for existing
containers: 'abcdefgh'[x for x in PrimeSlice()]


Back on the topic of slicing with negative steps, I did some
experimentation to see what could be done in current Python using a
callable that produces the appropriate slice objects, and it turns out
you can create a quite usable "rslice" callable, provided you pass in
the length when dealing with mismatched signs on the indices (that's
the advantage of the "[i:j][::-k]" interpretation of the reversed
slice - if you want to interpret it as "[i:j:k][::-1]" as I suggested
previously, I believe you would need to know the length of the
sequence in all cases):

def rslice(*slice_args, length=None):
    """For args (i, j, k) computes a slice equivalent to [i:j][::-k]
(which is not the same as [i:j:-k]!)"""
    forward = slice(*slice_args) # Easiest way to emulate slice arg parsing!
    # Always negate the step
    step = -forward.step
    # Given slice args are closed on the left, open on the right,
    # simply negating the step and swapping left and right will introduce
    # an off-by-one error, so we need to adjust the endpoints to account
    # for the open/closed change
    left = forward.start
    right = forward.stop
    # Check for an empty slice before tinkering with offsets
    if left is not None and right is not None:
        if (left >= 0) != (right >= 0):
            if length is None:
                raise ValueError("Must supply length for indices of
different signs")
            if left < 0:
                left += length
            else:
                right += length
        if left >= right:
            return slice(0, 0, 1)
    stop = left
    if stop is not None:
        # Closed on the left -> open stop value in the reversed slice
        if stop:
            stop -= 1
        else:
            # Converting a start offset of 0 to an end offset of -1 does
            # the wrong thing - need to convert it to None instead
            stop = None
    start = right
    if start is not None:
        # Open on the right -> closed start value in the reversed slice
        if start:
            start -= 1
        else:
            # Converting a stop offset of 0 to a start offset of -1 does
            # the wrong thing - need to convert it to None instead
            start = None
    return slice(start, stop, step)

# Test case
data = range(10)
for i in range(-10, 11):
    for j in range(-10, 11):
        for k in range(1, 11):
            expected = data[i:j][::-k]
            actual = data[rslice(i, j, k, length=len(data))]
            if actual != expected:
                print((i, j, k), actual, expected)

So, at this point, I still quite like the idea of adding a
"reverse=True" keyword only arg to slice and range (with the semantics
of rslice above), and then revisit the idea of offering syntax for it
in Python 3.5. Since slices are objects, they could store the
"reverse" flag internally, and only apply it when the indices() method
(or the C API equivalent) is called to convert the abstract indices to
real ones for the cases where the info is needed - otherwise they'd do
the calculation above to create a suitable "forward" definition for
maximum compatibility with existing container implementations.

A separate keyword only arg like "addlen=True" would also make it
possible to turn off the negative indexing support in a slice object's
indices() method, and switch it to clamping to zero instead.

An alternative to both of those ideas would be to eliminate the
restriction on subclassing slice objects in CPython, then you could
implement slice objects with different indices() method behaviour
(they'd still have to produce a start, stop, step triple though, so
they wouldn't offer the full generality Paul was describing).

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-ideas mailing list