Retrieve length of slice from slice object in Python

后端 未结 5 1213
灰色年华
灰色年华 2021-01-04 03:05

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

<
相关标签:
5条回答
  • 2021-01-04 03:25

    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.

    0 讨论(0)
  • 2021-01-04 03:25

    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)
    
    0 讨论(0)
  • There is no complete answer for this. slice doesn't give you a length because the length of the result is always dependent on the size of the sequence being sliced, a short sequence (including an empty sequence) will produce fewer items, and if the slice is unbounded, then the length will grow in tandem with the length of the sequence; a slice might just go "to end of sequence" by having a start or stop of None.

    For a quick and easy way to compute the length for a sequence of a known length, you just combine .indices with Py3's range (or xrange in Py2, though xrange has limitations on values that Py3 range does not). slice.indices gives you the concrete start, stop and stride values derived when a slice applies to a sequence of a given length, it's basically the values you'd fill in in a C-style for loop that traverses the same indices as the slice:

     for (ssize_t i = start; i < stop; i += stride)
    

    So to calculate the length of a slice when applied to a sequence with 1000 elements, you'd do:

    >>> len(range(*slice(0, 2).indices(1000)))
    2
    >>> len(range(*slice(10, None, 3).indices(1000)))
    330
    

    If you're on Python 2, and your values might exceed what xrange can handle (it's limited to bounds and total length equal to what a ssize_t can hold), you can just do the calculation by hand:

    def slice_len_for(slc, seqlen):
        start, stop, step = slc.indices(seqlen)
        return max(0, (stop - start + (step - (1 if step > 0 else -1))) // step)
    
    >>> slice_len_for(slice(10, None, 3), 1000)
    330
    

    Update: Unfortunately, slice.indices itself won't accept a len for the sequence beyond what a long can hold, so this doesn't gain you anything over using xrange in Py2. Left in place for those interested, but the workaround doesn't workaround anything unless you also perform the work slice does to convert negative values and None to concrete values based on the sequence length. Sigh.

    0 讨论(0)
  • 2021-01-04 03:33

    So it looks like slice.indices(n) returns the arguments to be given to range, to get the item indices which should be reflected in the slice of a sequence of length n (although it's not documented edit: as @ShadowRanger pointed out, it is indeed documented). So the following lines evaluate to the same values:

    # get some list to work on
    my_list = list(range(100))
    
    # slice syntax
    print(my_list[1:15:3])
    # regular item access
    print(my_list[slice(1,15,3)])
    # reinvent list slicing
    print([my_list[i] for i in range(*slice(1,15,3).indices(len(my_list)))])
    

    As you see, the resulting list's length is the same as the length of range(*slice(1,15,3).indices(len(my_list))), which depends on the slice object itself, and the length of the sequence to be sliced. That's why len(range(*slice.indices(n))) will give you the right answer in Python 3. (the range object is a generator, which fortunately has the __len__ function defined, so it can give you the item count, without the need to enumerate and count them.)

    If you work with large numbers in python 2, you can replicate the calculation as @ShadowRanger suggests.

    The original implementation of range.__len__ is the following:

    /* Return number of items in range (lo, hi, step).  step != 0
     * required.  The result always fits in an unsigned long.
     */
    static unsigned long
    get_len_of_range(long lo, long hi, long step)
    {
        /* -------------------------------------------------------------
        If step > 0 and lo >= hi, or step < 0 and lo <= hi, the range is empty.
        Else for step > 0, if n values are in the range, the last one is
        lo + (n-1)*step, which must be <= hi-1.  Rearranging,
        n <= (hi - lo - 1)/step + 1, so taking the floor of the RHS gives
        the proper value.  Since lo < hi in this case, hi-lo-1 >= 0, so
        the RHS is non-negative and so truncation is the same as the
        floor.  Letting M be the largest positive long, the worst case
        for the RHS numerator is hi=M, lo=-M-1, and then
        hi-lo-1 = M-(-M-1)-1 = 2*M.  Therefore unsigned long has enough
        precision to compute the RHS exactly.  The analysis for step < 0
        is similar.
        ---------------------------------------------------------------*/
        assert(step != 0);
        if (step > 0 && lo < hi)
        return 1UL + (hi - 1UL - lo) / step;
        else if (step < 0 && lo > hi)
        return 1UL + (lo - 1UL - hi) / (0UL - step);
        else
        return 0UL;
    }
    

    And slice.indices:

    int
    PySlice_GetIndices(PySliceObject *r, Py_ssize_t length,
                       Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step)
    {
        /* XXX support long ints */
        if (r->step == Py_None) {
            *step = 1;
        } else {
            if (!PyInt_Check(r->step) && !PyLong_Check(r->step)) return -1;
            *step = PyInt_AsSsize_t(r->step);
        }
        if (r->start == Py_None) {
            *start = *step < 0 ? length-1 : 0;
        } else {
            if (!PyInt_Check(r->start) && !PyLong_Check(r->step)) return -1;
            *start = PyInt_AsSsize_t(r->start);
            if (*start < 0) *start += length;
        }
        if (r->stop == Py_None) {
            *stop = *step < 0 ? -1 : length;
        } else {
            if (!PyInt_Check(r->stop) && !PyLong_Check(r->step)) return -1;
            *stop = PyInt_AsSsize_t(r->stop);
            if (*stop < 0) *stop += length;
        }
        if (*stop > length) return -1;
        if (*start >= length) return -1;
        if (*step == 0) return -1;
        return 0;
    }
    

    The sources are from svn

    0 讨论(0)
  • 2021-01-04 03:41
    >>> slice(0,2).__getattribute__('stop')
    2
    >>> slice(0,2).__getattribute__('start')
    0
    
    0 讨论(0)
提交回复
热议问题