从 Python 中的切片 object 中检索切片的长度

Retrieve length of slice from slice object in Python

标题不言自明,如何从 object

中取出 2
slice(0,2)

文档有些混乱,或者是错误的

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

特别是我不明白

的输出是什么意思
slice(0,2).indices(0)  # (0, 0, 1)
slice(0,2).indices(10 ** 10)  # (0, 2, 1)

一种可能的解决方法是使用切片 object

对列表进行切片
a = [1,2,3,4,5]
len(a[slice(0,2)])  # 2

但是对于任意大的切片这将失败。

谢谢,我在其他帖子中找不到答案。

对此没有完整的答案。 slice 不会给你一个长度,因为结果的长度总是取决于被切片的序列的大小,一个短的序列(包括一个空序列)会产生更少的项目,如果 slice是无界的,那么长度会随着序列的长度而增长; slice 可能只是 "to end of sequence" 通过 startstopNone.

为了快速简便地计算已知长度序列的长度,您只需将 .indices 与 Py3 的 range(或 Py2 中的 xrange 结合使用,尽管 xrange 对 Py3 range 没有的值有限制)。 slice.indices gives you the concrete start, stop and stride values derived when a slice applies to a sequence of a given length,它基本上是您在 C 风格 for 循环中填写的值,该循环遍历与 slice:

相同的索引
 for (ssize_t i = start; i < stop; i += stride)

因此,要计算 slice 应用于具有 1000 个元素的序列时的长度,您可以这样做:

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

如果你在 Python 2 上,你的值可能会超过 xrange 可以处理的范围(限制为边界和总长度等于 ssize_t 可以容纳的范围) , 你可以手工计算:

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

更新: 不幸的是,slice.indices 本身不会接受超出 long 可以容纳的序列的 len,所以与在 Py2 中使用 xrange 相比,这不会给您带来任何好处。留给那些感兴趣的人,但解决方法不会解决任何问题,除非你也 perform the work slice does to convert negative values and None to concrete values based on the sequence length. 叹息。

所以它看起来像 slice.indices(n) returns 给 range 的参数,以获得应该反映在长度序列的切片中的项目索引 n虽然没有记录 编辑: 正如@ShadowRanger 指出的,它确实是 documented)。所以下面几行计算出相同的值:

# 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)))])

如您所见,结果列表的长度与 range(*slice(1,15,3).indices(len(my_list))) 的长度相同,这取决于 slice 对象本身,以及要切片的序列的长度。这就是为什么 len(range(*slice.indices(n))) 会在 Python 中给你正确的答案 3.(范围对象是一个生成器,幸运的是它定义了 __len__ 函数,所以它可以给你项目计数,无需枚举和计数。)

如果您在 python 2 中使用大量数字,您可以按照@ShadowRanger 的建议复制计算。

range.__len__ 的原始实现如下:

/* 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;
}

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;
}

来源来自svn

>>> slice(0,2).__getattribute__('stop')
2
>>> slice(0,2).__getattribute__('start')
0

使用断言的简化方法

长度取决于切片的目标对象。 但是可以定义一个最大长度.

例子

像这样定义最大长度函数

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)

并检查输出

>>> 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

最后一次调用崩溃,因为切片没有定义 start 属性。

如果序列的长度已知

ShadowRanger 的回答涵盖了通用解决方案,但如果(像我一样)您 确实 知道序列的长度 - 这是一个简单的方法,应该像 range 会(包括大多数边缘情况),并且不会对潜在的长序列进行迭代。

这与 Markus 所写的类似,但处理了更多边缘情况。

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))

解释: 假设我们可以在没有 src_len 的情况下获得切片的“最大长度”, 然后我们可以通过将 src_len(列表的长度或您想要迭代的任何内容)作为切片的 stop(如果它小于当前的 stop)来构建它。

但这仍然存在寻找“最大长度”的问题。


获取切片的最大长度

python 中的切片构造创建了一种 Arithmetic Set
其中 a0 == start, d == step, n == len

一个公式告诉我们:a_n = a0+ (n-1)d
[a_n 是序列的第 n 个元素]
如果我们将 stop 视为 a_n,则:stop = start + (len - 1) * step
重新排列我们得到:len = [(stop-start)/step] + 1.

这很好地为我们处理了反向迭代(即 [10:0:-1]),

但它通常会 return 浮动,因为停止可能不是开始之后的完整“步骤”数。 (即 [0:10:3], (10-0) / 3 给我们 3.3333...)。
使用 ceil 解决了这个问题。

剩下的唯一问题是负结果([10:0:1] 将给我们 (0-10)/1 = -10),但实际“长度”应该为零。
解决方案是通过 returning dsteps if dsteps >= 0 else 0

来切断负面结果

测试

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)

执行此操作的最佳方法是将切片变成一个范围。

一个范围将具有您需要的所有功能。


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))

切片基本上只是一个 namedtuple,具有与范围相同的属性。这里发生的事情是您只是将切片的值传输到范围对象。

问题在于,大多数时候,切片的值是 None,而范围不接受这些值。我通过添加 DefaultTo lambda 来解决这个问题,它用给定的默认值替换 None,并为 start 和 step 设置各自的默认值。

还有一个问题是slice没有任何类型检查,所以你可以做slice("dfa", True)["hello": type]之类的,它不会在乎。但你可能不会经常遇到这种情况。如果这样做,范围将不会接受它并会引发错误。

最简单的方法是:

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