文章目录
在Python中我们使用地最多的容器应该就是list了,list具有非常强大的功能,它不仅拥有像Java等其他语言的数组一样通过下标索引来读取列表中元素的功能,而且又高于数组,因为它可以在容器中存储不同类型的数据。为什么列表会具有这么强大的功能呢?实际上,其底层就是一个数据结构中的线性表的顺序表结构,只不过,它并非简单的一体式存储,而是采用了分离式的顺序表结构,在后面的解析中我们会慢慢一步步揭开它的神秘面纱,那么当我们在利用列表容器来对一系列数据进行操作时,是否能够清楚地了解其底层做了哪些事情吗?接下来我们就来详细解析一下list的底层实现。
1、PyListObject对象
在之前的文章中我提到过,Python中的对象分为定长对象和变长对象,而list就是一种变长对象,很显然我们需要存储的数据不像Python2中的整数对象那样在一开始就已经确定了其大小,列表的长度是变化的。不仅如此,它还是可变对象,这和之前我们说的字符串对象不一样。列表在进行插入或者删除操作时可动态调整其维护的内存和元素。老规矩,我们来看一看它的定义:
// listobject.h
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
不错,从定义来看它确实是一个变长对象因为它具有PyObject_VAR_HEAD这个头部,这个宏拥有一个ob_size的变量,这个变量就是维护数据元素长度的,从之前的文章中我们应该很清楚了。ob_item是一个PyObject **类型的指针,这个指针指向了元素列表所在内存块的首地址。之前在解析字符串对象的时候我们知道其中有个变量ob_sval,它是一个长度为1的字符数组,它也指向字符串的第一个字符,同理,列表对象中的这个指针也是,list[0]实际上就是ob_item[0]. allocated这个变量是维护的列表中可容纳元素的长度。可能你会说PyObject_VAR_HEAD中的ob_size不也是维护的元素的长度吗?这不是多此一举吗?这是怎么回事?您先别急,且听我慢慢道来。
实际上,ob_size和allocated都是维护元素列表长度的,它们都与对象的内存管理机制相关,您看到后面就慢慢清楚了。我们知道,内存频繁申请会影响到程序的执行效率,性能会变得比较低下,如果采用存多大的数据就申请多大的内存的管理方式,其效率太低。因此,在为其申请内存的时候,会先申请一块内存,用allocated记录其数量,而ob_size变量则用来记录使用了多少数量的内存。也就是说假如一个PyListObject对象可容纳10个元素,其中已经存入了5个元素,那么,allocated为10,ob_size则为5。我们在利用len()这个函数计算列表长度时,实际上就是取出ob_size的值直接返回。
2、PyListObject对象的创建和维护
创建list对象是通过PyList_New这个函数来实现的,这个函数接收一个参数用于指定列表元素的个数:
// listobject.c
PyObject* PyList_New(int size)
{
PyListObject* op;
size_t nbytes;
// 内存溢出检查
nbytes = size * sizeof(PyObject *);
if(nbytes / sizeof(PyObject *) != (size_t) size)
return Py_Err_NoMemory();
// 为PyListObject对象申请内存空间
if (num_free_lists){
// 缓冲池可用
num_free_lists --;
op = free_lists[num_freee_lists];
_Py_New_Reference((PyObject *) op);
} else {
// 缓冲池不可用
op = PyObject_GC_New(PyListObject, &PyList_Type);
}
// 为PyListObject维护的元素列表申请空间
if (size <= 0)
op->ob_item = NULL;
else{
op->ob_item = (object **) PyMem_MALLOC(nbytes);
memset(op->ob_item, 0, nbytes);