我试图在python中反转一个列表。
那里有很多方法,[::-1]
似乎是一个很棒的方法!
但是我很好奇[::-1]
是如何完成的?它的时间复杂度是多少?
我在github中搜索了CPython存储库,但找不到任何线索。
我的搜索策略是在CPython存储库中搜索关键字::
。
由于python语法中有::
,即[::-1]
,因此CPython源代码中可能有::
,因此[::-1]
可以引用它并进行反转。这有道理吗?
答案 0 :(得分:8)
list[...]
使用的是索引,或更准确的是subscription syntax,而[start:stop:step]
是slicing。在这种情况下,Python 3将slice()
对象传递给订阅调用,CPython源代码中没有::
,因为这不是语言解析器如何威胁该部分。切片符号允许使用默认值,start:stop:step
部分之间的空白表示选择了默认值,而list[::-1]
将start
和stop
保留为默认值(用{{ 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()
,start
和stop
值。
关注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_mapping
在mapping 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}}来将其转换为具体的list
,slice(None, None, -1)
,list_subscript
和slicelength
值;对于负数start
和stop
和step
都设置为step
,最终结果是start
设置为stop
,{{1 }}设置为None
,而slicelength
设置为len(list)
。
因此,对上述start
循环的影响是将所有所有元素(从索引len(list) - 1
开始并迭代到索引stop
)复制到到新的列表对象。
因此,-1
的反向操作是一个O(N)操作,用于列表的长度N(这包括预分配,这需要多达O(N)的时间才能为列表的引用分配内存)新列表)。