深入理解Python虚拟机:元组实现原理及源码分析对于列表的实现,在Python中,元组是一个非常常用的数据类型。在本文中,我们将深入分析这是如何实现的。本节元组的结构主要介绍python中元组的数据结构:typedefstruct{PyObject_VAR_HEADPyObject*ob_item[1];/*ob_item包含“ob_size”元素的空间。*项目通常不能为NULL,除非在构造期间*元组在构建它的函数之外尚不可见。*/}PyTupleObject;#definePyObject_VAR_HEADPyVarObjectob_base;typedefstruct{PyObjectob_base;py_ssize_tob_size;;typedefstruct_object{_PyObject_HEAD_EXTRAPy_ssize_tob_refcnt;struct_typeobject*ob_type;}PyObject;从上面的数据结构来看,和list的数据结构基本类似,最终的使用方法也类似。将上面的结构展开后,PyTupleObject的结构大致如下:现在解释一下上面字段的含义:Py_ssize_t,整型数据类型。ob_refcnt,表示对象的引用计数数,对垃圾回收很有用。后面我们会深入分析虚拟机的垃圾回收部分。ob_type,表示这个对象的数据类型。在python中,有时候需要判断数据的数据类型,比如isinstance和type,这两个关键字都会用到这个字段。ob_size,该字段表示这个元组中有多少个元素。ob_item,这是一个指针,指向实际保存python对象数据的地址。它们之间大概的内存布局如下:需要注意的是tuple的数组大小是不能改变的,这和list的不一样,我们可以注意到data中有一个allocatedfield列表的结构,但在元组中不存在,主要是因为元组的数组大小是固定的,而列表的数组大小是可以改变的。元组操作函数源码分析创建元组首先我们要了解cpython内部元组内存分配的问题。首先,和list一样,在cpython中释放分配的元组时,不会直接执行。而是先保存起来,下次有其他元组申请内存时,直接归还内存即可。cpython内部会缓存的元组大小为20,如果元组长度为0-19,申请内存分配后不会直接释放,而是先保存,下次有有需求,直接Assignment,无需申请。在cpython内部,相关定义如下:staticPyTupleObject*free_list[PyTuple_MAXSAVESIZE];staticintnumfree[PyTuple_MAXSAVESIZE];free_list,保存指针——指向被释放的元组。numfree,对应的下标表示元组中元素的个数,numfree[i]表示第i个元素的元组个数。下面是新建元组对象的源程序:PyObject*PyTuple_New(Py_ssize_tsize){PyTupleObject*op;Py_ssize_t我;如果(大小<0){PyErr_BadInternalCall();返回空值;}#ifPyTuple_MAXSAVESIZE>0//Ifanempty元组对象当前free_list中是否有空元组对象,如果存在则直接返回if(size==0&&free_list[0])kop=free_list[0];Py_INCREF(操作);返回(PyObject*)操作;}//如果元组中的对象元素个数小于20且对应的free_list中还有剩余的元组对象,则不分配内存直接返回if(sizeob_item[0];numfree[大小]--;/*内联PyObject_InitVar*/_Py_NewReference((PyObject*)op);//_Py_NewReference这个宏是对象op的引用计数设置为1}else#endif{/*检查溢出*///如果元组中的元素个数大于等于20或者没有当前free_list中的剩余对象,如果((size_t)size>((size_t)PY_SSIZE_T_MAX-sizeof(PyTupleObject)-则需要申请内存sizeof(PyObject*))/sizeof(PyObject*)){//如果元组长度大于某个值,直接报内存错误returnPyErr_NoMemory();}//申请元组大小的内存空间op=PyObject_GC_NewVar(PyTupleObject,&PyTuple_Type,size);如果(op==NULL)返回NULL;}//初始化内存空间(i=0;iob_item[i]=NULL;#ifPyTuple_MAXSAVESIZE>0//因为size==0的元组不会被修改,所以可以直接将申请的对象放入free_list中,以备后续使用if(size==0){free_list[0]=op;++numfree[0];Py_INCREF(操作);/*额外的INCREF,这样它就永远不会被释放*/}#endif_PyObject_GC_TRACK(op);//_PyObject_GC_TRACK这个宏是把对象op放入垃圾回收队列中,直接退货;如果不是,则进行内存分配,然后初始化请求的内存空间。如果size==0,新分配的元组对象可以放在free_list中。查看元组长度的功能比较简单,用cpython中的宏Py_SIZE即可。他的宏定义为\#definePy_SIZE(ob)(((PyVarObject*)(ob))->ob_size)。staticPy_ssize_ttuplelength(PyTupleObject*a){returnPy_SIZE(a);}元组是否包含数据其实和列表一样,就是遍历元组中的数据,然后进行比较。staticinttuplecontains(PyTupleObject*a,PyObject*el){Py_ssize_ti;国际运动会;对于(i=0,cmp=0;cmp==0&&i=Py_SIZE(op)){PyErr_SetString(PyExc_IndexError,"元组索引超出范围");返回空值;}return((PyTupleObject*)op)->ob_item[i];}intPyTuple_SetItem(PyObject*op,Py_ssize_ti,PyObject*newitem){PyObject*olditem;对象**p;如果(!PyTuple_Check(op)||op->ob_refcnt!=1){Py_XDECREF(newitem);PyErr_BadInternalCall();返回-1;}if(i<0||i>=Py_SIZE(op)){Py_XDECREF(newitem);PyErr_SetString(PyExc_IndexError,"元组赋值索引超出范围");返回-1;}p=((PyTupleObject*)op)->ob_item+i;旧项目=*p;*p=新项目;Py_XDECREF(旧项目);return0;}释放元组内存空间我们在进行垃圾回收的时候,需要在判断一个对象的引用计数等于0的时候释放这块内存空间(相当于析构函数),下面是释放元组内存空间的函数staticvoidtupledealloc(PyTupleObject*op){Py_ssize_ti;Py_ssize_tlen=Py_SIZE(op);PyObject_GC_UnTrack(操作);//PyObject_GC_UnTrack将对象从垃圾回收队列中移出ExceptPy_TRASHCAN_SAFE_BEGIN(op)if(len>0){i=len;while(--i>=0)//从这个元组指向的对象的引用计数减一Py_XDECREF(op->ob_item[i]);#ifPyTuple_MAXSAVESIZE>0//如果这个元组对象满足条件添加到free_list,然后将此元组对象添加到free_listif(lenob_item[0]=(PyObject*)free_list[len];numfree[len]++;free_list[len]=op;完成;/*返回*/}#endif}Py_TYPE(op)->tp_free((PyObject*)op);done:Py_TRASHCAN_SAFE_END(op)}在回收元组的内存空间时,主要有以下步骤:从垃圾回收列表中移除元组对象。将元组指向的所有对象的引用计数减一。判断元组是否满足保存在free_list中的条件,如果满足则加入free_list,否则回收内存。加入free_list的整个tuple中ob_item指向的变化如下:如果释放的tuple对象不能加入free_list,否则内存会被释放回收。小结本文主要介绍如何在cpython中实现元组,以及相关的数据结构和一些基本函数。最后简单说一下元组内存的释放,这里面还涉及到一些其他的知识点,本文无法分析,本文的内存分配主要针对元组,主要分析内存分配和元组的free_list是如何交互的。本文是深入理解python虚拟机系列文章之一。文章地址为:https://github.com/Chang-LeHung/dive-into-cpython更多精彩内容合集可访问:https://github.com/Chang-LeHung/CSCore关注公众号:废柴研究僧,多学点计算机(Java,Python,计算机系统基础,算法和数据结构)。