How does a pop (index) operation work in Python?
Question:
Below is a pop()
operation of Python written in C.
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
/*[clinic end generated code: output=6bd69dcb3f17eca8 input=b83675976f329e6f]*/
{
PyObject *v;
int status;
if (Py_SIZE(self) == 0) {
/* Special-case most common failure cause */
PyErr_SetString(PyExc_IndexError, "pop from empty list");
return NULL;
}
if (index < 0)
index += Py_SIZE(self);
if (!valid_index(index, Py_SIZE(self))) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
v = self->ob_item[index];
if (index == Py_SIZE(self) - 1) {
status = list_resize(self, Py_SIZE(self) - 1);
if (status >= 0)
return v; /* and v now owns the reference the list had */
else
return NULL;
}
Py_INCREF(v);
status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
if (status < 0) {
Py_DECREF(v);
return NULL;
}
return v;
}
I understand that pop(index)
takes O(N)
time. But there’s no for
statement here.
It just returns v
.
If pop(0)
, does Python move all the pointers inside the list forward one by one?
Answers:
Recall that at the C level, a list
is an array of pointers to the objects contained in the list.
In the code above, it calls list_ass_slice()
which literally just calls memmove()
to relocate the pointers after the popped element.
From Objects/listobject.c:list_ass_slice()
:
if (d < 0) { /* Delete -d items */
Py_ssize_t tail;
tail = (Py_SIZE(a) - ihigh) * sizeof(PyObject *);
memmove(&item[ihigh+d], &item[ihigh], tail);
The O(N)
is a reflection of the time it takes to relocate (up to) N
objects of sizeof(PyObject *)
bytes.
Below is a pop()
operation of Python written in C.
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
/*[clinic end generated code: output=6bd69dcb3f17eca8 input=b83675976f329e6f]*/
{
PyObject *v;
int status;
if (Py_SIZE(self) == 0) {
/* Special-case most common failure cause */
PyErr_SetString(PyExc_IndexError, "pop from empty list");
return NULL;
}
if (index < 0)
index += Py_SIZE(self);
if (!valid_index(index, Py_SIZE(self))) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
v = self->ob_item[index];
if (index == Py_SIZE(self) - 1) {
status = list_resize(self, Py_SIZE(self) - 1);
if (status >= 0)
return v; /* and v now owns the reference the list had */
else
return NULL;
}
Py_INCREF(v);
status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
if (status < 0) {
Py_DECREF(v);
return NULL;
}
return v;
}
I understand that pop(index)
takes O(N)
time. But there’s no for
statement here.
It just returns v
.
If pop(0)
, does Python move all the pointers inside the list forward one by one?
Recall that at the C level, a list
is an array of pointers to the objects contained in the list.
In the code above, it calls list_ass_slice()
which literally just calls memmove()
to relocate the pointers after the popped element.
From Objects/listobject.c:list_ass_slice()
:
if (d < 0) { /* Delete -d items */
Py_ssize_t tail;
tail = (Py_SIZE(a) - ihigh) * sizeof(PyObject *);
memmove(&item[ihigh+d], &item[ihigh], tail);
The O(N)
is a reflection of the time it takes to relocate (up to) N
objects of sizeof(PyObject *)
bytes.