如何在CPython源代码中找到[::-1](在python中为反向列表)的实现

时间:2018-09-12 11:45:41

标签: python list cpython python-internals

我试图在python中反转一个列表。 那里有很多方法,[::-1]似乎是一个很棒的方法! 但是我很好奇[::-1]是如何完成的?它的时间复杂度是多少?

我在github中搜索了CPython存储库,但找不到任何线索。 我的搜索策略是在CPython存储库中搜索关键字::。 由于python语法中有::,即[::-1],因此CPython源代码中可能有::,因此[::-1]可以引用它并进行反转。这有道理吗?

1 个答案:

答案 0 :(得分:8)

list[...]使用的是索引,或更准确的是subscription syntax,而[start:stop:step]slicing。在这种情况下,Python 3将slice()对象传递给订阅调用,CPython源代码中没有::,因为这不是语言解析器如何威胁该部分。切片符号允许使用默认值,start:stop:step部分之间的空白表示选择了默认值,而list[::-1]startstop保留为默认值(用{{ 1}},并将步长值设置为None

所有这些仅意味着您需要记住将语法与操作分开。您可以通过将语法解析为抽象语法树或分解为该语法生成​​的字节码来分析语法。为-1 generate an abstract syntax tree时,您会看到解析器将切片部分分离出来:

list[::-1]

因此>>> import ast >>> ast.dump(ast.parse('list[::-1]', '', 'eval').body) # expression body only "Subscript(value=Name(id='list', ctx=Load()), slice=Slice(lower=None, upper=None, step=UnaryOp(op=USub(), operand=Num(n=1))), ctx=Load())" 语法节点以名称Subscript()进行操作,并传入一个切片。

Producing a disassembly会显示此字节码:

list

这里BUILD_SLICE() bytecode operation从堆栈中取出前三个元素(通过在索引2、4和6上的>>> dis.dis('list[::-1]') 1 0 LOAD_NAME 0 (list) 2 LOAD_CONST 0 (None) 4 LOAD_CONST 0 (None) 6 LOAD_CONST 1 (-1) 8 BUILD_SLICE 3 10 BINARY_SUBSCR 12 RETURN_VALUE 操作放入那里)来构建LOAD_CONST对象,然后该对象通过slice()传递到list对象(堆栈中的下一个)。最后一个字节码是AST中的BINARY_SUBSCR操作,字节码2-8是AST中的Subscript()对象。

手头有字节码,然后您可以转到Python bytecode evaluation loop,以查看这些字节码实际上是做什么的。我将跳过Slice(),这只是创建一个简单的BUILD_SLICE对象来容纳slice()startstop值。

关注step操作码,我们发现:

BINARY_SUBSCR

因此,Python占据了堆栈的顶部,并将其放入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()引用放入list,然后呼叫container。而已。然后,下一步是找到PyObject_GetItem(container, sub)。这是在abstract.c中定义的,它要做的第一件事是查看对象是否为映射类型:

PyObject_GetItem()

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; } 是对象类(->ob_type相同),并且->tp_as_mappingmapping protocol is supported时设置。如果支持映射协议,则调用type(object)作为列表对象,而m->mp_subscript(o, key);是列表映射支持定义。{p>

列表支持此协议,因为仅此协议支持切片对象(它实现sequence protocol as well,但仅接受整数和更古老,更有限的切片形式,仅包含两个索引)。我们可以看到o通过寻找tp_as_mapping entry in the PyTypeObject PyList_Type type definition来实现协议:

m

指向list_as_mapping,其定义为

list

所以现在我们知道在哪里可以找到索引操作的实际实现了;该实现由list_subscript() function处理,list_new_prealloc()使用PyTypeObject PyList_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "list", sizeof(PyListObject), 0, // ... &list_as_mapping, /* tp_as_mapping */ // ... }; 检查static PyMappingMethods list_as_mapping = { (lenfunc)list_length, (binaryfunc)list_subscript, (objobjargproc)list_ass_subscript }; 对象,然后仅创建一个新的列表对象并在以下位置复制索引:

PySlice_Check()

对于步长为slice()的非空切片,采用if (slicelength <= 0) { return PyList_New(0); } else if (step == 1) { return list_slice(self, start, stop); } else { result = list_new_prealloc(slicelength); if (!result) return NULL; src = self->ob_item; dest = ((PyListObject *)result)->ob_item; for (cur = start, i = 0; i < slicelength; cur += (size_t)step, i++) { it = src[cur]; Py_INCREF(it); dest[i] = it; } Py_SIZE(result) = slicelength; return result; } 分支,其中PySlice_Unpack()创建一个新的预分配列表以容纳所有切片索引,然后使用-1的{​​{1}}循环来生成索引以复制到新列表中。

对于您的else切片,将for对象传递到cur += (size_t)step对象中,并且list[::-1]使用PySlice_AdjustIndices() functions和{{3}}来将其转换为具体的listslice(None, None, -1)list_subscriptslicelength值;对于负数startstopstep都设置为step,最终结果是start设置为stop,{{1 }}设置为None,而slicelength设置为len(list)

因此,对上述start循环的影响是将所有所有元素(从索引len(list) - 1开始并迭代到索引stop)复制到到新的列表对象。

因此,-1的反向操作是一个O(N)操作,用于列表的长度N(这包括预分配,这需要多达O(N)的时间才能为列表的引用分配内存)新列表)。