Retrieve length of slice from slice object in Python

Viewed 14073

The title explains itself, how to get 2 out of the object

slice(0,2)

The documentation is somewhat confusing, or it is the wrong one

https://docs.python.org/2/c-api/slice.html

In particular I don't understand what is the meaning of the output of

slice(0,2).indices(0)  # (0, 0, 1)
slice(0,2).indices(10 ** 10)  # (0, 2, 1)

One possible workaround is to slice a list with the slice object

a = [1,2,3,4,5]
len(a[slice(0,2)])  # 2

But this will fail for an arbitrary large slice.

Thanks, I couldn't find an answer In other posts.

7 Answers

If the sequence's length is known

ShadowRanger's answer covered the generic solution, but if (like me) you do know the length of the sequence - here's a simple approach that should handle it like range would (including most edge cases), and without iteration over potentially long sequences.

This is similar to what Markus wrote but handles more edge cases.

from math import ceil

def max_slice_len(s: slice):
    assert s.stop or s.stop == 0, "Must define stop for max slice len!"
    assert s.step != 0, "Step slice cannot be zero"
    
    start = s.start or 0
    stop = s.stop
    step = s.step or 1
    
    delta = (stop - start)
    dsteps = int(ceil(delta / step))
    
    return dsteps if dsteps >= 0 else 0

def slice_len(s: slice, src_len: int):
    stop = min(s.stop, src_len)
    return max_slice_len(slice(s.start, stop, s.step))

Explanation: Assuming we can get the "max length" of a slice without some src_len, we can then build on that by taking the src_len (the length of the list or whatever you want to iterate over) as the stop of the slice if it's smaller than the current stop.

But that still leaves the problem of finding the "max length".


Getting the max length of a slice

The slice construct in python creates a sort of Arithmetic Set.
where a0 == start, d == step, n == len

a formula tells us: a_n = a0+ (n-1)d
[a_n is the nth element of the sequence]
if we treat stop as a_nthen: stop = start + (len - 1) * step.
rearranging we get: len = [(stop-start)/step] + 1.

This nicely takes care of reverse iteration for us just as easily(ie. [10:0:-1]),

But it will usually return a float, as stop might not be a full number of "steps" beyond start. (i.e for [0:10:3], (10-0) / 3 gives us 3.3333...).
using ceil fixes that.

The only issue remaining is negative results ([10:0:1] will give us (0-10)/1 = -10), but the actual "length" should be zero.
The solution is to cut off negative results by returning dsteps if dsteps >= 0 else 0


Tests

import unittest
# import max_slice_len, slice_len

class TestSliceUtil(unittest.TestCase):
    def test_max_len_suite(self):
        simple_test_cases = [
            (slice(0, 10, 1), 10),
            (slice(0, 10, 2), 5),
            (slice(0, 10, 3), 4),
            (slice(0, 10, 10), 1),
            (slice(0, 10, 100), 1),
            (slice(-1, 10, 5), 3),
            (slice(-10, -1, 3), 3),
            (slice(15, 10, 1), 0),
            (slice(0, 10, -1), 0),
            (slice(0, 10, -3), 0),
            (slice(15, 10, -1), 5),
            (slice(10, 0, -1), 10),

            # none replacement (without len)
            (slice(None, 10, 1), 10),
            (slice(0, 10, None), 10),
        ]

        def test_len(s: slice, expected_len: int):
            iter_len = s.stop + 1  # simulate some iterable that is longer than the max_len

            enumerated_idxs = list(range(s.start or 0, s.stop, s.step or 1))
            enumerated_len = len(enumerated_idxs)

            result = slice_len(s, iter_len)
            self.assertEqual(result, expected_len, "Not same as expected!")
            self.assertEqual(result, enumerated_len, "Not same as enumerated!")

        def test_max_len(s: slice, expected_len: int):
            result = max_slice_len(s)
            self.assertEqual(result, expected_len,
                             "Max len was not equal! slice: {}. expected: {}. Actual: {}".format(s, expected_len,
                                                                                                 result))
        for case in simple_test_cases:
            s, expected = case
            with self.subTest("max_len {} -> {}".format(s, expected)):
                test_max_len(s, expected)
            with self.subTest("len vs enumerated {} -> {}".format(s, expected)):
                test_len(s, expected)

The best way to do this is to turn the slice into a range.

A range will have all the functionality you'd need.


def SliceToRange(slc: slice) -> range:
    """Function for Converting a Slice to a Range"""
    DefaultTo = lambda value, default: value if value is not None else default
    return range(DefaultTo(slc.start, 0), slc.stop, DefaultTo(slc.step, 1))

A slice is basically just a namedtuple with the same attributes as a range. What's happening here is you're just transferring the values of the slice to a range object.

The problem is that most of the time, slices have Nones as their values, which ranges won't accept. I fixed this by adding the DefaultTo lambda, which replaces None with the default value given, and putting the respective defaults for start and step.

There is also the problem that slice doesn't have any type checking, so you can do something like slice("dfa", True) or ["hello": type], and it wouldn't care. But you're probably not going to encounter that very often. If you do, the range won't accept it and will raise an error.

a simplified approach with asserts

The length depends on the target object, which is sliced. But one can define a maximum length.

Example

define your maximum length function like this

def slice_len_max(s):
    assert (s.start is not None)
    assert (s.stop is not None)
    step = 1
    if s.step is not None:
        step = s.step
    return max((s.stop - s.start) // step, 1)

and check the output

>>> slice_len_max(slice(0, 10))
10
>>> slice_len_max(slice(0, 10, 2))
5
>>> slice_len_max(slice(0, 10, 3))
3
>>> slice_len_max(slice(0, 10, 10))
1
>>> slice_len_max(slice(0, 10, 100))
1
>>> slice_len_max(slice(3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in slice_len_max
AssertionError

The last call crashes, as the slice has no start attribute defined.

Easiest way to do this is:

>>> s = slice(0,2)
>>> len(range(s.stop)[s])
2
Related