13.15 *授权
13.15.1 包装
“包装”在Python编程世界中经常会被提到的一个术语。它是一个通用的名字,意思是对一个已存在的对象进行包装,不管它是数据类型还是一段代码,可以是对一个已存在的对象增加新的、删除不要的或修改其他已存在的功能。
在Python 2.2版本前,从Python标准类型子类化或派生类都是不允许的。即使你现在可以对新式类这样做,这一观念仍然很流行。你可以包装任何类型作为一个类的核心成员,以使新对象的行为模仿你想要的数据类型中已存在的行为,并且去掉你不希望存在的行为;它可能会要做一些额外的事情。这就是“包装类型”。在附录中,我们还将讨论如何扩充Python,包装的另一种形式。
包装包括定义一个类,它的实例拥有标准类型的核心行为。换句话说,它现在不仅能唱能跳,还能够像原类型一样步行,说话。图13-4举例说明了在类中包装的类型看起像个什么样子。在图的中心为标准类型的核心行为,但它也通过新的或最新的功能,甚至可能通过访问实际数据的不同方法得到提高。
类对象(其表现像类型)
你还可以包装类,但这不会有太多的用途,因为已经有用于操作对象的机制,并且在上面已描述过,对标准类型有对其进行包装的方式。你如何操作一个已存的类,模拟你需要的行为,删除你不喜欢的,并且可能让类表现出与原类不同的行为呢?我们前面已讨论过,就是采用派生。
图 13-4 包装类型
13.15.2 实现授权
授权是包装的一个特性,可用于简化处理相关命令性功能,采用已存在的功能以达到最大限度的代码重用。
包装一个类型通常是对已存在的类型的一些定制。我们在前面提到过,这种做法可以新建、修改或删除原有产品的功能。其他的则保持原样,或者保留已存功能和行为。授权的过程,即是所有更新的功能都是由新类的某部分来处理,但已存在的功能就授权给对象的默认属性。
实现授权的关键点就是覆盖getattr()方法,在代码中包含一个对getattr()内建函数的调用。特别地,调用getattrO以得到默认对象属性(数据属性或者方法)并返回它以便访问或调用。特殊方法getattr()的工作方式是,当搜索一个属性时,任何局部对象首先被找到(定制的对象)。如果搜索失败了,则getattr()会被调用,然后调用getattrO得到一个对象的默认行为。
换言之,当引用一个属性时,Python解释器将试着在局部名称空间中查找那个名字,比如一个自定义的方法或局部实例属性。如果没有在局部字典中找到,则搜索类名称空间,以防一个类属性被访问。最后,如果两类搜索都失败了,搜索则对原对象开始授权请求,此时,getattr()会被调用。
1.包装对象的简例
看一个例子。这个类已乎可以包装任何对象,提供基本功能,比如使用repr()和str()来处理字符串表示法。另外定制由get()方法处理,它删除包装并且返回原始对象。所以保留的功能都授权给对象的本地属性,在必要时,可由getattr()获得。
下面是包装类的例子:
在第一个例子中,我们将用到复数,因为所有Python数值类型,只有复数拥有属性:数据属性和conjugate()内建方法(求共轭复数,译者注)。记住,属性可以是数据属性,还可以是函数或方法:
一旦我们创建了包装的对象类型,只要由交互解释器调用repr(),就可以得到一个字符串表示。然后我们继续访问了复数的三种属性,我们的类中一种都没有定义。在例子中,寻找实部,虚部及共轭复数的定义……杳无踪影!
对这些属性的访问,是通过getattr()方法,授权给对象。最终调用get()方法没有授权,因为它是为我们的对象定义的——它返回包装的真实的数据对象。
下一个使用我们的包装类的例子用到一个列表。我们将会创建对象,然后执行多种操作,每次授权给列表方法。
注意,尽管我们正在我们的例子中使用实例,它们展示的行为与它们包装的数据类型非常相似。然后,需要明白,只有已存在的属性是在此代码中授权的。
特殊行为没有在类型的方法列表中,不能被访问,因为它们不是属性。一个例子是,对列表的切片操作,它是内建于类型中的,而不是像append()方法那样作为属性存在的。从另一个角度来说,切片操作符是序列类型的一部分,并不是通过getitem()特殊方法来实现的。
AttributeError异常出现的原因是切片操作调用了getitem()方法,且getitme()没有作为一个类实例方法进行定义,也不是列表对象的方法。回忆一下,什么时候调用getattr()呢?当在实例或类字典中的完整搜索失败后,就调用它来查找一个成功的匹配。你在上面可以看到,对getattrO的调用就是失败的那个,触发了异常。
然而,我们还有一种“作弊”的方法,访问实际对象[通过我们的get()方法]和它的切片能力。
你现在可能知道为什么我们实现get()方法了——仅仅是为了我们需要取得对原对象进行访问这种情况,我们可以从访问调用中直接访问对象的属性,而忽略局部变量(realList):
get()方法返回一个对象,随后被索引以得到切片片段。
一旦你熟悉了对象的属性,你就能够开始理解一些信息片段从何而来,能够利用新得到的知识来重复功能:
这总结了我们的简单包装类的例子。我们还刚开始接触使用类型模拟来进行类自定义。你将会发现你可以进行无限多的改进,来进一步增加你的代码的用途。一种改进方法是为对象添加时间戳。在下一小节中,我们将对我们的包装类增加另一个维度(dimension):
2.更新简单的包裹类
创建时间、修改时间和访问时间是文件的几个常见属性,但并不是说你不能为对象加上这类信息,毕竟一些应用能因有这些额外信息而受益。
如果你对使用这三类时间顺序(chronological)数据还不熟,我们将会对它们进行解释。创建时间(或‘ctime’)是实例化的时间,修改时间(或‘mtime’)指的是核心数据升级的时间[通常会调用新的set()方法],而访问时间(或‘atime’)是最后一次对象的数据值被获取或者属性被访问时的时间戳。
更新我们前面定义的类,可以创建一个模块twrapme.py,看例13.7。
如何更新这些代码呢?好,首先,你将会发现增加了三个新方法:gettimeval()、gettimestr()和set()。我们还增加数行代码,根据所执行的访问类型,更新相应的时间戳。
例13.7 包装标准类型(twrapme.py)
类定义包装了任何内建类型,增加时间属性;get(),set(),还有字符串表示的方法;授权所有保留的属性,访问这些标准类型。
gettimeval()方法带一个简单的字符参数,“c”、“m”或“a”,相应地,对应于创建、修改或访问时间,并返回相应的时间,以一个浮点值保存。gettimestr()仅仅返回一个经time.ctime()函数格式化的打印良好的字符串形式的时间。
为新的模块作一个测试驱动。我们已看到授权是如何工作的,所以,我们将包装没有属性的对象,来突出刚加入的新的功能。在例子中,我们包装了一个整型,然后将其改为字符串。
你将注意到,一个对象在第一次被包装时,创建、修改及最后一次访问时间都是一样的。一旦对象被访问,访问时间即被更新,但其他的没有动。如果使用set()来置换对象,则修改和最后一次访问时间会被更新。例子中,最后是对对象的读访问操作。
改进包装一个特殊对象
下一个例子,描述了一个包装文件对象的类。我们的类与一般带一个异常的文件对象行为完全一样:在写模式中,字符串只有全部为大写时才写入文件。
这里我们要解决的问题是,当你正在写一个文本文件,其数据可能会被一台大型机读取。很多老式机器在处理时严格要求大写字母,所以我们要实现一个文件对象,其中所有写入文件的文本会自动转化为大写,程序员就不必担心了。
事实上,唯一值得注意的不同点是并不使用open()内建函数,而是调用CapOpen类时行初始化,尽管参数同open()完全一样。
例13.8展示那段代码,文件名是capOpen.py。下面看一下例子中是如何使用这个类的:
例13.8 包装文件对象(capOpen.py)
这个类扩充了《Python FAQ》中的一个例子,提供一个文件类对象,定制write()方法,同时,给文件对象授权其他的功能。
可以看到,唯一不同的是第一次对CapOpen()的调用,而不是open()。如果你正与一个实际文件对象,而非行为像文件对象的类实例进行交互,那么其他所有代码与你本该做的是一样的。除了write(),所有属性都已授权给文件对象。为了确定代码是否正确,我们加载文件,并显示其内容(注:可以使用open()或CapOpen(),这里因在本例中用到,所以选用CapOpen())。