Python中关于Sequence切片的下标问题详解


Posted in Python onJune 15, 2017

前言

在python中, 切片是一个经常会使用到的语法, 不管是元组, 列表还是字符串, 一般语法就是:

sequence[ilow:ihigh:step] # ihigh,step 可为空; 为了简短易懂, 暂时排除step的用法考虑

先来简单示范下用法

sequence = [1,2,3,4,5]
sequence [ilow:ihigh] # 从ilow开始到ihigh-1结束
sequence [ilow:]  # 从ilow开始直到末尾
sequence [:ihigh]  # 从头部开始直到ihigh结束
sequence [:]   # 复制整个列表

语法很简洁, 也很容易理解, 这种语法在我们日常使用中 是简单又好用, 但我相信在我们使用这种切片语法时, 都会习惯性谨遵一些规则:

  • ilow, ihigh均小于 sequece的长度
  • ilow < ihigh

因为在大部分情况下, 只有遵循上面的规则, 才能得到我们预期的结果! 可是如果我不遵循呢? 切片会怎样?

不管我们在使用元组, 列表还是字符串, 当我们想取中一个元素时, 我们会用到如下语法:

sequence = [1,2,3,4,5]
print sequence[1] # 输出2
print sequence[2] # 输出3

上面出现的 1,2 我们姑且称之为下标, 不管是元组, 列表还是字符串, 我们都能通过下标来取出对应的值, 但是如果下标超过对象的长度, 那么将触发索引异常(IndexError)

sequence = [1,2,3,4,5]
print sequence[15] 

### 输出 ###
Traceback (most recent call last):
 File "test.py", line 2, in <module>
 print a[20]
IndexError: list index out of range

那么对于切片呢? 两种语法很相似, 假设我 ilow 和 ihigh分别是10和20, 那么结果是怎样呢

情景重现

# version: python2.7

a = [1, 2, 3, 5]
print a[10:20] # 结果会报异常吗?

看到10和20, 完全超出了序列a的长度, 由于前面的代码, 或者以前的经验, 我们总会觉得这样肯定也会导致一个IndexError,那我们开终端来试验下:

>>> a = [1, 2, 3, 5]
>>> print a[10:20]
[]

结果居然是: [], 这感觉有点意思.是只有列表才会这么, 字符串呢, 元组呢?

>>> s = '23123123123'
>>> print s[400:2000]
''
>>> t = (1, 2, 3,4)
>>> print t[200: 1000]
()

结果都和列表的类似, 返回属于各自的空结果.

看到结果的我们眼泪掉下来, 不是返回一个IndexError, 而是直接返回空, 这让我们不禁想到, 其实语法相似, 背后的东西肯定还是不同的, 那我们下面一起来尝试去解释下这结果吧

原理分析

在揭开之前, 咱们要先搞清楚, python是怎样处理这个切片的, 可以通过dis模块来协助:

############# 切片 ################
[root@iZ23pynfq19Z ~]# cat test.py
a = [11,2,3,4]
print a[20:30]

#结果:
[root@iZ23pynfq19Z ~]# python -m dis test.py 
 1   0 LOAD_CONST    0 (11)
    3 LOAD_CONST    1 (2)
    6 LOAD_CONST    2 (3)
    9 LOAD_CONST    3 (4)
    12 BUILD_LIST    4
    15 STORE_NAME    0 (a)

 2   18 LOAD_NAME    0 (a)
    21 LOAD_CONST    4 (20)
    24 LOAD_CONST    5 (30)
    27 SLICE+3    
    28 PRINT_ITEM   
    29 PRINT_NEWLINE  
    30 LOAD_CONST    6 (None)
    33 RETURN_VALUE 

############# 单下标取值 ################
[root@gitlab ~]# cat test2.py
a = [11,2,3,4]
print a[20]

#结果:
[root@gitlab ~]# python -m dis test2.py
 1   0 LOAD_CONST    0 (11)
    3 LOAD_CONST    1 (2)
    6 LOAD_CONST    2 (3)
    9 LOAD_CONST    3 (4)
    12 BUILD_LIST    4
    15 STORE_NAME    0 (a)

 2   18 LOAD_NAME    0 (a)
    21 LOAD_CONST    4 (20)
    24 BINARY_SUBSCR  
    25 PRINT_ITEM   
    26 PRINT_NEWLINE  
    27 LOAD_CONST    5 (None)
    30 RETURN_VALUE

在这简单介绍下dis模块, 有经验的老司机都知道, python在解释脚本时, 也是存在一个编译的过程, 编译的结果就是我们经常看到的pyc文件, 这里面codeobject对象组成的字节码, 而dis就是将这些字节码用比较可观的方式展示出来, 让我们看到执行的过程, 下面是dis的输出列解释:

  • 第一列是数字是原始源代码的行号。
  • 第二列是字节码的偏移量:LOAD_CONST在第0行.以此类推。
  • 第三列是字节码人类可读的名字。它们是为程序员所准备的
  • 第四列表示指令的参数
  • 第五列是计算后的实际参数

前面就不赘述了, 就是读常量存变量的过程, 最主要的区别就是: test.py 切片是使用了字节码 SLICE+3实现的, 而test2.py 单下标取值主要通过字节码BINARY_SUBSCR实现的,如同我们猜测的一样, 相似的语法却是截然不同的代码.因为我们要展开讨论的是切片(SLICE+3), 所以就不再展开BINARY_SUBSCR, 感兴趣的童鞋可以查看相关源码了解具体实现, 位置: python/object/ceval.c

那我们下面来展开讨论下 SLICE+3

/*取自: python2.7 python/ceval.c */

// 第一步: 
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
  .... // 省略n行代码
  TARGET_WITH_IMPL_NOARG(SLICE, _slice)
  TARGET_WITH_IMPL_NOARG(SLICE_1, _slice)
  TARGET_WITH_IMPL_NOARG(SLICE_2, _slice)
  TARGET_WITH_IMPL_NOARG(SLICE_3, _slice)
  _slice:
  {
   if ((opcode-SLICE) & 2)
    w = POP();
   else
    w = NULL;
   if ((opcode-SLICE) & 1)
    v = POP();
   else
    v = NULL;
   u = TOP();
   x = apply_slice(u, v, w); // 取出v: ilow, w: ihigh, 然后调用apply_slice
   Py_DECREF(u);
   Py_XDECREF(v);
   Py_XDECREF(w);
   SET_TOP(x);
   if (x != NULL) DISPATCH();
   break;
  }

 .... // 省略n行代码
}

// 第二步:
apply_slice(PyObject *u, PyObject *v, PyObject *w) /* return u[v:w] */
{
 PyTypeObject *tp = u->ob_type;  
 PySequenceMethods *sq = tp->tp_as_sequence;

 if (sq && sq->sq_slice && ISINDEX(v) && ISINDEX(w)) { // v,w的类型检查,要整型/长整型对象
  Py_ssize_t ilow = 0, ihigh = PY_SSIZE_T_MAX;
  if (!_PyEval_SliceIndex(v, &ilow))    // 将v对象再做检查, 并将其值转换出来,存给ilow
   return NULL;
  if (!_PyEval_SliceIndex(w, &ihigh))    // 同上
   return NULL;
  return PySequence_GetSlice(u, ilow, ihigh);  // 获取u对象对应的切片函数
 }
 else {
  PyObject *slice = PySlice_New(v, w, NULL);
  if (slice != NULL) {
   PyObject *res = PyObject_GetItem(u, slice);
   Py_DECREF(slice);
   return res;
  }
  else
   return NULL;
 }

// 第三步:
PySequence_GetSlice(PyObject *s, Py_ssize_t i1, Py_ssize_t i2)
{
 PySequenceMethods *m;
 PyMappingMethods *mp;

 if (!s) return null_error();

 m = s->ob_type->tp_as_sequence;
 if (m && m->sq_slice) {
  if (i1 < 0 || i2 < 0) {
   if (m->sq_length) {
    // 先做个简单的初始化, 如果左右下表小于, 将其加上sequence长度使其归为0
    Py_ssize_t l = (*m->sq_length)(s);
    if (l < 0)
     return NULL;
    if (i1 < 0)
     i1 += l;
    if (i2 < 0)
     i2 += l;
   }
  }
  // 真正调用对象的sq_slice函数, 来执行切片的操作
  return m->sq_slice(s, i1, i2);
 } else if ((mp = s->ob_type->tp_as_mapping) && mp->mp_subscript) {
  PyObject *res;
  PyObject *slice = _PySlice_FromIndices(i1, i2);
  if (!slice)
   return NULL;
  res = mp->mp_subscript(s, slice);
  Py_DECREF(slice);
  return res;
 }

 return type_error("'%.200s' object is unsliceable", s);

虽然上面的代码有点长, 不过关键地方都已经注释出来, 而我们也只需要关注那些地方就足够了. 如上, 我们知道最终是要执行 m->sq_slice(s, i1, i2) , 但是这个sq_slice有点特别, 因为不同的对象, 它所对应的函数不同, 下面是各自的对应函数:

// 字符串对象
StringObject.c: (ssizessizeargfunc)string_slice, /*sq_slice*/

// 列表对象
ListObject.c: (ssizessizeargfunc)list_slice,  /* sq_slice */

// 元组
TupleObject.c: (ssizessizeargfunc)tupleslice,  /* sq_slice */

因为他们三个的函数实现大致相同, 所以我们只分析其中一个就可以了, 下面是对列表的切片函数分析:

/* 取自ListObject.c */
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大于a长度, 那么重新赋值为a的长度
  ilow = Py_SIZE(a);
 if (ihigh < ilow)  
  ihigh = ilow;
 else if (ihigh > Py_SIZE(a))    // 如果ihigh大于a长度, 那么重新赋值为a的长度 
  ihigh = Py_SIZE(a);
 len = ihigh - ilow;
 np = (PyListObject *) PyList_New(len); // 创建一个ihigh - ilow的新列表对象
 if (np == NULL)
  return NULL;

 src = a->ob_item + ilow;
 dest = np->ob_item;
 for (i = 0; i < len; i++) {    // 将a处于该范围内的成员, 添加到新列表对象
  PyObject *v = src[i];
  Py_INCREF(v);
  dest[i] = v;
 }
 return (PyObject *)np;
}

结论

从上面的sq_slice函数对应的切片函数可以看到, 如果在使用切片时, 左右下标都大于sequence的长度时, 都将会被重新赋值成sequence的长度, 所以咱们一开始的切片: print a[10:20] , 实际上运行的是: print a4:4 . 通过这次的分析, 以后在遇到下标大于对象长度的切片, 应该不会再懵逼了~

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python 字典(dict)按键和值排序
Jun 28 Python
Python 稀疏矩阵-sparse 存储和转换
May 27 Python
解决python3 网络请求路径包含中文的问题
May 10 Python
Python处理中文标点符号大集合
May 14 Python
python并发和异步编程实例
Nov 15 Python
如何基于pythonnet调用halcon脚本
Jan 20 Python
python求一个字符串的所有排列的实现方法
Feb 04 Python
python GUI库图形界面开发之PyQt5 MDI(多文档窗口)QMidArea详细使用方法与实例
Mar 05 Python
Python面向对象程序设计之类和对象、实例变量、类变量用法分析
Mar 23 Python
解决Keras中循环使用K.ctc_decode内存不释放的问题
Jun 29 Python
Python如何爬取51cto数据并存入MySQL
Aug 25 Python
pytorch实现线性回归以及多元回归
Apr 11 Python
解决python3在anaconda下安装caffe失败的问题
Jun 15 #Python
带你了解python装饰器
Jun 15 #Python
解决Linux系统中python matplotlib画图的中文显示问题
Jun 15 #Python
Python之os操作方法(详解)
Jun 15 #Python
基于Linux系统中python matplotlib画图的中文显示问题的解决方法
Jun 15 #Python
Python使用QRCode模块生成二维码实例详解
Jun 14 #Python
Python中扩展包的安装方法详解
Jun 14 #Python
You might like
linux下php上传文件注意事项
2016/06/11 PHP
PHP4和PHP5版本下解析XML文档的操作方法实例分析
2017/05/20 PHP
js对象的构造和继承实现代码
2010/12/05 Javascript
Array.prototype.concat不是通用方法反驳[译]
2012/09/20 Javascript
jQuery数据类型小结(14个)
2016/01/08 Javascript
JS从一组数据中找到指定的单条数据的方法
2016/06/02 Javascript
Bootstrap精简教程中秋大放送
2016/09/15 Javascript
jquery实现图片列表鼠标移入微动
2016/12/01 Javascript
利用Node.js编写跨平台的spawn语句详解
2017/02/12 Javascript
MUI 上拉刷新/下拉加载功能实例代码
2017/04/13 Javascript
使用vux实现上拉刷新功能遇到的坑
2018/02/08 Javascript
JavaScript单线程和任务队列原理解析
2020/02/04 Javascript
Quasar Input:type=&quot;number&quot; 去掉上下小箭头 实现加减按钮样式功能
2020/04/09 Javascript
[01:07:47]Secret vs Optic Supermajor 胜者组 BO3 第一场 6.4
2018/06/05 DOTA
编写同时兼容Python2.x与Python3.x版本的代码的几个示例
2015/03/30 Python
Python学生信息管理系统修改版
2018/03/13 Python
Pytorch 解决自定义子Module .cuda() tensor失败的问题
2020/06/23 Python
基于python实现简单网页服务器代码实例
2020/09/14 Python
15个Pythonic的代码示例(值得收藏)
2020/10/29 Python
Python调用SMTP服务自动发送Email的实现步骤
2021/02/07 Python
详解前端HTML5几种存储方式的总结
2016/12/27 HTML / CSS
video.js支持m3u8格式直播的实现示例
2020/05/20 HTML / CSS
广州品高软件.net笔面试题目
2012/04/18 面试题
《狼》教学反思
2014/03/02 职场文书
校园安全标语
2014/06/07 职场文书
2014年财务工作总结与计划
2014/12/08 职场文书
长城导游词
2015/01/30 职场文书
病危通知书样本
2015/04/17 职场文书
2015年社区计生工作总结
2015/04/21 职场文书
三好学生竞选稿
2015/11/21 职场文书
确保减税降费落地生根,用实实在在措施
2019/07/19 职场文书
创业计划书之酒厂
2019/10/14 职场文书
深入浅析React中diff算法
2021/05/19 Javascript
springboot集成springCloud中gateway时启动报错的解决
2021/07/16 Java/Android
windows11怎么查看wifi密码? win11查看wifi密码的技巧
2021/11/21 数码科技
python如何为list实现find方法
2022/05/30 Python