为啥在 Python 2.7 中手动字符串反转比切片反转更糟糕? Slice 中使用的算法是啥?

Posted

技术标签:

【中文标题】为啥在 Python 2.7 中手动字符串反转比切片反转更糟糕? Slice 中使用的算法是啥?【英文标题】:Why manual string reverse is worse than slice reverse in Python 2.7? What is the algorithm being used in Slice?为什么在 Python 2.7 中手动字符串反转比切片反转更糟糕? Slice 中使用的算法是什么? 【发布时间】:2014-10-05 04:23:28 【问题描述】:

低于 Slice 和手动反向操作的性能差异。如果是这样,那是什么原因呢?

timeit.timeit("a[::-1]","a=[1,2,3,4,5,6]",number=100)
6.054327968740836e-05

timeit.timeit("[a[i] for i in range(len(a)-1,-1,-1)]","a=[1,2,3,4,5,6]",number=100)
0.0003132152330920235

【问题讨论】:

查看操作码会很有趣,但请记住,第二个示例创建了第二个列表(作为范围调用的结果),而第一个没有(当然不是 python range) - 所以这可能是原因之一。 两个版本都涉及循环,但是对于切片,循环由 python 解释器(用 C 编写)执行,因此比 python 循环快得多。 【参考方案1】:

用于反转列表的切片符号下降到 C 中,这比纯 Python 实现的反转要快得多。例如,在纯 python 方法中,python 解释器必须读取、解码和执行字节码中的每条指令,而 C 调用将在本地执行,不会受到这样的惩罚。这种惩罚还扩展到诸如索引项目时的方法查找等事情,而在 C 调用中没有方法,只是地址算术。 C 实现是如此高效,以至于它甚至不需要专门的反向切片函数,并且仍然优于纯 python 实现。相反,它会创建切片的副本并在原地反转切片(在其他地方完成)。

List slice code for cpython:

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;

【讨论】:

【参考方案2】:

3 个不同版本的 Dissasembly -(没有屏幕截图):

import dis

a = [1,2,3,4,5,6]

def x( l ):
    return l[::-1]

dis.dis(x)
2           0 LOAD_FAST                0 (l)
            3 LOAD_CONST               0 (None)
            6 LOAD_CONST               0 (None)
            9 LOAD_CONST               1 (-1)
           12 BUILD_SLICE              3
           15 BINARY_SUBSCR       
          16 RETURN_VALUE        

def y( l ):
   return [l[i] for i in range(len(l)-1,-1,-1)]

dis.dis(y)
2           0 BUILD_LIST               0
            3 LOAD_GLOBAL              0 (range)
            6 LOAD_GLOBAL              1 (len)
            9 LOAD_FAST                0 (l)
           12 CALL_FUNCTION            1
           15 LOAD_CONST               1 (1)
           18 BINARY_SUBTRACT     
           19 LOAD_CONST               2 (-1)
           22 LOAD_CONST               2 (-1)
           25 CALL_FUNCTION            3
           28 GET_ITER            
      >>   29 FOR_ITER                16 (to 48)
           32 STORE_FAST               1 (i)
           35 LOAD_FAST                0 (l)
           38 LOAD_FAST                1 (i)
           41 BINARY_SUBSCR       
           42 LIST_APPEND              2
           45 JUMP_ABSOLUTE           29
      >>   48 RETURN_VALUE        

def z( l ):
    return [i for i in reversed(a)]

dis.dis(z)
2           0 BUILD_LIST               0
            3 LOAD_GLOBAL              0 (reversed)
            6 LOAD_GLOBAL              1 (a)
            9 CALL_FUNCTION            1
           12 GET_ITER            
      >>   13 FOR_ITER                12 (to 28)
           16 STORE_FAST               1 (i)
           19 LOAD_FAST                1 (i)
           22 LIST_APPEND              2
           25 JUMP_ABSOLUTE           13
      >>   28 RETURN_VALUE    

【讨论】:

【参考方案3】:

这是字节码

from dis import dis
a = [1,2,3,4,5,6]

def func1():
    a[::-1]

def func2():
    [a[i] for i in range(len(a)-1,-1,-1)]

def func3():
    reversed(a)

在第二种方法中,您将找到长度,创建一个带范围的副本并创建变量 i。

也可以使用 reversed 创建可迭代对象。

【讨论】:

以上是关于为啥在 Python 2.7 中手动字符串反转比切片反转更糟糕? Slice 中使用的算法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 Python 2.7 中两级字典的值都指向同一个对象?

当我的包是 Python 3.6 时,为啥 conda 要将所有内容降级到 Python 2.7?

为啥这个函数会返回一个值呢? (蟒蛇2.7)

为啥我们在从中缀转换为前缀时必须反转字符串

[python 2.7抓取网页]如何抓取.js里面的内容(下拉框里面的中文字符列表)

为啥这个字符串反转 C 代码会导致分段错误? [复制]