Python虚拟机类机制之填充tp_dict(二)
填充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(二)的更多相关文章
- Python虚拟机类机制之instance对象(六)
instance对象中的__dict__ 在Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式. # 首先寻 ...
- Python虚拟机类机制之descriptor(三)
从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...
- Python虚拟机类机制之绑定方法和非绑定方法(七)
Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...
- Python虚拟机类机制之从class对象到instance对象(五)
从class对象到instance对象 现在,我们来看看如何通过class对象,创建instance对象 demo1.py class A(object): name = "Python&q ...
- Python虚拟机类机制之对象模型(一)
Python对象模型 在Python2.2之前,Python中存在着一个巨大的裂缝,就是Python的内置类type,比如:int和dict,这些内置类与程序员在Python中自定义的类并不是同一级别 ...
- Python虚拟机类机制之自定义class(四)
用户自定义class 在本章中,我们将研究对用户自定义class的剖析,在demo1.py中,我们将研究单个class的实现,所以在这里并没有关于继承及多态的讨论.然而在demo1.py中,我们看到了 ...
- Python虚拟机函数机制之名字空间(二)
函数执行时的名字空间 在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题.在执行MAKE_FUNCTION指 ...
- Python虚拟机函数机制之位置参数的默认值(五)
位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...
- Python虚拟机函数机制之扩展位置参数和扩展键参数(六)
扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...
随机推荐
- websocket的加密和解密
补充个小知识点:按位与运算 按位与运算是同位都为1才为1,有一个不为1就是0 websocket_hand import socket, base64, hashlib import websocke ...
- RING0到RING3
在前一篇文章里面,我们将了CPU保护模式中的几种特权RING0,RING1,RING2,RING3!操作系统通常运行在RING0,应用程序通常运行在RING3. CPU如何从RING0到RING3 先 ...
- 【转】Nginx跳转任意Http请求到Https
网站买了证书,绿条,多霸气! 那么自然得拦截http的访问方式了. 拦截http,301到https 各种Google,最后在Nginx官网找到例子,配置很简单,如下: server { listen ...
- vue组件总结(三)
一.什么是组件 组件(component)是Vue最强大的功能之一.组件可以扩展HTML元素,封装可重用的代码,根据项目需求,抽象出一些组件,每个组件里包含了展现.功能和样式.每个页面,根据自己的需要 ...
- 洛谷 P1951 收费站_NOI导刊2009提高(2)
题目描述 在某个遥远的国家里,有n个城市.编号为1,2,3,…,n. 这个国家的政府修建了m条双向的公路.每条公路连接着两个城市.沿着某条公路,开车从一个城市到另一个城市,需要花费一定的汽油. 开车每 ...
- 关于调用百度地图api在自己写的网页中实现和解决在https中正常显示
百度地图开发者:http://lbsyun.baidu.com/index.php?title=jspopular 我们打开百度地图开发者网站,注册一个百度账号,然后打开控制台,开始创建应用:(如果你 ...
- linux 命令——3 pwd (转)
Linux中用 pwd 命令来查看”当前工作目录“的完整路径. 简单得说,每当你在终端进行操作时,你都会有一个当前工作目录. 在不太确定当前位置时,就会使用pwd来判定当前目录在文件系统内的确切位置. ...
- python时间转换 ticks-FYI
#设a为字符串 import time a = "2011-09-28 10:00:00" #中间过程,一般都需要将字符串转化为时间数组 time.strptime(a,'%Y-% ...
- scrum敏捷开发☞
scrum是什么?说白了就是自带一些规则的工具,团队成员按照scrum的规则计划项目,执行项目,完成项目..可以让团队提高工作效率 当前除了scrum还有其他很多类似的像Kanban,XP,RUP(规 ...
- 【转载】WEB架构师成长之路
本人也是coding很多年,虽然很失败,但也总算有点失败的心得,不过我在中国,大多数程序员都是像我一样,在一直走着弯路,如果想成为一个架构师,就必须走正确的路,否则离目标越来越远,正在辛苦工作的程序员 ...