任务
你需要从某个类或者类型继承,但是需要对继承做一些调整。比如,需要选择性地隐藏某些基类的方法,而继承并不能做到这一点。
解决方案
继承是很方便的,但它并不是万用良药。比如,它无法让你隐藏基类的方法或者属性。而自动托管技术则提供了一种很好的选择。假设需要把一些对象封起来变成只读对象从而避免意外修改的情况。那么,除了禁止属性设置的功能,还需要隐藏修改属性的方法。下面我们给出一个办法:
#同时支持2.3和2.4
try:set
except NameError:from sets import Set as set
class ROError(AttributeError):pass
class Readonly:#这里并没有用继承,我们会在后面讨论其原因mutators = {list:set('''__delitem__ __delslice__ __iadd__ __imul____setitem__ __setslice__ __append extend insert pop remove sort'''.split()),dict:set('''__delitem__ __setitem__ clear pop popitemsetdefault update'''.split()),}def __init__(self,o):object.__setattr__(self,'_o',o)object.__setattr__(self,'_no',self.mutators.get(type(o),()))def __setattr__(self,n,y):raise ROError,"Can't set attr %r on RO obiect" %n def __delattr__(self,n):raise ROError,"Can't del attr %r from Ro object" %n def __getattr__(self,n):if n in self._no:raise ROError,"Can't get attr %r from Ro object" %nreturn getattr(self._o, n)
通过修改 mutators,即 Readonly.mutators[sometype] = the_mutators,还可以轻松地增加其他需要处理的类型。
讨论
自动托管是一种强大而通用的技术。在本节的例子中,通过使用这个技术我们能得到和类继承几乎完全一样的效果,同时还能隐藏一些名字。我们在任务中使用这个模拟的子类将一些对象封装起来,使之变成只读对象。它的性能也许不如真正的继承,但另一方面,作为补偿,我们获得了更好的灵活度和更精细的粒度控制。
基本的想法是,我们的类的每个实例都含有我们想要封装的类型的实例。每当客户代码试图从我们的类的实例中获取属性时,除非该属性已经在类中被定义了(比如定义在 Readonly类的 mutators 字典中),否则__getattr__ 在完成检查之后,会透明地将这个请求转交给被封装的实例。在Python中,方法同样也是属性,访问的方式也一样,所以无论是访问方法还是属性,代码无须改变。用来访问属性的__getattr__方法同时也可用于访问方法。
解决方案的注释没有解释不使用继承的原因,这里我们会给出一点解释。这种基于__getattr__的方式也可用于特殊方法,但仅对旧风格类的实例有效。在新的对象模型中,Python 操作直接通过类的特殊方法来进行,而不是实例的。关于这个问题的更多内容可以在 6.6 节和 20.8节中看到。本节采用的方案——让 Readonly 类成为旧风格类,从而避开这个问题,并把相关内容留到其他章节——在真实的生产代码中是不值得推荐的。我在这里用仅仅是为了控制篇幅,同时避免重复其他章节的内容。
setattr__的角色类似于__getattr,当客户代码设置实例的属性时,它就会被调用,这个任务要求某些属性为只读,我们只需简单地禁止属性访问操作即可。记住,要在方法的代码编写中避免激发对__setattr__的调用,在有__setattr__的类的方法中你不应该使用self.n = v这样的语句。最简单的是直接把设置操作委托给类object,如同类Readonly在它的__init__ 方法中所做的那样。方法__delattr__完成了最后拼图,它会处理那些试图从实例中删除属性的操作。
以自动托管方式完成的封装并不适用于采用了类型检查的客户代码或者框架代码。在那种情况下,客户代码或框架代码完全破坏了多态性,代码本身应该是被重写的。记住不要在你自己的代码中使用类型检查,因为你可能根本无须那么做。见6.13节提供的更好的选择。
在 Python 的老版本中,自动托管的流行程度甚至比现在还高,那是因为当时 Python 不支持从内建的类型继承。而对于现在的 Python,从内建类型继承是允许的,因此自动托管就用得不那么频繁了。不过,自动托管仍然具有它的地位——它只是稍微远离了聚光灯一点点。托管比继承更加灵活,而有时这种灵活是无价的。除了选择性地托管(从而高效地实现了某些属性的“隐藏”),一个对象还可以在不同的时间托管给不同的子对象,或者一次托管给多个子对象,继承无法提供任何能够与之相比的特性。下面给出托管给多个特定子对象的例子。假设你有个类,提供各种“转发方法”,比如:
class Pricing(object):def __init__(self,location,event):self.location = locationself.event = eventdef setlocation(self,location):self.location = locationdef getprice(self):return self.location.getprice()def getquantity(self):return self.location.getquantity()def getdiscount(self):return self.event.getdiscount()and many more such methods
继承很明显不适用,因为 Pricing的实例需要托管给特定的location和event实例,这些实例在初始化阶段传入而且可能会被修改。自动托管的补救方法是:
class AutoDelegator(object):delegates = ()do_not_delegate = ()def __getattr__(self,key):if key not in self.do_not_delegate:for d in self.delegates:try:return getattr(d,key)except AttributeError:passraise AttributeError,key
class Pricing(AutoDelegator):def __init__(self,location,event):self.delegates = [location,event]def setlocation(self,location):self.delegates[0] = location
在此例中,我们没有托管属性的删除和设置,而只是托管了属性的获取(还有一些非特殊方法)。当然,这个方式只有在我们想要托管的各个对象的方法(以及其他属性)不会互相干扰的情况下才会充分有效,比如,location最好不要有个getdiscount方法否则它会抢先进行方法的托管,而此方法原本应该是由event对象来执行的。
如果一个需要大量托管的类涉及这种问题,它可以简单地定义一些对应的方法,这是因为只有在用别的方式无法找到属性和方法时,__getattr__才会介入。而通过do_not_delegate 属性还可以隐藏托管对象的一些属性和方法,而且它也可以被子类改写。举个例子,如果类 Pricing 想要隐藏一个叫做 setdiscount 的方法,此方法由 event提供,做一点点修改就可以了:
class Pricing(AutoDelegator):
do_not_delegate = ('set_discount')
其余部分则与前面代码片段相同。