从slot到descriptor

在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__getitem__关联在一起的,一定不会是一个slot,原因很简单,slot不是一个PyObject,它不能存放在dict对象中。当然,我们再深入思考一下,会发现slot也不会被“调用”。既然slot不是一个PyObject,那么它就没有type,也就无从谈起什么tp_call了,所以slot是无论如何也不满足前面所描述的Python的“可调用”这个概念

前面我们说过,Python虚拟机会在tp_dict找到__geiitem__对应的操作后,调用该操作,所以在tp_dict中与__getitem__对应的只能是另一个包装了slot的PyObject,在Python中,我们称为descriptor

在Python内部,存在多种descriptor,与PyTypeObject中的操作对应的是PyWrapperDescrObject。在此后的描述,我们将用术语descriptor来专门表示PyWrapperDescrObject。一个descriptor包含一个slot,其创建是通过PyDescr_NewWrapper

descrobject.h

#define PyDescr_COMMON \PyObject_HEAD \PyTypeObject *d_type; \PyObject *d_nametypedef struct {PyDescr_COMMON;struct wrapperbase *d_base;void *d_wrapped; /* This can be any function pointer */
} PyWrapperDescrObject;

  

descrobject.c

static PyDescrObject *
descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
{PyDescrObject *descr;//申请空间descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);if (descr != NULL) {Py_XINCREF(type);descr->d_type = type;descr->d_name = PyString_InternFromString(name);if (descr->d_name == NULL) {Py_DECREF(descr);descr = NULL;}}return descr;
}PyObject *
PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped)
{PyWrapperDescrObject *descr;descr = (PyWrapperDescrObject *)descr_new(&PyWrapperDescr_Type,type, base->name);if (descr != NULL) {descr->d_base = base;descr->d_wrapped = wrapped;}return (PyObject *)descr;
}

  

Python内部的各种descriptor都将包含PyDescr_COMMON,其中的d_type被设置为PyDescr_NewWrapper的参数type,而d_wrapped则存放着最重要的信息:操作对应的函数指针,比如对于PyList_Type来说,其tp_dict["__getitem__"].d_wrapped就是&mp_subscript。而slot则被存放在了d_base中

PyWrapperDescrObject的type是PyWrapperDescr_Type,其中的tp_call是wrapperdescr_call,当Python虚拟机调用一个descriptor时,也就会调用wrapperdescr_call,对于descriptor的调用过程,后面还会详细解析

建立联系

排序后的结果仍然存放在slotdefs中,Python虚拟机就可以从头到尾遍历slotdefs,基于每一个slot建立一个descriptor,然后在tp_dict中建立从操作名到descriptor的关联,这个过程在add_operators中完成

typeobject.c

static int add_operators(PyTypeObject *type)
{PyObject *dict = type->tp_dict;slotdef *p;PyObject *descr;void **ptr;//对slotdefs进行排序init_slotdefs();for (p = slotdefs; p->name; p++) {//如果slot中没有指定wrapper,则不处理if (p->wrapper == NULL)continue;//获得slot对应的操作在PyTypeObject中的函数指针ptr = slotptr(type, p->offset);if (!ptr || !*ptr)continue;//如果tp_dict中存在操作名,则放弃if (PyDict_GetItem(dict, p->name_strobj))continue;//创建descriptordescr = PyDescr_NewWrapper(type, p, *ptr);if (descr == NULL)return -1;//将(操作名,descriptor)放入tp_dict中if (PyDict_SetItem(dict, p->name_strobj, descr) < 0)return -1;Py_DECREF(descr);}if (type->tp_new != NULL) {if (add_tp_new_wrapper(type) < 0)return -1;}return 0;
}

  

在add_operators中,首先会调用前面剖析过的init_slotdefs函数进行排序,然后遍历排序完后的slotdefs结构体数组,对其中每一个slot(slotdef),通过slotptr获得该slot对应的操作在PyTypeObject中的函数指针,并接着创建descriptor,在tp_dict中建立从操作名(slotdef.name_strobj)到操作(descriptor)的关联

需要注意的是,在创建descriptor之前,Python虚拟机会检查在tp_dict中操作名是否已存在,如果已经存在,则不会再次建立从操作名到操作的关联。正是这种检查机制与上面的排序机制相结合,使得Python虚拟机能够在拥有相同操作名的多个操作中选择优先级最高的操作

在add_operators中,上面描述的动作都很直观、简单。而最难的动作隐藏在slotptr这个函数中,它的功能是完成slot到slot对应操作的真实函数指针的转换。我们已经知道,在slot中存放着操作的offset,但很不幸,这个offset是相对于PyHeadTypeObject的偏移,而操作的真实函数指针则在PyTypeObject中指定。更不幸的是,PyTypeObject和PyHeadTypeObject不是同构的,因为PyHeadTypeObject中包含了PyNumberMethods结构体,而PyTypeObject中只包含了PyNumberMethods*指针。所以slot中存储的这个关于操作的offset对于PyTypeObject来说,不可能直接使用,必须通过转换

举个例子,假如说调用slotptr(&PyList_Type, offset(PyHeadTypeObject, mp_subscript)),首先判断这个偏移大于offset(PyHeadTypeObject, as_mapping),所以会先从PyTypeObject对象中获得as_mapping指针P,然后在P的基础上进行偏移就可以得到实际的函数地址了,而偏移量delta为:

delta = offset(PyHeadTypeObject, mp_subscript) - offset(PyHeadTypeObject, as_mapping)

  

这个复杂的转换过程在slotptr中完成:

typeobject.c

static void ** slotptr(PyTypeObject *type, int ioffset)
{char *ptr;long offset = ioffset;assert(offset >= 0);assert((size_t)offset < offsetof(PyHeapTypeObject, as_buffer));//判断从PyHeapTypeObject中排后面的PySequenceMethods开始if ((size_t)offset >= offsetof(PyHeapTypeObject, as_sequence)) {ptr = (char *)type->tp_as_sequence;offset -= offsetof(PyHeapTypeObject, as_sequence);}else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_mapping)) {ptr = (char *)type->tp_as_mapping;offset -= offsetof(PyHeapTypeObject, as_mapping);}else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_number)) {ptr = (char *)type->tp_as_number;offset -= offsetof(PyHeapTypeObject, as_number);}else {ptr = (char *)type;}if (ptr != NULL)ptr += offset;return (void **)ptr;
}

  

为什么判断首先从PySequenceMethods开始,然后向前,依次判断PyMappingMethods和PyNumberMethods呢?假如我们先从PyNumberMethods开始判断,如果一个操作的offset大于PyHeadTypeObject中as_number在PyNumberMethods的偏移量,那么我们还是没有办法确定在这个操作是属于PyNumberMethods还是属于PyMappingMethods或PySequenceMethods。只有从后往前进行判断,才能解决这个问题

现在,我们摸清楚Python在改造PyTypeObject时对tp_dict做了什么,图1-1显示了PyList_Type完成初始化之后的整个布局,其中包括我们讨论的descriptor和slot

图1-1   add_operators完成之后的PyList_Type

在图1-1中,PyList_Type.tp_as_mapping中延伸出去的部分是在编译时已经确定好的,而从tp_dict中延伸出去的部分是在Python运行时环境初始化才建立的。

PyType_Ready在通过add_operators添加了PyTypeObject对象中定义了的一些操作后,还会通过add_methods、add_members、add_getset添加在PyTypeObject中定义的tp_methods、tp_members和tp_getset函数集,这些过程与add_operators类似,不过最后添加到tp_dict中的descriptor就不再是PyWrapperDescrObject,而分别是PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject

图1-1所显示的class对象大部分正确,但还不算全部正确,考虑下面的例子:

>>> class A(list):
...     def __repr__(self):
...         return "Python"
...
>>> s = "%s" % A()
>>> s
'Python'

  

熟悉Python的人都知道,__repr__是Python中的特殊方法。当Python执行表达式"s = '%s' %A()"时,最终会调用A.tp_repr。如果按照图1-1的布局,并且对照PyList_Type,那么就应该调用list_repr这个函数,但并不是这样的,Python虚拟机最终调用的是A中重写后的__repr__。这意味着,Python在初始化A时,对tp_repr进行了特殊处理。为什么Python虚拟机会知道要对tp_repr进行特殊处理呢?答案还是在slot身上

在slotdefs中,有一条slot为TPSLOT:

typeobject.c

TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc, "x.__repr__() <==> repr(x)")

  

Python虚拟机在初始化A时,会检查<class A>的tp_dict中是否存在__repr__。在后面剖析用户自定义的class对象时,我们会看到,因为在定义class A时重写__repr__这个操作,所以A.tp_dict中__repr__一开始就会存在,Python虚拟机会检测到它的存在。一旦检测到__repr__存在,Python虚拟机将tp_repr这个函数指针替换为slot中指定的&slot_tp_repr。所以当Python虚拟机调用A.tp_repr时,实际上执行的是slot_tp_repr

typeobject.c

static PyObject * slot_tp_repr(PyObject *self)
{PyObject *func, *res;static PyObject *repr_str;//[1]:查找__repr__属性func = lookup_method(self, "__repr__", &repr_str);if (func != NULL) {//[2]:调用__repr__对应的对象res = PyEval_CallObject(func, NULL);Py_DECREF(func);return res;}PyErr_Clear();return PyString_FromFormat("<%s object at %p>",self->ob_type->tp_name, self);
}

  

在slot_tp_repr中,会寻找__repr__属性对应的对象,正好就会找到我们在A中重写的函数,这个对象其实是一个PyFunctionObject。这样一来,就完成了对默认list的repr行为的替换,所以对A来说,其初始化结束后的内存布局则如图1-2所示:

图1-2   初始化完成后的A

当然,并是不会A中所有的操作都会有这样的变化。A的其他操作还是会指向PyList_Type中指定的函数,比如tp_iter还是会指向list_iter。对于A来说,这个变化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,对于内置class对象,不会进行这样的操作,这个操作是属于创建自定义class对象时的动作

对于A来说,这个变化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,对于内置class对象,不会进行这样的操作,这个操作是属于创建自定义class对象时的动作

确定MRO

所谓的MRO,即是指Method Resolve Order,更一般地,也是一个class对象的属性解析顺序。如果Python像java那样仅支持单继承,那就不是一个问题了。但是Python是支持多继承的,在多重继承时,就必须设置按照何种顺序解析属性,考虑如下Python代码:

>>> class A(list):
...     def show(self):
...         print("A::show")
...
>>> class B(list):
...     def show(self):
...         print("B::show")
...
>>> class C(A):
...     pass
...
>>> class D(C, B):
...     pass
...
>>> d = D()
>>> d.show()
A::show

  

由于D的基类A和B中都实现了show,那么在调用d.show()时,究竟是调用A的show方法还是B的show方法呢?Python内部在PyType_Ready中通过mro_internal函数完成了对一个类型的mro顺序的建立。Python虚拟机将创建一个tupple对象,在对象中依次存放着一组class对象。在tupple中,class对象的顺序就是Python虚拟机在解析属性时的mro顺序。最终这个tupple将被保存在PyTypeObject.tp_mro中

对于上述的class D,Python虚拟机会在内部创建一个list,其中根据D的声明依次放入D和它的基类,如图1-3所示:

图1-3   D建立mro列表时Python虚拟机内部的辅助list

注意在list的最后一项存放着一个包含所有D的直接基类列表。Python虚拟机将从左到右遍历该list,当访问到list中的任一个基类时,如果基类存在mro列表,则会转而访问基类的mro列表。在访问的过程中,不断将所访问到的class对象放入到D自身的mro列表中

我们跟踪这个遍历的过程来看一下:

  1. mro列表(tp_mro)中没有D,所以先获得D
  2. D的mro列表没有C,所以放入C,现在Python虚拟机发现C中存在mro列表,所以转而访问C的mro列表。:(1)D的mro列表中没有A,放入A;(2)接下来是list,这里需要注意,尽管D的mro列表没有list,但是后面的B的mro列表中出现了list,那么Python虚拟机会跳过这里的list,将list的获得推迟到处理B的mro列表时;(3)list之后是object,同样,将对object的处理推迟
  3. D的mro列表中没有B,所以放入B,转而访问B的mro列表:(1)处理list,这时可以将list放入D的mro列表;(2)处理object,这时可以将object放入D的mro列表

当遍历的过程结束后,D的mro列表也就存储了一个class对象的顺序列表了。从上面的遍历过程可以看到,这个列表是(D、C、A、B、list、object),我们可以来验证一下:

>>> for t in D.__mro__:
...     print(t)
...
<class '__main__.D'>
<class '__main__.C'>
<class '__main__.A'>
<class '__main__.B'>
<class 'list'>
<class 'object'>

  

图1-4   展示不同顺序下mro列表

继承基类操作

Python虚拟机确定了mro列表后,就会遍历mro列表(注意,由于第一个class对象的mro列表的第一项总是其自身,所以遍历是从第二项开始的)。在mro列表中实际上还存储的就是class对象的所有直接和间接基类,Python虚拟机会将class对象自身没有设置而基类中设置了的操作拷贝到class对象中,从而完成对基类操作的继承动作:

这个继承操作的动作发生在inherit_slots中

typeobject.c

int PyType_Ready(PyTypeObject *type)
{……bases = type->tp_mro;n = PyTuple_GET_SIZE(bases);for (i = 1; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b))inherit_slots(type, (PyTypeObject *)b);}……
}

  

在inherit_slots中,会拷贝相当多的操作,这里我们拿nb_add来做个例子:

typeobject.c

static void inherit_slots(PyTypeObject *type, PyTypeObject *base)
{PyTypeObject *basebase;#define SLOTDEFINED(SLOT) \(base->SLOT != 0 && \(basebase == NULL || base->SLOT != basebase->SLOT))#define COPYSLOT(SLOT) \if (!type->SLOT && SLOTDEFINED(SLOT)) type->SLOT = base->SLOT#define COPYNUM(SLOT) COPYSLOT(tp_as_number->SLOT)if (type->tp_as_number != NULL && base->tp_as_number != NULL) {basebase = base->tp_base;if (basebase->tp_as_number == NULL)basebase = NULL;COPYNUM(nb_add);……}……
}

  

我们知道PyBool_Type中并没有设置nb_add操作,但它的tp_base设置的是&PyInt_Type,而PyInt_Type中却设置了nb_add操作。所以我们可以在PyType_Ready中添加输出语句,当处理type分别为bool和int时,输出其nb_add的地址,进行验证。因为按照inherit_slots的结果,这两个地址应该都指向同一个地址,即int_add的地址

typeobject.c

int PyType_Ready(PyTypeObject *type)
{……for (i = 1; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b))inherit_slots(type, (PyTypeObject *)b);}//打印bool中的nb_add地址if (strcmp(type->tp_name, "bool") == 0) {printf("bool nb_add: 0x%X\n", *(type->tp_as_number->nb_add));}//打印int中的nb_add地址if (strcmp(type->tp_name, "int") == 0) {printf("int nb_add: 0x%X\n", *(type->tp_as_number->nb_add));}……
}

  

然后打开Python命令行,可以看到int类型和bool类型在初始化时打印其nb_add地址:

# ./python
int nb_add: 0x43C570
bool nb_add: 0x43C570

  

这个结果预示着Python中的两个bool对象,我们可以进行加法操作

填充基类中的子类列表

到这里,PyType_Ready还剩下最后一个重要的动作了:设置基类中的子类列表。在每一个PyTypeObject中,有一个tp_subclasses,这个东西在PyType_Type完成后将是一个list对象。其中存放着所有直接继承该类型的class对象。PyType_Ready通过调用add_subclass完成这个向tp_subclass中填充子类对象的动作

typeobject.c

int PyType_Ready(PyTypeObject *type)
{PyObject *dict, *bases;PyTypeObject *base;Py_ssize_t i, n;……bases = type->tp_bases;……n = PyTuple_GET_SIZE(bases);for (i = 0; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b) &&add_subclass((PyTypeObject *)b, type) < 0)goto error;}……
}

  

我们验证这个子类列表的存在:

>>> int.__subclasses__()
[<class 'bool'>]
>>> object.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>,……
>>>

  

果然,object是万物之母,很多的类都直接继承于object。可以看到,Python虚拟机对Python的内置类型对应的PyTypeObject进行了多种复杂的改造工作,总结一下,主要包括:

  • 设置type信息,基类及基类列表
  • 填充tp_dict
  • 确定mro列表
  • 基于mro列表从基类继承操作
  • 设置基类的子类列表

转载于:https://www.cnblogs.com/beiluowuzheng/p/9621918.html

Python虚拟机类机制之descriptor(三)相关推荐

  1. Python虚拟机函数机制之无参调用(一)

    PyFunctionObject对象 在Python中,任何一个东西都是对象,函数也不例外.函数这种抽象机制,是通过一个Python对象--PyFunctionObject来实现的 typedef s ...

  2. 【Python数据结构】 抽象数据类型 Python类机制和异常

    这篇是<数据结构与算法Python语言描述>的笔记,但是大头在Python类机制和面向对象编程的说明上面.我也不知道该放什么分类了..总之之前也没怎么认真接触过基于类而不是独立函数的Pyt ...

  3. Python虚拟机的函数机制

    本文参考Python源码剖析第11章: python中函数就是一个对象,函数这种抽象机制是通过PyFunctionObject这个对象来实现的. 对比PyCodeObject与PyFunctionOb ...

  4. Python源码学习:Python类机制分析

    Python源码分析 本文环境python2.5系列 参考书籍<<Python源码剖析>> 本文主要分析Python中类时如何实现的,在Python中,一切都是对象:任何对象都 ...

  5. python的类作用_python——类中的self到底的作用及三个应用场景

    class Test(object): def __init__ (self, val1): self.val0 = val1 def fun1(self): print(self.val0) def ...

  6. python描述符魔术方法_学习笔记-Python基础9-面向对象编程OOP-类的成员描述符(property)、类的常用内置属性、类的常用魔术方法、类和对象的三种方法...

    一.类的成员描述符(property) 类的成员描述符是为了在类中,对类的成员属性进行相关操作而创建的一种方式,大部分属于数据清洗 属性有三种操作:get获取属性值.set修改或添加属性.delete ...

  7. python定义一个学生类姓名、年龄、性别、成绩、你成绩评价_.定义一个学生类Student,包含三个属性姓名、年龄、性别,创建三个学生对象存入ArrayList集合中。...

    3.定义一个学生类Student,包含三个属性姓名.年龄.性别,创建三个学生对象存入ArrayList集合中. A:遍历集合遍历输出. B:求出年龄最大的学生,让后将该对象的姓名变为:王者荣耀. pa ...

  8. Python垃圾回收机制详解

    一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题. 在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. #e ...

  9. python虚拟机 基于寄存器_虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩...

    转载:http://rednaxelafx.iteye.com/blog/492667 大前天收到一条PM: 引用 你好,很冒昧的向你发短消 息,我现在在看JS引擎,能过看博客发现你对js engin ...

最新文章

  1. 用python绘制柱状图标题-使用Python绘制柱形竞赛图
  2. python 培训-Python培训
  3. ftime()函数的用法----算函数运行时间
  4. 【emWin】例程二十五:窗口对象——Iconview
  5. 业务服务管理究竟为何可望而不可及
  6. Go语言参数校验(go-playground / validator)
  7. POJ 3254 状态压缩DP
  8. 【终极完美高效】C语言实用算法系列之学生管理系统_单向链表外排序_堆内数组存储链表节点指针_函数指针数组
  9. 脉冲宽度调制pdm_PWM (脉冲宽度调制)原理与实现
  10. 把java复制到e_编写一个程序,将e:\java目录下的所有.java文件复制到e:\jad目录下...
  11. JSON数据、字符串拼接、宽字符处理、数组、Notice警告、isset和empty、变量、作用域、常量、include和require
  12. 对Lucene PhraseQuery的slop的理解
  13. C语言获取系统当前时间的两种方式
  14. 华为手机系统更新提示网络连接到服务器失败,华为网络正常app连不上网络
  15. 邓迎春绘画201702作品5
  16. 计划排布方式--甘特图
  17. oracle导入的表在哪,oracle导入导出表及表结构
  18. SVN仓库解决APIcloud2检出错误
  19. JS 校验车牌号码(全)
  20. linux 消息队列

热门文章

  1. Python 微信机器人-用itchat库向好友发送名片、转发名片实例演示
  2. C++ leetCode 1. 两数之和 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个
  3. 模拟电路技术之基础知识(五)
  4. CTFshow 反序列化 web267
  5. conj--复数的共轭值
  6. 径向基函数插值(3)二维数据的插值
  7. 【tf下载mnist数据】urlopen error [Errno 11004] getaddrinfo failed
  8. 猫狗收养所问题(指针模拟)
  9. 第一章:1.1 信号表征
  10. php mysql 开发微博_3款php+mysql国产微博程序推荐