结合python3源码浅析切片对象(冒号)用法详解及其对特殊情况的处理

在写代码的时候遇到了bug,debug后发现是list中的冒号(切片)用错了,特地写了几个测试的小例子,准备认真探究一下对 list 、str和 tuple 切片时冒号的用法,一般会在取数组某一部分的时候用到冒号。实际上在python中这是一个切片对象(slice object),一般情况下有start、stop、step三个参数,如果不指定step的话默认step为1。

如果觉得文章太长可以直接翻到最底下看结论。

下面直接上例子,python版本3.7.3。

①省略start和stop参数的情况

i = [1, 2, 3, 4, 5]
print("list_test1", i[:])  # 等同于print(i[::])

输出结果:

list_test1 [1, 2, 3, 4, 5]

可以看到,省略start、stop和step三个参数相当于输出整个list。实际上这里面是这样的流程:程序首先查看step的值,发现step是None,因此默认step为1,切片方向为正;又发现start和stop是None,因此系统初始化start为0,stop为len(i),切片长度是stop-start=len(i),这样就相当于输出整个list。
那么,如果是这样的情况,系统又怎么处理呢?

print("list_test1", i[::-1])

输出结果:

list_test1 [5, 4, 3, 2, 1]

这样的情况相当于reverse,逆序输出;程序查看step的值是-1,因此切片方向为负;又因为start和stop是None,因此初始化它们为len(i)-1和-1,切片长度是start-stop=len(i),相当于逆序输出整个list。
这里贴上cpython的源码可以帮助理解(分析时方便起见默认|step|=1):

/*这个函数的作用是获取用户指定的start、stop、step参数并对其作初步调整,如果不合法就要作进一步处理*/
int
PySlice_GetIndices(PyObject *_r, Py_ssize_t length,
	Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step)
{
	PySliceObject *r = (PySliceObject*)_r;
	/* XXX support long ints */
	if (r->step == Py_None) { //如果*step为None
		*step = 1;  //初始化为1
	}
	else {
		if (!PyLong_Check(r->step)) return -1;
		*step = PyLong_AsSsize_t(r->step);
	}
	if (r->start == Py_None) { //如果*start为None
		*start = *step < 0 ? length - 1 : 0; //若*step<0则*start初始化为length-1(最右端元素的索引),反之就是0(最左端元素的索引)
	}
	else {
		if (!PyLong_Check(r->start)) return -1;
		*start = PyLong_AsSsize_t(r->start);
		if (*start < 0) *start += length;//*start<0就设为*start+length
	}
	if (r->stop == Py_None) {//如果*stop为None
		*stop = *step < 0 ? -1 : length;//若*step<0则*stop初始化为-1(切片长度就是*start+1),反之就是length(切片长度为length-*start)
	}
	else {
		if (!PyLong_Check(r->stop)) return -1;
		*stop = PyLong_AsSsize_t(r->stop);
		if (*stop < 0) *stop += length;//*stop<0就设为*stop+*length
	}
	if (*stop > length) return -1;//以下情况明显越界,return -1
	if (*start >= length) return -1;
	if (*step == 0) return -1;
	return 0;
}

②省略start或stop参数

若只有start参数:

i = [1, 2, 3, 4, 5]
print("list_test2", i[0:])
print("list_test3", i[1:])

输出结果:

list_test2 [1, 2, 3, 4, 5]
list_test3 [2, 3, 4, 5]

可以看到,只有start参数的切片表示以这个start数值作为索引值处的元素开始切片到最右端(或者切片到最左端,这个切片方向由step决定),比如0:表示从第一个元素开始切,1:表示从第二个元素开始切。
若只有stop参数:

i = [1, 2, 3, 4, 5]
print("list_test4", i[:4])
print("list_test5", i[:5])

输出结果:

list_test4 [1, 2, 3, 4]
list_test5 [1, 2, 3, 4, 5]

只有stop参数的切片表示从最左端或者最右端元素开始一直切出 |stop| 长的对象为止(比如:4表示从第一个元素到第4个元素,:5表示从第一个元素到第5个元素)。因此,诸如i[:n](或i[:n:-1])这样的用法是包含了第1到第n个元素,并不包含索引为n的元素(即第n+1个元素)。其实之前的分析已经可以推出这个结论了,这里只是为了条理再叙述一遍。

事实上,python对于数组的索引标号是这样的:
python对于sequence数组索引形式也就是说,在我们这个例子中,正向数i中的元素依次为i[0]、i[1]、i[2]、i[3]、i[4],反向数i中的元素就是i[-5]、i[-4]、i[-3]、i[-2]、i[-1],那么就可以得到接下来的结论③。
而负索引之所以是这样的形式,是因为python对合法负索引采用了+length的处理方式,代码见下

 if (*start < 0) *start += length;//start<0就设为start+length
if (*stop < 0) *stop += length;//stop<0就设为stop+length

③指定start和stop参数

经过了前面两点的分析,这里的结论其实已经可以很明显地得出了。但是本着严谨的态度,我还是写上几个example。
如:

i[x : y]

如果0<x<y≤len(list),就是从第x+1个元素到第y个元素。
如果 -len(list)≤x<y<0 ,就是从第x+1+len(list)到第y+len(list)个元素。

i[x + len(i) : y + len(i)]

print("list_test6", i[-3:-2])
print("list_test7", i[-4:-2])

就等同于

print("list_test6", i[2:3])
print("list_test7", i[1:3])

因此输出结果是:

list_test6 [3]
list_test7 [2, 3]

在这个过程中,我还发现了一个有趣的现象。我将list和tuple的情况作了对比,发现其他测试都一致,除了以下这种情况比较特别:

i = [1, 2, 3, 4, 5]
j = (1, 2, 3, 4, 5)
print('list_test', i[2:3], i[-4:-3])
print('tuple_test', j[2:3], j[-4:-3])

输出结果:

list_test [3] [2]
tuple_test (3,) (2,)

在用x:x+1这样的方式(切片长度为1时)来输出元素的时候,前者正常返回,后者tuple会返回一个带多余逗号的tuple对象;在cmd命令行中测试也是如此;
在这里插入图片描述
当start、stop参数都指定的时候,唯一需要注意的是start指向stop的方向一定要与step的方向一致,否则就会返回空数组[],比如i[1:4]和i[4:1:-1]都是合法的,但是i[4:1]和i[1:4:-1]就是不合法的。

④其他特殊情况——越界

以上几种情况都是在start和stop不越界的前提下讨论的,当start越界时(start≥len(list)或<-len(list) 时or stop>len(list)或≤-len(list)时,视为越界),python3该如何处理呢?
简单的输入了几个例子之后,发现情况还挺复杂,因此我把所有step>0的情况一一列举,还是以这个为例:

# 这个example中左侧≥5或≤-6算越界;右侧>=6或≤-5算越界
i = [1, 2, 3, 4, 5]  # len(i) = 5

情况列举如下(每种情况后均随意举一个例子,并且附上其输出结果,step<0的情况就不列举了,显得太过冗余):
左正右无 越界:i[7:]–>[]
左负右无 越界:i[-8:]–>[1, 2, 3, 4, 5]
右正左无 越界:i[:8]–>[1, 2, 3, 4, 5]
右负左无 越界:i[:-6]–>[]
左正右正 都越:i[8:10]–>[];左越右不越:i[6:2]–>[];右越左不越:i[3:7]–>[4, 5]
左负右负 都越:i[-8:-10]–>[];左越右不越:i[-10:-3]–>[1, 2];右越左不越:i[-2:-10]–>[]
左正右负 都越:i[10:-12]–>[];左越右不越:i[12:-3]–>[];右越左不越:i[2:-9]–>[]
左负右正 都越:i[-12:10]–>[1, 2, 3, 4, 5];左越右不越:i[-12:3]–>[1, 2, 3];右越左不越:i[-2:9]–>[4, 5]
我们可以发现,就算有时候参数很“离谱”,明显越界了,但是程序还是输出了正常的结果;有时候却又会返回空数组。那么,为什么会这样呢?答案还是在源码中:(分析时方便起见默认|step|=1)

/********这个函数调整start和stop的值,并返回切片对象的长度***********/
Py_ssize_t
PySlice_AdjustIndices(Py_ssize_t length,
                      Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t step)
{
    /* this is harder to get right than you might think */

    assert(step != 0);
    assert(step >= -PY_SSIZE_T_MAX);//step要满足以上两个条件,否则报错

    if (*start < 0) {  
        *start += length;  //加上length
        if (*start < 0) {  //若*start小于零越界,需要对*start进行调整
            *start = (step < 0) ? -1 : 0; //由step方向决定,小于零置-1(没有此索引,无法切片),大于零就置0(最左端元素的索引)
        }
    }
    else if (*start >= length) {  //*start大于零越界,即*start的值已经大于length了,同样也要调整
        *start = (step < 0) ? length - 1 : length;  //由step方向决定,小于零置length-1(即最右端元素的索引),大于零就置length(没有此索引,无法切片)
    }

    if (*stop < 0) { 
        *stop += length;
        if (*stop < 0) {  //*stop小于零越界
            *stop = (step < 0) ? -1 : 0;  //由step方向决定,小于零置-1(切片长度为*start+1),大于零置0(切片长度≤0,无法切片)
        }
    }
    else if (*stop >= length) { //*stop大于零越界
        *stop = (step < 0) ? length - 1 : length; //由step方向决定,小于零置length-1(切片长度≤0,无法切片),大于零置length(切片长度为length-*start)
    }
	//经过以上调整,非常巧妙的把合法和非法的情况都包含在了一起
	//以下程序作用是:如果参数合法就返回切片后对象的长度,如果不合法就返回0
	//猜想这里是为了更合理分配内存而这样写,参数合法就分配对应大小的内存,不合法就不分配(相当于分配长度为0的内存)
    if (step < 0) {
        if (*stop < *start) {
            return (*start - *stop - 1) / (-step) + 1;
        }
    }
    else {
        if (*start < *stop) {
            return (*stop - *start - 1) / step + 1;
        }
    }
    return 0;
}

对以上四点归纳总结后可以得到以下结论:
1、要判断用冒号切片时是否合法,首先看stop-start是否与step方向(符号)一致,如不一致一定是非法的;一致则观察start和stop是否满足第二点中所述的合法越界条件,若不满足也是非法的。非法并不会报错,只是返回一个空数组[]。
满足合法条件时,切片从a[start]开始,切片长度为((|stop-start|-1)/|step|)+1(|step|=1时就是|stop-start|),方向与步长由step决定(如果遇到start或stop为负值就作+length处理)。

2、合法越界条件及其处理:在step>0时,start允许负数越界,stop允许正数越界;在step<0时,start允许正数越界,stop允许负数越界(如本例中尽管i只是一个长度为5的list,但i[-1000:+1000:1]、i[+1000:-1000:-1]这样的写法都是被允许的)。合法越界的数字当成“空气”处理(可以理解为写了跟没写一样,比如前面两个例子可以当成i[::1]、i[::-1]处理)。其他情况下都是非法的。

总的来说,处理冒号切片只要关注三个问题:

①我是谁?(合法吗?)—>关注根本性的矛盾问题

②我从哪里来?(从哪start呢?)—>关注切片起点

③我要到哪去?(step方向和大小是什么?在哪stop呢?) —>关注切片方向、步长及切片终点

至此,这个问题算是得到了答案。虽然对一个小小的冒号如此“大动干戈”显得有些夸张,可能也并没有多大意义。但是我本着一种探究的精神写到了这里,也可以看出一个小小的冒号背后其实有着非常多内部处理的步骤,见微知著,还是有所收获的。希望以后debug能用上!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值