视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001 知道1 知道21 知道41 知道61 知道81 知道101 知道121 知道141 知道161 知道181 知道201 知道221 知道241 知道261 知道281
问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501
关于Sequence切片的下标问题及解决方法
2020-11-27 14:24:03 责编:小采
文档

这篇文章主要给大家介绍了Python中关于Sequence切片下标问题的相关资料,文中通过示例代码介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。

前言

在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 . 通过这次的分析, 以后在遇到下标大于对象长度的切片, 应该不会再懵逼了~

    下载本文
    显示全文
    专题