13.16 新式类的高级特性(Python 2.2+)

13.16.1 新式类的通用特性

我们已提讨论过有关新式类的一些特性。由于类型和类的统一,这些特性中最重要的是能够子类化Python数据类型。其中一个副作用是,所有的Python内建的“casting”或转换函数现在都是工厂函数。当这些函数被调用时,你实际上是对相应的类型进行实例化。

下面的内建函数,追随Python多日,都已“悄悄地(也许不是)”转化为工厂函数:

13.16 新式类的高级特性(Python 2.2+) - 图1

还有,加入了一些新的函数来管理这些“散兵游勇”:

  • basestring() [2]

  • dict()

  • bool()

  • set() [3]

  • object()

  • classmethod()

  • staticemethod()

  • super()

  • property()

  • file()

这些类名及工厂函数使用起来,很灵活。不仅能够创建这些类型的新对象,它们还可以用来作为基类,去子类化类型,现在还可以用于isinstance()内建函数。使用isinstance()能够用于替换用烦了的旧风格,而使用只需少量函数调用就可以得到清晰代码的新风格。比如,为测试一个对象是否是一个整型,旧风格中,我们必须调用type()两次或者import相关的模块并使用其属性;但现在只需要使用isinstance(),甚至在性能上也有所超越:

13.16 新式类的高级特性(Python 2.2+) - 图2

记住:尽管isinstance()很灵活,但它没有执行“严格匹配”比较——如果obj是一个给定类型的实例或其子类的实例,也会返回True。但如果想进行严格匹配,你仍然需要使用is操作符。

请复习13.12.2节中有关isinstance()的深入解释,还有在第4章中介绍这些调用是如何随同Python的变化而变化的。

13.16.2 slots类属性

字典位于实例的“心脏”。dict属性跟踪所有实例属性。举例来说,你有一个实例inst,它有一个属性foo,那使用inst.foo来访问它与使用inst.dict[‘foo’]来访问是一致的。

字典会占据大量内存,如果你有一个属性数量很少的类,但有很多实例,那么正好是这种情况。为内存上的考虑,用户现在可以使用slots属性来替代dict

基本上,slots是一个类变量,由一序列型对象组成,由所有合法标识构成的实例属性的集合来表示。它可以是一个列表,元组或可迭代对象。也可以是标识实例能拥有的唯一的属性的简单字符串。任何试图创建一个其名不在slots中的名字的实例属性都将导致AttributeError异常:

13.16 新式类的高级特性(Python 2.2+) - 图3

这种特性的主要目的是节约内存。其副作用是某种类型的“安全”,它能防止用户随心所欲的动态增加实例属性。带slots属性的类定义不会存在dict了(除非你在slots中增加‘dict’元素)。更多有关slots的信息,请参见《Python(语言)参考手册》(Python(Language)Reference Manual)中有关数据模型章节。

13.16.3 getattribute()特殊方法

Python类有一个名为getattr()的特殊方法,它仅当属性不能在实例的dict或它的类(类的dict),或者祖先类(其dict)中找到时,才被调用。我们曾在实现授权中看到过使用getattr()。

很多用户碰到的问题是,他们想要一个适当的函数来执行每一个属性访问,不光是当属性不能找到的情况。这就是getattribute()用武之处了。它使用起来,类似getattr(),不同之处在于,当属性被访问时,它就一直都可以被调用,而不局限于不能找到的情况。

如果类同时定义了getattribute()及getattr()方法,除非明确从get-attribute()调用,或getattribute()引发了AttributeError异常,否则后者不会被调用。

如果你将要在此(译者注:getattribute()中)访问这个类或其祖先类的属性,请务必小心。如果你在getattribute()中不知何故再次调用了getattribute(),你将会进入无穷递归。为避免在使用此方法时引起无穷递归,为了安全地访问任何它所需要的属性,你总是应该调用祖先类的同名方法;比如,super(obj,self).getattribute(attr)。此特殊方法只在新式类中有效。同slots一样,你可以参考《Python(语言)参考手册》中数据模型章节,以得到更多有关getattribute()的信息。

13.16.4 描述符

描述符是Python新式类中的关键点之一。它为对象属性提供强大的API。你可以认为描述符是表示对象属性的一个代理。当需要属性时,可根据你遇到的情况,通过描述符(如果有)或者采用常规方式句点属性标识法)来访问它。

如你的对象有代理,并且这个代理有一个“get”属性(实际写法为get),当这个代理被调用时,你就可以访问这个对象了。当你试图使用描述符(set)给一个对象赋值或删除一个属性(delete)时,这同样适用。

  1. get()、set()和delete()特殊方法

严格来说,描述符实际上可以是任何(新式)类,这种类至少实现了三个特殊方法get()、set()和delete()中的一个,这三个特殊方法充当描述符协议的作用。刚才提到过,get()可用于得到一个属性的值,set()是为一个属性进行赋值的,在采用del语句(或其他,其引用计数递减)明确删除掉某个属性时会调用delete()方法。在三者中,后者很少被实现。

还有,也不是所有的描述符都实现了set()方法。它们被当作方法描述符,或者更准确地说,是非数据描述符来被引用。那些同时覆盖get()和set()的类被称作数据描述符,它比非数据描述符要强大些。

get()set()及delete()的原型如下所示:

13.16 新式类的高级特性(Python 2.2+) - 图4

如果你想要为一个属性写个代理,必须把它作为一个类的属性,让这个代理来为我们做所有的工作。当你用这个代理来处理对一个属性的操作时,你会得到一个描述符来代理所有的函数功能。我们在前面的一节中已经讲过封装的概念。这里我们会进一步来探讨封装的问题。现在让我们来处理更加复杂的属性访问问题,而不是将所有任务都交给你所写的类中的对象们。

  1. getattribute()特殊方法(2)

使用描述符的顺序很重要,有一些描述符的级别要高于其他的。整个描述符系统的心脏是getattribute(),因为对每个属性的实例都会调用到这个特殊的方法。这个方法被用来查找类的属性,同时也是你的一个代理,调用它可以进行属性的访问等操作。

回顾一下上面的原型,如果一个实例调用了get()方法,这就可能传入了一个类型或类的对象。举例来说,给定类X和实例x,x.foo由getattribute()转化成:

13.16 新式类的高级特性(Python 2.2+) - 图5

如果类调用了get()方法,那么None将作为对象被传入(对于实例,传入的是self):

13.16 新式类的高级特性(Python 2.2+) - 图6

最后,如果super()被调用了,比如,给定Y为X的子类,然后用super(Y,obj).foo在obj.class.mro中紧接类Y沿着继承树来查找类X,然后调用:

13.16 新式类的高级特性(Python 2.2+) - 图7

然后,描述符会负责返回需要的对象。

3.优先级别

由于getattribute()的实现方式很特别,我们在此对getattribute()方法的执行方式做一个介绍。因此了解以下优先级别的排序就非常重要了:

  • 类属性

  • 数据描述符

  • 实例属性

  • 非数据描述符

  • 默认为getattr()

描述符是一个类属性,因此所有的类属性皆具有最高的优先级。你其实可以通过把一个描述符的引用赋给其他对象来替换这个描述符。比它们优先级别低一等的是实现了get()和set()方法的描述符。如果你实现了这个描述符,它会像一个代理那样帮助你完成所有的工作!

否则,它就默认为局部对象的dict的值,也就是说,它可以是一个实例属性。接下来是非数据描述符。可能第一次听起来会吃惊,有人可能认为在这条“食物链”上非数据描述符应该比实例属性的优先级更高,但事实并非如此。非数据描述符的目的只是当实例属性值不存在时,提供一个值而已。这与以下情况类似:当在一个实例的dict中找不到某个属性时,才去调用getattr()。

关于getattr()的说明,如果没有找到非数据描述符,那么getattribute()将会抛出一个AttributeError异常,接着会调用getattr()作为最后一步操作,否则AttributeError会返回给用户。

4.描述符举例

让我们来看一个简单的例子……用一个描述符禁止对属性进行访问或赋值的请求。事实上,以下所有示例都忽略了全部请求,但它们的功能逐步增多,我们希望你通过每个示例逐步掌握描述符的使用:

13.16 新式类的高级特性(Python 2.2+) - 图8

我们建立一个类,这个类使用了这个描述符,给它赋值并显示其值:

13.16 新式类的高级特性(Python 2.2+) - 图9

这并没有什么有趣的……让我们来看看在这个描述符中写一些输出语句会怎么样?

13.16 新式类的高级特性(Python 2.2+) - 图10

现在我们来看看修改后的结果:

13.16 新式类的高级特性(Python 2.2+) - 图11

最后,我们在描述符所在的类中添加一个占位符,占位符包含有关于这个描述符的有用信息:

13.16 新式类的高级特性(Python 2.2+) - 图12

13.16 新式类的高级特性(Python 2.2+) - 图13

下面的输出结果表明我们前面提到的优先级层次结构的重要性,尤其是我们说过,一个完整的数据描述符比实例的属性具有更高的优先级:

13.16 新式类的高级特性(Python 2.2+) - 图14

请注意我们是如何给实例的属性赋值的。给实例属性c3.foo赋值为一个字符串“bar”。但由于数据描述符比实例属性的优先级高,所赋的值“bar”被隐藏或覆盖了。

同样地,由于实例属性比非数据描述符的优先级高,你也可以将非数据描述符隐藏。这就和你给一个实例属性赋值,将对应类的同名属性隐藏起来是同一个道理:

13.16 新式类的高级特性(Python 2.2+) - 图15

这是一个直白的示例。我们将foo作为一个函数调用,然后又将它作为一个字符串访问,但我们也可以使用另一个函数,而且保持相同的调用机制:

13.16 新式类的高级特性(Python 2.2+) - 图16

要强调的是:函数是非数据描述符,实例属性有更高的优先级,我们可以遮蔽任一个非数据描述符,只需简单的把一个对象赋给实例(使用相同的名字)就可以了。

我们最后这个示例完成的功能更多一些,它尝试用文件系统保存一个属性的内容,这是个雏形版本。

1 ~ 10行

在引入相关模块后,我们编写一个描述符类,类中有一个类属性(saved),它用来记录描述符访问的所有属性。描述符创建后,它将注册并且记录所有从用户处接收的属性名。

12 ~ 26行

在获取描述符的属性之前,我们必须确保用户给它们赋值后才能使用。如果上述条件成立,接着我们将尝试打开pickle文件以读取其中所保存的值。如果文件打开失败,将引发一个异常。文件打开失败的原因可能有以下几种:文件已被删除了(或从未创建过),或是文件已损坏,或是由于某种原因,不能被pickle模块反串行化。

18 ~ 38行

将属性保存到文件中需要经过以下几个步骤:打开用于写入的pickle文件(可能是首次创建一个新的文件,也可能是删掉旧的文件),将对象串行化到磁盘,注册属性名,使用户可以读取这些属性值。如果对象不能被pickle,将引发一个异常。注意,如果你使用的是Python2.5以前的版本,你就不能合并try-except和try-finally语句(第30~38行)。

例13.9 使用文件来存储属性(descr.py)

这个类是一个雏形,但它展示了描述符的一个有趣的应用——可以在一个文件系统上保存属性的内容。

13.16 新式类的高级特性(Python 2.2+) - 图17

13.16 新式类的高级特性(Python 2.2+) - 图18

40 ~ 45行

最后,如果属性被删除了,文件会被删除,属性名字也会被注销。以下是这个类的用法示例:

13.16 新式类的高级特性(Python 2.2+) - 图19

13.16 新式类的高级特性(Python 2.2+) - 图20

属性访问没有什么特别的,程序员并不能准确判断一个对象是否能被打包后存储到文件系统中(除非如最后示例所示,将模块pickle,我们不该这样做)。我们也编写了异常处理的语句来处理文件损坏的情况。在本例中,我们第一次在描述符中实现delete()方法。

请注意,在示例中,我们并没有用到obj的实例。别把obj和self搞混淆,这个self是指描述符的实例,而不是类的实例。

5.描述符总结

你已经看到描述符是怎么工作的。静态方法、类方法、属性(见下面一节),甚至所有的函数都是描述符。想一想:函数是Python中常见的对象。有内置的函数、用户自定义的函数、类中定义的方法、静态方法、类方法。这些都是函数的例子。它们之间唯一的区别在于调用方式的不同。通常,函数是非绑定的。虽然静态方法是在类中被定义的,它也是非绑定的。但方法必须绑定到一个实例上,类方法必须绑定到一个类上。一个函数对象的描述符可以处理这些问题,描述符会根据函数的类型确定如何“封装”这个函数和函数被绑定的对象,然后返回调用对象。它的工作方式是这样的:函数本身就是一个描述符,函数的get()方法用来处理调用对象,并将调用对象返回给你。描述符具有非常棒的适用性,因此从来不会对Python自己的工作方式产生影响。

6.属性和property()内建函数

属性是一种有用的特殊类型的描述符。它们是用来处理所有对实例属性的访问,其工作方式和我们前面说过的描述符相似。“一般”情况下,当你使用点属性符号来处理一个实例属性时,其实你是在修改这个实例的dict属性。

表面上来看,你使用property()访问和一般的属性访问方法没有什么不同,但实际上这种访问的实现是不同的——它使用了函数(或方法)。在本章的前面,你已看到在Python的早期版本中,我们一般用getattr()和setattr()来处理和属性相关的问题。属性的访问会涉及以上特殊的方法(和getattribute()),但是如果我们用property()来处理这些问题,你就可以写一个和属性有关的函数来处理实例属性的获取(getting)、赋值(setting)和删除(deleting)操作,而不必再使用那些特殊的方法了(如果你要处理大量的实例属性,使用那些特殊的方法将使代码变得很臃肿)。

property()内建函数有四个参数,它们是:

13.16 新式类的高级特性(Python 2.2+) - 图21

请注意property()的一般用法是,将它写在一个类定义中,property()接受一些传进来的函数(其实是方法)作为参数。实际上,property()是在它所在的类被创建时被调用的,这些传进来的(作为参数的)方法是非绑定的,所以这些方法其实就是函数!

下面的例子在类中建立一个只读的整型属性,用逐位异或操作符将它隐藏起来:

13.16 新式类的高级特性(Python 2.2+) - 图22

我们来运行这个例子,会发现它只保存我们第一次给出的值,而不允许我们对它做第二次修改:

13.16 新式类的高级特性(Python 2.2+) - 图23

下面是另一个关于setter的例子:

13.16 新式类的高级特性(Python 2.2+) - 图24

本示例的输出结果:

13.16 新式类的高级特性(Python 2.2+) - 图25

13.16 新式类的高级特性(Python 2.2+) - 图26

属性成功保存到x中并显示出来,是因为在调用构造器给x赋初始值前,在getter中已经将~x赋给了self.__x。

你还可以给自己写的属性添加一个文档字符串,参见下面这个例子:

13.16 新式类的高级特性(Python 2.2+) - 图27

为了说明这是可行的实现方法,我们在property中使用的是一个函数而不是方法。注意在调用函数时self作为第一个(也是唯一的)参数被传入,所以我们必须加一个伪变量把self丢弃。下面是本例的输出:

13.16 新式类的高级特性(Python 2.2+) - 图28

你明白properties是如何把你写的函数(fget、fset和fdel)影射为描述符的get()、set()和delete()方法的吗?你不必写一个描述符类,并在其中定义你要调用的这些方法。只要把你写的函数(或方法)全部传递给property()就可以了。

在你写的类定义中创建描述符方法的一个弊端是它会搞乱类的名字空间。不仅如此,这种做法也不会像property()那样很好地控制属性访问。如果不用property()这种控制属性访问的目的就不可能实现。我们的第二个例子没有强制使用property(),因为它允许对属性方法的访问(由于在类定义中包含属性方法):

13.16 新式类的高级特性(Python 2.2+) - 图29

APNPC(ActiveState Programmer Network Python Cookbook, http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183)上的一个聪明的办法解决了以下问题:

  • “借用”一个函数的名字空间;

  • 编写一个用作内部函数的方法作为property()的(关键字)参数;

  • (用locals())返回一个包含所有的(函数/方法)名和对应对象的字典;

  • 把字典传入property();

  • 然后,去掉临时的名字空间。

这样,方法就不会再把类的名字空间搞乱了,因为定义在内部函数中的这些方法属于其他的名字空间。由于这些方法所属的名字空间已超出作用范围,用户是不能够访问这些方法的,所以通过使用属性property()来访问属性就成为了唯一可行的办法。根据APNPC上的方法,我们来修改这个类:

13.16 新式类的高级特性(Python 2.2+) - 图30

13.16 新式类的高级特性(Python 2.2+) - 图31

我们的代码工作如初,但有两点明显不同:(1)类的名字空间更加简洁,只有[‘doc’,‘init’,‘module’,‘x’];(2)用户不能再通过inst.set_x(40)给属性赋值,必须使用init.x=40。我们还使用函数修饰符(@property)将函数中的x赋值到一个属性对象。由于修饰符是从Python2.4版本开始引入的,如果你使用的是Python的早期版本2.2.x或2.3.x,请将修饰符@property去掉,在x()的函数声明后添加x=property(**x())。

13.16.5 元类和metaclass

1.元类(Metaclasses)是什么

元类可能是添加到新风格类中最难以理解的功能了。元类让你来定义某些类是如何被创建的,从根本上说,赋予你如何创建类的控制权(你甚至不用去想类实例层面的东西)。早在Python1.5的时代,人们就在谈论这些功能(当时很多人都认为不可能实现),但现在终于实现了。

从根本上说,你可以把元类想成是一个类中类,或是一个类,它的实例是其他的类。实际上,当你创建一个新类时,你就是在使用默认的元类,它是一个类型对象(对传统的类来说,它们的元类是types.ClassType)。当某个类调用type()函数时,你就会看到它到底是谁的实例:

13.16 新式类的高级特性(Python 2.2+) - 图32

2.什么时候使用元类

元类一般用于创建类。在执行类定义时,解释器必须要知道这个类的正确的元类。解释器会先寻找类属性metaclass,如果此属性存在,就将这个属性赋值给此类作为它的元类。如果此属性没有定义,它会向上查找父类中的metaclass。所有新风格的类如果没有任何父类,会从对象或类型中继承(type(object)当然是类型)。

如果还没有发现metaclass属性,解释器会检查名字为metaclass的全局变量;如果它存在,就使用它作为元类。否则,这个类就是一个传统类,并用types.ClassType作为此类的元类。(注意:在这里你可以运用一些技巧……如果你定义了一个传统类,并且设置它的metaclass=type,其实你是在将它升级为一个新风格的类!)

在执行类定义的时候,将检查此类正确的(一般是默认的)元类,元类(通常)传递三个参数(到构造器):类名、从基类继承数据的元组和(类的)属性字典。

3.谁在用元类

元类这样的话题对大多数人来说属于理论化或纯面向对象思想的范畴,认为它在实际编程中没有什么实际意义。从某种意义上讲这种想法是正确的;但最重要的请铭记在心的是,元类的最终使用者不是用户,正是程序员自己。你通过定义一个元类来“迫使”程序员按照某种方式实现目标类,这将既可以简化他们的工作,也可以使所编写的程序更符合特定标准。

4.元类何时被创建

前面我们已提到创建的元类用于改变类的默认行为和创建方式。大多数Python用户都无须创建或明确地使用元类。创建一个新风格的类或传统类的通用做法是使用系统自己所提供的元类的默认方式。

用户一般都不会觉察到元类所提供的创建类(或元类实例化)的默认模板方式。虽然一般我们并不创建元类,还是让我们来看下面一个简单的例子(关于更多这方面的示例请参见本节末尾的文档列表)。

元类示例1

我们第一个关于元类的示例非常简单(希望如此)。它只是在用元类创建一个类时,显示时间标签(你现在该知道,这发生在类被创建的时候)。

看下面这个脚本。它包含的print语句散落在代码各个地方,便于我们了解所发生的事情:

13.16 新式类的高级特性(Python 2.2+) - 图33

当我们执行此脚本时,将得到以下输出:

13.16 新式类的高级特性(Python 2.2+) - 图34

DONE

当你明白了一个类的定义其实是在完成某些工作的事实以后,你就容易理解这是怎么一回事情了。

元类示例2

在第二个示例中,我们将创建一个元类,要求程序员在他们写的类中提供一个str()方法的实现,这样用户就可以看到比我们在本章前面所见到的一般Python对象字符串(<object object at id>)更有用的信息。

如果你还没有在类中覆盖repr()方法,元类会(强烈)提示你这么做,但这只是个警告。如果未实现str()方法,将引发一个TypeError的异常,要求用户编写一个同名方法。以下是关于元类的代码:

13.16 新式类的高级特性(Python 2.2+) - 图35

我们编写了三个关于元类的示例,其中一个(Foo)重载了特殊方法str()和repr(),另一个(Bar)只实现了特殊方法str(),还有一个(FooBar)没有实现str()和repr(),这种情况是错误的。完整的程序见示例13.10。

执行此脚本,我们得到如下输出:

13.16 新式类的高级特性(Python 2.2+) - 图36

13.16 新式类的高级特性(Python 2.2+) - 图37

例13.10 将直线的两个端点元类示例(meta.py)

这个模块有一个元类和三个受此元类限定的类。每创建一个类,将打印一条输出语句。

13.16 新式类的高级特性(Python 2.2+) - 图38

13.16 新式类的高级特性(Python 2.2+) - 图39

注意我们是如何成功声明Foo定义的;定义Bar时,提示警告repr()未实现;FooBar的创建没有通过安全检查,以致程序最后没有打印出关于FooBar的语句。另外要注意的是我们并没有创建任何测试类的实例……这些甚至根本不包括在我们的设计中。但别忘了这些类本身就是我们自己的元类的实例。这个示例只显示了元类强大功能的一方面。

关于元类的在线文档众多,包括Python文档PEPs 252和PEPs 253,“What’s New in Python 2.2”文档,名为”Unifying Types and Classes in Python 2.2”的文章。在Python 2.2.3发布的主页上你也可以找到相关文档的链接地址。