填充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. 使用Docx.dll插入图片在Centos 7 上报错:system.DllNotFound:libgdiplus.so

    熬了N天,终于把WORD 文档打开替换.插入图片完好,部署,本机测试没有问题 可是一放到服务器(Centos 7) 就提示下面的错误: 度娘一下: https://www.cnblogs.com/xi ...

  2. 【Android开发笔记】程序崩溃异常总结

    广播注册相关(broadcastReceiver) 没有注册广播就注销广播 注册广播但未注销广播 注册广播后重复注销广播 解决办法: 添加一个布尔变量,注册广播后为true,若为true在执行注销,注 ...

  3. 奇怪的Unrooted Tests错误

    错误如图: 条件如下: Eclipse里的Maven工程. 使用JUnit4(这个是否必须不知,反正我的工程用的4) 修改某个Test类里的方法名,或者增加一个Test方法. 现象: 在MyEclip ...

  4. Oracle grant connect, resource to user语句中的权限

    博主在 Oracle 11g r2上测试(测试日期:2017.10.30): 用sys登陆到oracle中,执行以下两条语句: select * from role_sys_privs WHERE R ...

  5. Excel自动从身份证中提取生日、性别、年龄

    现在学生的身份证号已经全部都是18位的新一代身份证了,里面的数字都是有规律的.前6位数字是户籍所在地的代码,7-14位就是出生日期.第17位“2”代表的是性别,偶数为女性,奇数为男性.我们要做的就是把 ...

  6. JavaScript深拷贝与浅拷贝的理解

    个人是这么理解深拷贝和浅拷贝的:就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力. 一起看看我举的浅拷贝栗子: let ...

  7. GNU C中__attribute__

    __attribute__基本介绍: 1. __attribute__ 可以设置函数属性.变量属性和类型属性. 2. __attribute__ 语法格式为:__attribute__ ((attri ...

  8. mysql 索引的统计

    查看一个库里面没有使用过的索引select object_type,object_schema,object_name,index_name,count_star,count_read,COUNT_F ...

  9. Mysql--select基础查询

    基本语法:select 查询列表 from 表名 查询列表可以是表中字段.常量值.表达式.函数:查询的结果是一个虚拟的表格. 注意: ①sql语言大小写不敏感 ②关键字不能分行或略写 ③一般书写方式为 ...

  10. Linux-git安装

    基本操作 安装yum install git 生成SSH KEY :先cd ~/.ssh,在这个目录下输入ssh-keygen,一直回车就可以了,这个时候就会出现id_rsd.pub公钥和id_rsa ...