1、Python切片对象可以为任意类型

https://github.com/python/cpython/blob/master/Include/sliceobject.h

/* Slice object interface */

/*

A slice object containing start, stop, and step data members (the

names are from range).  After much talk with Guido, it was decided to

let these be any arbitrary python type.  Py_None stands for omitted values.

*/

#ifndef Py_LIMITED_API

typedef struct {

PyObject_HEAD

PyObject *start, *stop, *step;      /* not NULL */

} PySliceObject;

#endif

cpython/sliceobject.c at master · python/cpython https://github.com/python/cpython/blob/master/Objects/sliceobject.c#L135

/* start, stop, and step are python objects with None indicating no

index is present.

*/

PyObject *

PySlice_New(PyObject *start, PyObject *stop, PyObject *step)

{

if (step == NULL) {

step = Py_None;

}

if (start == NULL) {

start = Py_None;

}

if (stop == NULL) {

stop = Py_None;

}

PyInterpreterState *interp = _PyInterpreterState_GET();

PySliceObject *obj;

if (interp->slice_cache != NULL) {

obj = interp->slice_cache;

interp->slice_cache = NULL;

_Py_NewReference((PyObject *)obj);

}

else {

obj = PyObject_GC_New(PySliceObject, &PySlice_Type);

if (obj == NULL) {

return NULL;

}

}

Py_INCREF(step);

obj->step = step;

Py_INCREF(start);

obj->start = start;

Py_INCREF(stop);

obj->stop = stop;

_PyObject_GC_TRACK(obj);

return (PyObject *) obj;

}

深入 Python —— 切片(Slice)原理 | Protream's Blog https://protream.com/2019/understand-python-slice/

深入 Python —— 切片(Slice)原理

 发表于 2019-04-03 | 

在阅读本文前,先来测试一下对切片的掌握情况吧,尝试回答下面几个问题:

1
a = [1, 2, 3, 4, 5]
  1. 获取 [2, 3, 4]
  2. 获取 [2, 4]
  3. 获取 [5, 3, 1]
  4. a[:-1] = ?
  5. a[::-1] = ?
  6. a[-1:-2] = ?
  7. a[6:-1] = ?
  8. a[:-1:-1] = ?
  9. a[1:-1:0] = ?
  10. 切片返回的是序列的深拷贝还是浅拷贝?

如果在不借助 Python 解释器的情况下你能很快的说出答案,那说明你已经对切片掌握的不错了。相信还是有很多人不能完全解答出来或者对其中的一个或者多个不完全确定。没关系,这篇文章我们就一起来从底层实现层面来彻底掌握切片机制,学完再回过头来看相信这些就不再是问题了。

从字节码看起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [1]: def test():
...: a = [1, 2, 3, 4, 5]
...: b = a[1:-1]
...:

In [2]: import dis

In [3]: dis.dis(test)
2 0 LOAD_CONST 1 (1)
2 LOAD_CONST 2 (2)
4 LOAD_CONST 3 (3)
6 LOAD_CONST 4 (4)
8 LOAD_CONST 5 (5)
10 BUILD_LIST 5
12 STORE_FAST 0 (a)

3 14 LOAD_FAST 0 (a)
16 LOAD_CONST 1 (1)
18 LOAD_CONST 6 (-1)
20 BUILD_SLICE 2
22 BINARY_SUBSCR
24 STORE_FAST 1 (b)

这段字节码分为俩个部分,上半部分构建列表 a,下半部分通过对 a 切片得到列表 b。和本文主题相关的俩个字节码指令就在下半部分,它们是:

  • BUILD_SLICE
  • BINARY_SUBSCR

BUILD_SLICE

1
2
3
16 LOAD_CONST               1 (1)
18 LOAD_CONST 6 (-1)
20 BUILD_SLICE 2

在执行 BUILD_SLICE 之前,解释器将 slice 的俩个关键参数 start 和 stop 压入栈,然后执行 BUILD_SLICE 指令,注意,这里传入的参数是 2。2 代表构建 slice 对象的参数只有两个,也就是说并没有是指定第三个参数 step。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TARGET(BUILD_SLICE) {
PyObject *start, *stop, *step, *slice;
if (oparg == 3)
step = POP();
else
step = NULL;
stop = POP();
start = TOP();
slice = PySlice_New(start, stop, step);
Py_DECREF(start);
Py_DECREF(stop);
Py_XDECREF(step);
SET_TOP(slice);
if (slice == NULL)
goto error;
DISPATCH();
}

这段代码比较简单,首先根据传入的参数个数判断 slice 有没有第三个参数 step ,有的话它在 BUILD_SLICE 之前肯定是最后一个被压入栈的,现在从栈中取出的第一个就是 step,没有的话,step 被设为 NULL。然后,继续取出 start 和 stop,将这三个参数传入 PySlice_New 函数创建一个 slice 对象,再将这个对象放回栈中。

我们可以来看一下 slice 对象到底是什么了:

1
2
3
4
typedef struct {
PyObject_HEAD
PyObject *start, *stop, *step; /* not NULL */
} PySliceObject;

现在明白了吧,slice 对象就是一个记录了 start,stop,step 值(对象)的一个 Python 对象,它的创建过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static PySliceObject *slice_cache = NULL;
PyObject *
PySlice_New(PyObject *start, PyObject *stop, PyObject *step)
{
PySliceObject *obj;
if (slice_cache != NULL) {
/* 为了效率,Python 会缓存一个 slice 对象,有缓存就省得申请内存了 */
obj = slice_cache;
slice_cache = NULL;
_Py_NewReference((PyObject *)obj);
} else {
obj = PyObject_GC_New(PySliceObject, &PySlice_Type);
if (obj == NULL)
return NULL;
}

/* step start stop 为设置会被置为 None */
if (step == NULL) step = Py_None;
Py_INCREF(step);
if (start == NULL) start = Py_None;
Py_INCREF(start);
if (stop == NULL) stop = Py_None;
Py_INCREF(stop);

/* 设置 slice 对象的 step start stop */
obj->step = step;
obj->start = start;
obj->stop = stop;

_PyObject_GC_TRACK(obj);
return (PyObject *) obj;
}

现在我们明白了一点,当我们对一个序列进行切片时,解释器会根据传入的 start,stop,step 创建一个 slice 对象,slice 对象和你要切片的原序列并没有直接的关系

Python 提供了 slice 内建函数来创建 slice 对象:

1
2
3
4
5
6
7
8
9
10
11
12
In [68]: s = slice(1, -1, 2)

In [69]: type(s)
Out[69]: slice
In [70]: s.start
Out[70]: 1

In [71]: s.stop
Out[71]: -1

In [72]: s.step
Out[72]: 2

下面的俩种获取切片的方式是等价的:

1
2
3
4
5
6
7
In [73]: a = [1, 2, 3, 4, 5]

In [74]: a[1:-1:2]
Out[74]: [2, 4]

In [75]: a[slice(1, -1, 2)]
Out[75]: [2, 4]

BINARY_SUBSCR

这个指令翻译过来叫二元下标, a[0] 这种方式是一元下标,可以猜测一下,通过 slice 对象对序列切片和通过 index 对序列取值直接是不是有什么联系呢?我们继续往下看源码:

1
2
3
4
5
6
7
8
9
10
11
TARGET(BINARY_SUBSCR) {
PyObject *sub = POP();
PyObject *container = TOP();
PyObject *res = PyObject_GetItem(container, sub);
Py_DECREF(container);
Py_DECREF(sub);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}

这里从栈中取出来给 sub 的对象就是我们前面构建的 slice 对象,而 container 对象就是我们要切片的原列表,它们被传给了 PyObject_GetItem

答案是不是呼之欲出了?二元下标也就是切片是通过 PyObject_GetItem 这个函数处理的,它也是用来处理一元下标的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 PyObject *
PyObject_GetItem(PyObject *o, PyObject *key)
{
PyMappingMethods *m;

if (o == NULL || key == NULL) {
return null_error();
}

m = o->ob_type->tp_as_mapping;
if (m && m->mp_subscript) {
PyObject *item = m->mp_subscript(o, key);
assert((item != NULL) ^ (PyErr_Occurred() != NULL));
return item;
}

if (o->ob_type->tp_as_sequence) {
if (PyIndex_Check(key)) {
Py_ssize_t key_value;
key_value = PyNumber_AsSsize_t(key, PyExc_IndexError);
if (key_value == -1 && PyErr_Occurred())
return NULL;
return PySequence_GetItem(o, key_value);
}
else if (o->ob_type->tp_as_sequence->sq_item)
return type_error("sequence index must "
"be integer, not '%.200s'", key);
}

return type_error("'%.200s' object is not subscriptable", o);
}

PyObject_GetItem 是用来实现多态的,它根据要切片对象的不同,调用对象的特定函数做出不同的处理。我们后面会讲在列表列表的处理,现在我们需要明白,序列的下标可以是 int 对象或者是 slice 对象,处理它们的函数接口是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [76]: a = [1, 2, 3, 4, 5]

In [77]: a.__getitem__(1)
Out[77]: 2

In [78]: a.__getitem__(slice(1, -1, 2))
Out[78]: [2, 4]

In [80]: s = 'python'

In [81]: s.__getitem__(1)
Out[81]: 'y'

In [82]: s.__getitem__(slice(1, -1, 2))
Out[82]: 'yh'

切片参数的处理

start 、stop 和 step 的值可以是整数,可以是负数,start 和 stop 的值还可能超过列表的长度,对于特殊 step、stop 值的处理也就决定了切片的结果,而这些处理正是在PySlice_GetIndicesEx 这个函数中完成的,理解切片行为的核心就是要理解这个函数的逻辑。我们拆开来看这个函数是怎么处理 start stop step 的。

  1. step 的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (r->step == Py_None) {
/* step 默认是 1,这不难理解 */
*step = 1;
} else {
if (!_PyEval_SliceIndex(r->step, step)) return -1;
/* step 不能为零,否则报 ValueError,要注意的是,这个异常是在执行 BINARY_SUBSCR 才报出来,
* 在创建 slice 对象时如果 step 为 0 并不会报错 */
if (*step == 0) {
PyErr_SetString(PyExc_ValueError, "slice step cannot be zero");
return -1;
}
/* step 的最小值,他是根据 size_t 来定义的
* #define PY_SSIZE_T_MAX ((Py_ssize_t)(((size_t)-1)>>1))
* 所以在 32 为系统上就是 -2147483647 */
if (*step < -PY_SSIZE_T_MAX)
*step = -PY_SSIZE_T_MAX;
}
  1. start 的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 当 start 未设置时的默认值,length 是序列的长度
* 如果切片从序列头部开始(step > 0),start = 0
* 如果切片从序列末尾开始(step < 0),start = length - 1 */
defstart = *step < 0 ? length-1 : 0;
if (r->start == Py_None) {
*start = defstart;
}
else {
if (!_PyEval_SliceIndex(r->start, start)) return -1;
/* 如果 start 是负数,其实是通过加上序列长度转化成正数的 a[-1:] <=> a[4:] */
if (*start < 0) *start += length;
/* 如果加上 length 还是小于 0,也就是 -start 超出了序列长度,这时候会根据 step 的正负将start
* 设置为序列的开始(0)或结束(-1)位置
* a[-6:-1] <=> a[0:-1]
* a[-6:-1:-1] <=> a[-1:-1:-1] */
if (*start < 0) *start = (*step < 0) ? -1 : 0;
/* start 超出了序列长度,这时候会根据 step 的正负将start
* 设置为序列的长度或序列长度减 1(最后一个元素)
* a[6:-1] <=> a[5:-1]
* a[6:-1:-1] <=> a[4:-1:-1] */
if (*start >= length)
*start = (*step < 0) ? length - 1 : length;
}
  1. stop 的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /* 当 stop 未设置时的默认值,length 是序列的长度
* 如果切片从序列头部开始(step > 0),stop = length,比最后一个元素的下标多 1
* 如果切片从序列末尾开始(step < 0),start = -1,比第一个元素的下标少 1 */
defstop = *step < 0 ? -1 : length;
if (r->stop == Py_None) {
*stop = defstop;
} else {
if (!_PyEval_SliceIndex(r->stop, stop)) return -1;
/* 如果 stop 是负数,其实是通过加上序列长度转化成正数的 a[:-1] <=> a[:4] */
if (*stop < 0) *stop += length;
/* 如果加上 length 还是小于 0,也就是 -stop 超出了序列长度,这时候会根据 step 的正负将 stop
* 设置为序列的开始(0)或结束(-1)位置
* a[3:-6] <=> a[3:0]
* a[3:-6:-1] <=> a[3::-1] */
if (*stop < 0) *stop = (*step < 0) ? -1 : 0;
if (*stop >= length)
*stop = (*step < 0) ? length - 1 : length;
}
  1. slicelength 的处理,slicelength 是切片结果的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 如:a[6:1]      处理后 start = 6 stop = 1 start > stop
* a[6:-1] 处理后 start = 6 stop = 4 start > stop
* a[2:3:-1] 处理后 start = 2 stop = 3 stop >= start
* a[-3:-2:-1] 处理后 start = 2 stop = 3 stop >= start */
if ((*step < 0 && *stop >= *start)
|| (*step > 0 && *start >= *stop)) {
*slicelength = 0;
}
else if (*step < 0) {
*slicelength = (*stop-*start+1)/(*step)+1;
}
else {
*slicelength = (*stop-*start-1)/(*step)+1;
}

记住几点:

  • 切片结果是通过 start、stop 处理后的值决定的,从 start 开始止于 stop 不包括 stop,[start, stop)
  • 如果 step > 0,从 start 位置往后,每 step 取一个值,如果 start >= stop,结果为空
  • 如果 step < 0,从 start 位置往前,每 step 取一个值,如果 start <= stop,结果为空
  • start 或 stop 为负数时,如果绝对值在 length 内,那么和 length + start 或 stop 等价
  • start 或 stop 为负数时,如果绝对值超过 length ,那么就要根据切片方向将 start 或 stop 转换为边界值

列表切片

切片可以作用于所有序列对象:列表,字符串,元组。我们日常最常用的就是列表切片,这里就深入看下列表切片的处理,其他俩种处理方式应该也类似。

1
2
3
4
5
6
7
8
9
10
static PyMappingMethods list_as_mapping = {
...
(binaryfunc)list_subscript,
...
}

static PyMethodDef list_methods[] = {
{"__getitem__", (PyCFunction)list_subscript, METH_O|METH_COEXIST, getitem_doc},
...
}

深入到 list 对象的源码后发现, o->ob_type->tp_as_mapping->mp_subscript(回看 PyObject_GetItem 的逻辑)和 list.__getitem__ 都指向了同一个函数——list_subscript,list 的切片正是在这里处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 static PyObject *list_subscript(PyListObject* self, PyObject* item)
{
...
/* 如果下标是 slice 对象 */
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength, cur, i;
PyObject* result;
PyObject* it;
PyObject **src, **dest;

/* 从 slice 对象解析并处理 start stop step 和 slicelength */
if (PySlice_GetIndicesEx(item, Py_SIZE(self),
&start, &stop, &step, &slicelength) < 0) {
return NULL;
}

/* 从 start stop step 计算出切片结果的长度小于等于 0,返回空列表 */
if (slicelength <= 0) {
return PyList_New(0);
}
/* 结果长度大于 0 而 step = 1,交给 list_slice 处理 */
else if (step == 1) {
return list_slice(self, start, stop);
}
else {
/* 创建一个与结果长度相等的列表对象 */
result = PyList_New(slicelength);
if (!result) return NULL;

src = self->ob_item;
dest = ((PyListObject *)result)->ob_item;
/* 从 start 开始,每次前进 step,开始 copy 列表元素 */
for (cur = start, i = 0; i < slicelength;
cur += (size_t)step, i++) {
/* 从这里可以看出,从原列表拷贝元素到切片,只是一个简单的赋值,所以切片是浅拷贝 */
it = src[cur];
Py_INCREF(it);
dest[i] = it;
}

return result;
}
...
}

其中的 list_slice 函数像是 list_subscript 当 step 等于 1 时的简化版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static PyObject *
list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
PyListObject *np;
PyObject **src, **dest;
Py_ssize_t i, len;
if (ilow < 0)
ilow = 0;
else if (ilow > Py_SIZE(a))
ilow = Py_SIZE(a);
if (ihigh < ilow)
ihigh = ilow;
else if (ihigh > Py_SIZE(a))
ihigh = Py_SIZE(a);
len = ihigh - ilow;
np = (PyListObject *) PyList_New(len);
if (np == NULL)
return NULL;

src = a->ob_item + ilow;
dest = np->ob_item;
for (i = 0; i < len; i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
return (PyObject *)np;
}

总结

本文从源码层面剖析了什么是 slice 对象,切片中对 start、stop、step 值的处理,及虚拟机生成一个列表切片的整个过程。弄懂了 Python 对 start、stop、step 的处理逻辑后,文章开始处的几道题也就不能得出答案了。

slice 切片实现 Slice object interface的更多相关文章

  1. python定制类(1):__getitem__和slice切片

    python定制类(1):__getitem__和slice切片 1.__getitem__的简单用法: 当一个类中定义了__getitem__方法,那么它的实例对象便拥有了通过下标来索引的能力. c ...

  2. Python入门篇-基础数据类型之整型(int),字符串(str),字节(bytes),列表(list)和切片(slice)

    Python入门篇-基础数据类型之整型(int),字符串(str),字节(bytes),列表(list)和切片(slice) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Py ...

  3. GoLang基础数据类型-切片(slice)详解

    GoLang基础数据类型-切片(slice)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 数组的长度在定义之后无法再次修改:数组是值类型,每次传递都将产生一份副本.显然这种数 ...

  4. golang:slice切片

    一直对slice切片这个概念理解的不是太透彻,之前学习python的就没搞清楚,不过平时就用python写个工具啥的,也没把这个当回事去花时间解决. 最近使用go开发又遇到这个问题,于是打算彻底把这个 ...

  5. Go语言学习笔记十一: 切片(slice)

    Go语言学习笔记十一: 切片(slice) 切片这个概念我是从python语言中学到的,当时感觉这个东西真的比较好用.不像java语言写起来就比较繁琐.不过我觉得未来java语法也会支持的. 定义切片 ...

  6. golang slice切片的原理以及内置函数cap, len

    golang中slice(切片)是常用的类型, slice是对数组进行封装 package main import ( "fmt" "strconv") fun ...

  7. Go - 切片(Slice)

    定义 切片本身不是数组,它指向底层的数组或者数组的一部分.因此,可以使用Slice来处理变长数组的应用场景. Silice 是一种引用类型. 1.定义一个空的Slice package main im ...

  8. golang基础---Slice切片

    切片Slice在go语言中是单独的类型(指向底层的数组),不同于python(对可迭代对象操作的工具),注意区分数组和slice的区别 定义一个空slice,格式var s []int,这种既没有长度 ...

  9. golang slice 切片原理

    golang 中的 slice 非常强大,让数组操作非常方便高效.在开发中不定长度表示的数组全部都是 slice .但是很多同学对 slice 的模糊认识,造成认为golang中的数组是引用类型,结果 ...

随机推荐

  1. python序列(十)字典

    字典是无序可变序列. 定义字典是,每个元素的键和值用冒号分隔,元素之间用逗号分隔,所有的元素放在一对大括号"{ }"中. 字典中的键可以为任意不可变数据,比如.整数.实数.复数.字 ...

  2. [Machine Learning] 逻辑回归 (Logistic Regression) -分类问题-逻辑回归-正则化

    在之前的问题讨论中,研究的都是连续值,即y的输出是一个连续的值.但是在分类问题中,要预测的值是离散的值,就是预测的结果是否属于某一个类.例如:判断一封电子邮件是否是垃圾邮件:判断一次金融交易是否是欺诈 ...

  3. H5-地理定位/本地存储/拖放

    一.地理定位 Geolocation 兼容性:Internet Explorer 9+, Firefox, Chrome, Safari 和 Opera 支持Geolocation(地理定位). 一次 ...

  4. 2020年Python文章盘点,我选出了个人TOP10

    大家好,我是猫哥.2020年过得真快啊!总感觉这一年里还没有做成多少事,一眨眼就又到了写年度总结的时候了-- 去年1月1日的时候,我写了<我的 2019 年 Python 文章榜单>,简单 ...

  5. Linux 时间同步 05 chrony时间同步

    Linux 时间同步 05 chrony时间同步 目录 Linux 时间同步 05 chrony时间同步 chrony 的优势: chrony包介绍 安装chrony 配置与外部时间服务器进行时间同步 ...

  6. Ubuntu/Liinux睡眠无法唤醒解决方法:ACPI设置

    最近给笔记本换了一个固态,顺便就重装了一下系统,版本是Ubuntu 20.10.装完之后各种功能基本都是正常的,触摸板轻触单击.双指右键等功能开箱即用.但是在安装完最新的Nvidia驱动之后就出现了问 ...

  7. .NET Core 中的日志与分布式链路追踪

    目录 .NET Core 中的日志与分布式链路追踪 .NET Core 中的日志 控制台输出 非侵入式日志 Microsoft.Extensions.Logging ILoggerFactory IL ...

  8. 网络设置-指定ip

    1 克隆centos6.设置网卡 vim /etc/udev/rules.d/70-persistent-net.rules 确定光标所在的位置 d3d删除以下三行,更改NAME为0 vim /etc ...

  9. NOIP初赛篇——04计算机软件系统

    计算机软件是指计算机系统中的程序及其文档,也是用户与硬件之间的接口,用户主要通过软件与计算机进行交流,软件是计算机的灵魂.没有安装软件的计算机称为"裸机",无法完成任何工作.一般软 ...

  10. SqlLoad的简单使用

    sqlload的简单使用: 能实现: 快速导入大量数据 1.先安装oracle 客户端机器.有点大,600M+, 2.安装时选择管理员安装(1.1g) 3.第三步的时候我的出错了.说是环境变量校验不通 ...