填充tp_dict

Python虚拟机类机制之对象模型(一)这一章中,我们介绍了Python的内置类型type如果要完成到class对象的转变,有一个重要的步骤就是填充tp_dict对象,这是一个极其繁杂的过程

typeobject.c

int PyType_Ready(PyTypeObject *type)
{
PyObject *dict, *bases;
PyTypeObject *base;
Py_ssize_t i, n;
……
//设定tp_dict
dict = type->tp_dict;
if (dict == NULL) {
dict = PyDict_New();
if (dict == NULL)
goto error;
type->tp_dict = dict;
} //将与type相关的descriptor加入到tp_dict中
if (add_operators(type) < 0)
goto error;
if (type->tp_methods != NULL) {
if (add_methods(type, type->tp_methods) < 0)
goto error;
}
if (type->tp_members != NULL) {
if (add_members(type, type->tp_members) < 0)
goto error;
}
if (type->tp_getset != NULL) {
if (add_getset(type, type->tp_getset) < 0)
goto error;
}
……
}

    

在这个阶段,将完成("__add__", &nb_add)在tp_dict的映射。这个阶段的add_operators、add_methods、add_members、add_getset都是这样完成填充tp_dict的动作。那么,一个问题浮现了,Python虚拟机是如何知道"__add__"和nb_add之间存在关联呢?这种关联是在Python源代码中预先约定好的,存放在一个名为slotdefs的全局数组

slot与操作排序

在进入填充tp_dict的复杂操作之前,我们先来介绍Python内部的一个概念:slot。在Python内部,slot可以视为表示PyTypeObject中定义的操作,一个操作对应一个slot,但是slot不仅仅包含一个函数指针,它还包含其他一些信息。在Python内部,slot是通过slotdef这个结构体来实现的

//typeobject.c
typedef struct wrapperbase slotdef; //descrobject.h
struct wrapperbase {
char *name;
int offset;
void *function;
wrapperfunc wrapper;
char *doc;
int flags;
PyObject *name_strobj;
};

  

在一个slot中,存储着与PyTypeObject中一种操作相对应的各种信息。name就是操作对应的名称,如字符串__add__,offset则是操作的函数地址在PyHeapTypeObject中的偏移量,而function则指向一种称为slot function的函数,这里新引进一个类型PyHeapTypeObject,PyHeapTypeObject可是个好东西,它在以后分析用户自定义类型还会在用到,这里我们简单看一下PyHeapTypeObject的定义,以及PyType_Type中关于PyHeapTypeObject的定义,后续还会讲解PyHeapTypeObject

//object.h
typedef struct _heaptypeobject {
PyTypeObject ht_type;
PyNumberMethods as_number;
PyMappingMethods as_mapping;
PySequenceMethods as_sequence;
PyBufferProcs as_buffer;
PyObject *ht_name, *ht_slots;
} PyHeapTypeObject; //typeobject.c
PyTypeObject PyType_Type = {
……
sizeof(PyHeapTypeObject), /* tp_basicsize */
……
};

  

Python中提供了多个宏来定义一个slot,其中最基本的是TPSLOT和ETSLOT:

//typeobject.c
#define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
{NAME, offsetof(PyTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
PyDoc_STR(DOC)} #define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
{NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
PyDoc_STR(DOC)} //structmember.h
#define offsetof(type, member) ( (int) & ((type*)0) -> member )

  

TPSLOT和ETSLOT的区别在于TPSLOT计算的是操作对应的函数指针在PyTypeObject中的偏移量,而ETSLOT计算的是函数指针在PyHeadTypeObject中的偏移量。但是观察下面列出的PyHeadTypeObject的代码,可以发现,因为PyHeadTypeObject的第一个域就是PyTypeObject,所以TPSLOT计算出的偏移量实际上也就是相对于PyHeadTypeObject的偏移量

对于一个PyTypeObject来说,有的操作,比如nb_add,其函数指针在PyNumberMethods中存放,而PyTypeObject中却是通过一个tp_as_number指针指向另一个PyNumberMethods结构,所以,实际上根本没有办法算出nb_add在PyTypeObject中的偏移量,只能计算其在PyHeadTypeObject这种的偏移量

因此,与nb_add对应的slot必须是通过ETSLOT来定义的。如果说与nb_add对应的slot中的记录的offset是基于PyHeadTypeObject的,而PyInt_Type却是一个PyTypeObject,那么显然通过这个偏移量是不可能得到PyInt_Type中为int准备的nb_add,那么这个offset有什么用呢?

其实这个offset是用来排序的,为了理解为什么需要对操作进行排序,需要来看看Python预定义的slot集合——slotdefs

typeobject.c

……
#define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
{NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
PyDoc_STR(DOC)}
#define SQSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
ETSLOT(NAME, as_sequence.SLOT, FUNCTION, WRAPPER, DOC)
#define MPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
ETSLOT(NAME, as_mapping.SLOT, FUNCTION, WRAPPER, DOC)
#define BINSLOT(NAME, SLOT, FUNCTION, DOC) \
ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_l, \
"x." NAME "(y) <==> x" DOC "y")
#define RBINSLOT(NAME, SLOT, FUNCTION, DOC) \
ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_r, \
"x." NAME "(y) <==> y" DOC "x")
……
static slotdef slotdefs[] = {
……
//不同操作名对应相同操作
BINSLOT("__add__", nb_add, slot_nb_add,
"+"),
RBINSLOT("__radd__", nb_add, slot_nb_add,
"+"),
//相同操作名对应不同操作
SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item,
"x.__getitem__(y) <==> x[y]"),
MPSLOT("__getitem__", mp_subscript, slot_mp_subscript,
wrap_binaryfunc,
"x.__getitem__(y) <==> x[y]"),
……
};

  

其中,SQSLOT、MPSLOT、BINSLOT、RBINSLOT都是对ETSLOT的一个简单的包装,在slotdefs中,可以发现,操作名(比如__add__)和操作并不是一一对应的,存在着多个操作对应同一操作名的情况,同样也存在着同一个操作对应不同操作名的情况。对于相同操作名对应不同操作的情况,在填充tp_dict时,就会出现问题,比如对于__getitem__,在填充tp_dict中与其对应的是sq_item还是mp_subscript呢?

为了解决这个问题,就需要利用slot中的offset信息对slot(也就是对操作)进行排序。回顾一下前面的PyHeadTypeObject的代码,它与一般的struct定义不同,其实定义中各个域的顺序也是相当关键的,在顺序中隐含着操作优先级的信息。比如在PyHeadTypeObject中,PyMappingMethods的位置在PySequenceMethods之前,mp_subscript是PyMappingMethods中的PyObject*,而sq_item是PySequenceMethods中的PyObject*,所以最终计算出的偏移存在如下的关系:offset(mp_subscript)<offset(sq_item)。

如果一个PyTypeObject中,既定义了mp_subscript又定义了sq_item,那么Python虚拟机将选择mp_subscript与__getitem__建立关系,而PyList_Type正是这样的一个PyTypeObject,在PyList_Type中,tp_as_mapping.mp_subscript指向list_subscript,而tp_as_sequence.sq_item指向list_item

listobject.c

PyTypeObject PyList_Type = {
……
&list_as_sequence, /* tp_as_sequence */
&list_as_mapping, /* tp_as_mapping */
……
}; static PySequenceMethods list_as_sequence = {
……
(ssizeargfunc)list_item, /* sq_item */
……
}; static PyMappingMethods list_as_mapping = {
(lenfunc)list_length,
(binaryfunc)list_subscript,
(objobjargproc)list_ass_subscript
};

  

现在,让我们在list_item和list_subscript两个方法中添加打印语句,看看究竟是执行list_item还是执行list_subscript

listobject.c

static PyObject * list_subscript(PyListObject* self, PyObject* item)
{
printf("call list_subscript\n");
if (PyIndex_Check(item)) {
……
}
……
} static PyObject * list_item(PyListObject *a, Py_ssize_t i)
{
printf("call list_item\n");
if (i < 0 || i >= a->ob_size) {
……
}
……
}

  

因为Python对list的索引元素的操作有优化,所以我们必须用一个类继承自list才能看到效果,A中的__getitem__对应的操作就是对PyList_Type中的mp_subscript和sq_item选择的结果,可以看到,list_subscript被选中了,但后面还跟着一个list_item。为什么会出现这样的情况?是因为在list_subscript函数中还调用了list_item

>>> a = A()
>>> a.append(1)
>>> print(a[0])
call list_subscript
call list_item
1

  

slotdefs的排序在init_slotdefs中完成:

typeobject.c

static void init_slotdefs(void)
{
slotdef *p;
static int initialized = 0;
//init_slotdefs只会进行一次
if (initialized)
return;
for (p = slotdefs; p->name; p++) {
//填充slotdef结构体中name_strobj
p->name_strobj = PyString_InternFromString(p->name);
if (!p->name_strobj)
Py_FatalError("Out of memory interning slotdef names");
}
//对slotdefs中的slotdef进行排序
qsort((void *)slotdefs, (size_t)(p-slotdefs), sizeof(slotdef),
slotdef_cmp);
initialized = 1;
} //slot排序的比较策略
static int slotdef_cmp(const void *aa, const void *bb)
{
const slotdef *a = (const slotdef *)aa, *b = (const slotdef *)bb;
int c = a->offset - b->offset;
if (c != 0)
return c;
else
return (a > b) ? 1 : (a < b) ? -1 : 0;
}

  

在slot的排序策略函数slotdef_cmp中,可以清晰地看到,slot中的offset正是操作排序的关键所在

Python虚拟机类机制之填充tp_dict(二)的更多相关文章

  1. Python虚拟机类机制之instance对象(六)

    instance对象中的__dict__ 在Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式. # 首先寻 ...

  2. Python虚拟机类机制之descriptor(三)

    从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...

  3. Python虚拟机类机制之绑定方法和非绑定方法(七)

    Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...

  4. Python虚拟机类机制之从class对象到instance对象(五)

    从class对象到instance对象 现在,我们来看看如何通过class对象,创建instance对象 demo1.py class A(object): name = "Python&q ...

  5. Python虚拟机类机制之对象模型(一)

    Python对象模型 在Python2.2之前,Python中存在着一个巨大的裂缝,就是Python的内置类type,比如:int和dict,这些内置类与程序员在Python中自定义的类并不是同一级别 ...

  6. Python虚拟机类机制之自定义class(四)

    用户自定义class 在本章中,我们将研究对用户自定义class的剖析,在demo1.py中,我们将研究单个class的实现,所以在这里并没有关于继承及多态的讨论.然而在demo1.py中,我们看到了 ...

  7. Python虚拟机函数机制之名字空间(二)

    函数执行时的名字空间 在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题.在执行MAKE_FUNCTION指 ...

  8. Python虚拟机函数机制之位置参数的默认值(五)

    位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...

  9. Python虚拟机函数机制之扩展位置参数和扩展键参数(六)

    扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...

随机推荐

  1. java中的递归思想及应用

    递归就是自己调自己,最需要注意的就是结束条件,否则可能就是死循环,导致内存溢出 public T a(Object x,Object y) { if(条件true) { a(x1,y1); } els ...

  2. Day5 CSS基本样式和C3选择器

    Day5 CSS基本样式和C3选择器 一.背景属性 1.背景颜色            background-color:transparent(默认值,透明); 颜色的取值:            ...

  3. (转载)C#中的lock关键字

    lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.这是通过在代码块运行期间为给定对象获取互斥锁来实现的. 先来看看执行过程,代码示例如下: 假设线程A先执行,线程B稍微慢一点.线程A执 ...

  4. intellij idea中设置SVN插件教程

    1.选择VCS→Browser VCS Repository→Browse Subversion Repository 2.在弹出的SVN Repository菜单中,选择左上角的绿色“+”号,填写S ...

  5. 在vue-cli中引入图片不能正常显示

    我们用vue-cli构建项目的时候,图片的地址是后台的,可是在template中item.img放到src中是不能正常显示的为什么? 原因是:url-loader无法解析js动态生成的路径. 解决: ...

  6. HDU3954 线段树(区间更新 + 点更新)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3954 , 一道比较好的线段树题,值得做. 题目是NotOnlySuccess大神出的,借此题来膜拜一下 ...

  7. IOS tableView 去除分割线 和 不允许选中表格cell

    //去除分割线 self.tableView.backgroundColor=[UIColor colorWithRed:///255.0 alpha:1.0]; self.tableView.sep ...

  8. 如何修改集群的公网信息(包括 VIP) (文档 ID 1674442.1)

    适用于: Oracle Database - Enterprise Edition - 版本 11.2.0.3 到 12.1.0.2 [发行版 11.2 到 12.1]本文档所含信息适用于所有平台 用 ...

  9. mac 远程桌面提示: 证书或相关链无效

    左上角 RDC -->首选项-->安全性-->即使验证失败,也始终链接

  10. Centos 编译安装bind错误

    [root@linux-node1 tmp]# tar xf bind-9.11.1-P1.tar.gz [root@linux-node1 tmp]# cd bind-9.11.1-P1 [root ...