隐藏]
这个章节的目标是对可实现的各种类型方法以及它们可以做什么做一个概览。下面是PyTypeObject的定义,省略了debug构建的一些字段。
这里有许多的方法。但是不需要太担心,如果你有一个需要定义的类型,你只需要修改这些方法中的极少数。
正如你现在想到的,我们将重温这些方法并且给出各种处理的更多信息。我们不会按照这些字段在结构体中的顺序,因为这里有许多历史包袱影响了字段的排序,请保证你的类型字段是按照的正确顺序初始化的。找一个包含所有你需要的所有字段的例子是非常容易的(即使他们初始化为0),然后修改这些字段来符合你定义的新类型。
这个类型的名字 - 正如在上一章提到的,类型名将会出现在各个地方,几乎都是为了诊断的目的。在错误诊断的时候类型名是非常有用的。
这个字段用来告诉运行环境当创建这个类型的对象是需要分配多少内存。Python有一些内置的支持可变长度结构(思考:strings,lists),这是tp_itemsize的用武之地。这个我们稍后处理。
这里可以放置一个字符串,或者字符串的地址,这个字符串将会在Python脚本中调用obj.__doc__调用是返回的doc字符串。
现在我们回到基础类型方法,大多数的类型扩展将会被实现。
当你的类型实例引用计数减为0以及Python解释器想回收这个实例时调用这个函数。如果你的类型有需要释放的内存或其他需要清理的操作,将它放在这里。对象本身也需要在这里释放。下面是关于这个函数的例子:
关于释放内存函数的一个重要的要求是离挂起的异常远点。这是非常重要的,因为当解释器展开Python栈(unwinds the Python stack)时会频繁调用释放内存;当一个异常导致栈展开(Stack unwinding),没有任何操作保证释放内存看到了已经被设置的异常。释放内存任何操作都可能导致额外的Python代码检测到已经设置一个异常。这就导致从解释器产生误导性错误解释。正确的方法来防止这些的方法是在执行一些不安全的操作之前保存挂起的异常。这些操作可以使用PyErr_Fetch()和PyErr_Restore()函数。
在Python中,这里有两种方法来生成一个对象的文本表示:repr()函数和str()函数(print()函数只是调用str()函数)。这些处理都是可选的。
tp_repr处理应该为调用者返回一个包含该实例的描述的字符串对象。下面是一个简单的例子:
如果没有tp_repr处理被设置,解释器将会使用类型的tp_name和一个该对象的唯一标识作为描述。
tp_str是为str(),上面描述的tp_repr是为repr(),也就是说,当在Python中调用对象的实例的str()方法时这里的str()将会被调用。他的实现和tp_repr是非常相似的,但是返回的字符串是intended for human consumption(谁帮忙翻一下)。如果没有指定tp_str,将会使用tp_repr替代处理。
下面是一个简单的例子:
对于每个可以支持属性的对象,相应的类型必须提供函数来控制怎样处理这些属性。这里需要可以检索这些属性的函数(如果定义了属性),和其他属性设置(如果允许设置属性)。移除一个属性是一种特殊情况,这种情况传递给处理程序的是NULL。
Python支持两对类型的属性处理;支持属性的类型只需要实现一对函数。不同的是其中的一对需要属性名称作为一个char* ,而另一种接收PyObject*。每个类型可以根据使用方便选择使用哪对。
如果想访问一个对象的属性总是一个简单的操作(这个过会解释),通常可以使用属性管理提供的PyObject*版本。从Python2.2开始需要处理指定类型属性的函数已基本消失,虽然这里的例子尚未更新,但是使用新的机制的例子有很多。
大多数扩展类型仅仅使用简单的属性。那么,是什么让属性简单?这里有几个条件必须满足:
当PyType_Ready()被调用时属性名字必须是已知的
不需要做特殊处理来记录属性被查询或设置,或不需要对这个变量做任何处理。
注意:这个列表当变量被计算或相关数据如何存储时没有对属性的变量做任何的限制。
当PyType_Ready()被调用时,它使用类型对象的三个表来创建放在类型对象字典中的描述符。每个描述符控制实例对象中一个属性的访问。每个表格都是可选的;如果所有的表都是NULL,该类型的实例将会仅仅只有继承自基类型的属性,并且最好将tp_getattro和tp_setattro设置为NULL为好,允许基类型处理属性。
下面的表是类型对象申明的三个域:
如果tp_methods不是NULL,那么它必须指向一个PyMethodDef结构的数组。表中的每个元素都是这个结构的一个实例。
类型提供的每个方法应该定义一个条目;不需要为从基础类型继承来的方法定义条目。数组最后需要一个额外的条目来标志这个数组的结束(定点条目)。这个标志的ml_name域必须是NULL。
第二个表是用来定义在这个实例中直接映射到数据存储的属性。各种C类型都是被支持的,并且访问类型可能是只读或只写。表的数据结构定义为:
表中的每一项,描述符将会被构造并且添加到从实例结构体中可以提取一个变量的类型中。type域将会包含一个定义在structmember.h头文件的类型代码,这个变量被用来决定怎样从C变量转换为Python变量。flags域用来保存属性的访问方式标志。
下面的标志包含了定义在structmember.h的标志,它们也可以按位进行组合使用。
常量 含义
READONLY 不可写
READ_RESTRICTED 在受限模式下不可读
READ_RESTRICTED 在受限模式下不可写
RESTRICTED 在受限模式下不可读或不可写
使用tp_members表来构建运行时使用的描述符的一个有趣的优势是,这种方式定义的任何属性通过table提供的字符串这个属性可以拥有一个doc string。程序可以使用内省API来检索类对象的描述符,并且可以使用__doc__属性可以获取doc string。
至于tp_methods表,定点条目的name变量要求为NULL。
为了简单,这里只使用char *版本,name变量的类型唯一的不同就是char*和PyObject*。这个例子的效果和上面通用的例子一样,但是没有使用在Python2.2中添加的通用支持。他解释了如何处理函数调用,所以如果你确实需要扩展它们的函数,你需要明白需要做什么。
当一个对象请求一个属性查询时tp_getattr将会调用。类的__getattr__()方法也会在同样的场景下调用。
这里是一个例子:
当调用一个类实例的__setattr__()或__delattr()__方法调用时tp_setattr控制的函数将会调用。当一个属性可能被删除时,第三个参数将会是NULL。这里是一个简单抛出一个异常的例子;如果所有这些真是你想要的,tp_setattr应该设置为NULL。
当需要比较时将会调用tp_richcompare句柄。它类似于rich comparison methods、__lt__(),也象PyObject_RichCompare()和PyObject_RichCompareBool().
这个函数使用两个Python对象和operator作为参数调用,这里的参数是Py_EQ, Py_NE, Py_LE, Py_GT,Py_LT 或Py_GT之一。它应该根据指定操作来比较两个对象并且当比较成功是应该返回Py_True或Py_False。Py_NotImplimented来指出比较方法没有继承并且将会尝试其他对象的比较方法,或者NULL,如果一个异常被设置了。
这里有一个简单的实现,如果一个数据类型内部指针的大小是相等的则认为是相等的。
Python支持各种各样的抽象‘protocols’,使用这些接口特定的接口在文档AbstractObjects Layer.
许多这些抽象的接口在Python实现的发展中早就被定义了。特别是,number,mapping和sequence 协议在Python开始发展时就已经定义了。其他协议随着时间的推移也已经添加。协议依赖于一系列从类型继承的处理方法,旧版的协议将类型方法引用的处理方法定义为可选的程序块。新版的协议在主要的类型对象中添加了额外的插槽,通过一个标志来指示这个插槽是否存在并且由解释器来检查。(标志位不能只是这个插槽的值为非NULL。设置这个插槽指示这个插槽是否存在,但是这个插槽仍然可能是没有定义)。
如果你希望你的对象可以象一个number、一个sequence或一个mapping对象一样,你应该方式挨个实现的C类型PyNumberMethods, PySequenceMethods, 或PyMappingMethods的结构体指针。它是有你来填写进这个结构体中适当的值。你可以Python源码分发的Objects目录下找一个使用他们的例子。
如果你选择提供这个函数,应该为你的数据类型的实例返回一个hash值。下面是一个简单的例子:
当你数据类型的一个实例对象被调用时,这个函数将会被调用。例如,如果obj1是你对象的一个实例,Python脚本包含obj1('hello'),tp_call句柄捡回被调用。
这个函数有三个参数:
arg1是被调用的子对象的数据类型的实例。如果调用是obj1(‘hello’),这个arg1就是obj1。
arg2是一个包含参数调用的元组。你可以使用Py_Arg_ParseTuple()来提取参数。
arg3是一个传递键值参数的字典。如果它不为null并且你支持键值参数,使用PyArg_ParseTupleAndKeywords()来提取参数。如果你不想支持键值参数并且它不是NULL,抛出一个异常来来说明键值参数是不被支持的。
这里是一个胡乱写的一个实现call函数的例子:
这些函数提供迭代协议的支持。任何希望支持支持迭代的对象必须实现tp_iter句柄。通过tp_iter句柄返回的对象必须实现tp_iter和tp_iternext句柄。这两种处理程序使用一个参数,也就是调用这个方法的对象实例,并且返回一个新的引用。在产生错误的情况下,它将会设置一个异常并且返回NULL。
对于一个可迭代的集合对象,tp_iter句柄据需返回一个iterator对象。iterator对象负责保持迭代的状态。对于可以支持多种迭代的集合他们之间互不影响,一个新的迭代其将会创建并且放回。只能遍历一次的对象应该通过返回他们自己的引用来实现这个句柄,也应该实现tp_iternext句柄。文件对象就是这样一个例子。
迭代其对象应该同样实现这两个句柄。tp_iter句柄应该为这个迭代器返回一个新的引用。如果下一个对象存在的话tp_iternext句柄将会返回下一个对象的引用。如果迭代器到达了末尾,他可能不设置异常返回NULL或可能设置StopIteration来避免异常,这样可以得到更好的性能。如果实际发生错误,就应该设置一个异常并且返回NULL。
Python弱引用的实现的一个目标是为了允许任何类型可以加入弱引用机制,而通过弱引用不会产生对这些对象额外的开销。
要让一个对象是弱引用,扩展必须为使用弱引用机制的实例结构中包含一个PyObject*域;这个域必须使用NULL初始化。他必须使用合适的类型对象的字段的偏移量来设置tp_weaklistoffset域。如类型实例被定义为如下结构:
为实例定义的静态类型对象声明可以使用如下方法定义:
类型构造函数负责初始化弱引用list为NULL。
唯一需要增加的特性就是析构函数需要清理弱引用管理中的弱引用。如果弱引用列表不为空这是必须做的。
记住:你可以省略这里的大多数函数,在这种情况下你提供0作为变量。类型定义为你提供了每个你必须提供的函数。他们在在Python源码分发中include目录下object.h文件中。
为了学习怎样为你的新类型添加特殊的方法实现,请按照下面的做:下载并且解压Python源码。转到Objects目录,然后搜索C源文件中你想要的插槽函数(例如, tp_richcompare),你会发现你想实现的功能的例子。
当你需要验证一个对象是否是实现你的类型的时候,可以使用PyObject_TypeCheck()函数。一个简单的例子是:
1、当我们知道object是一个基本的类型时,这是真实的,例如一个字符串或一个浮点数。
2、在这个例子中我们依靠tp_dealloc句柄,因为我们的类型不支持垃圾收集。尽管一个类型支持垃圾收集,但是调用它们可以让对象远离垃圾收集,当然,这个调用还有一些其他好处没有在这里讲。但是我们不能保证不会重新分配的对象不会调用到我们本身。
3、我们现在知道first和last成员是string,所以我们可以在减少引用技术上少操点心,然而,我们接收字符串子类的实例,尽管重新分配的对象不会调用到我们对象本身,
4、尽管在第三个版本,我们仍然不能保证避免循环引用。字符串子类对象的实例是允许的循环引用并且字符串子类可能允许循环,尽管通常字符串对象是不允许的。