继续介绍运算符重载的知识。
运算符重载(1)回顾这里
========================================================================
属性引用:__getattr__和__setattr__
__getattr__方法是拦截属性点号运算。确切地说,当通过对【未定义(即不存在)】的属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果Python可通过其继承树搜索流程找到这个属性,该方法就不会被调用。因为有这种情况,所以__getattr__可以作为钩子来通过通用的方式响应属性请求。如下例:
>>> class empty: def __getattr__(self,attrname): if attrname == 'age': return 40 else: raise AttributeError(attrname) >>> X = empty() >>> X.age 40 >>> X.name Traceback (most recent call last): File "<pyshell#11>", line 1, in <module> X.name File "<pyshell#8>", line 6, in __getattr__ raise AttributeError(attrname) AttributeError: name在这里,empty类和其实例X本身没有属性,所以对X.age的存取会转至__getattr__方法,self则赋值为实例(X),而attrname则赋值为未定义的属性名称字符串('age')。这个类传回一个实际值作为X.age点号表达式的结果(40),让age看起来像实际的属性。
>>> class accesscontrol: def __setattr__(self,attr,value): if attr == 'age': self.__dict__[attr] = value else: raise AttributeError(attr + ' not allowed') >>> X = accesscontrol() >>> X.age = 40 >>> X.age 40 >>> X.name = 'Gavin' Traceback (most recent call last): File "<pyshell#28>", line 1, in <module> X.name = 'Gavin' File "<pyshell#24>", line 6, in __setattr__ raise AttributeError(attr + ' not allowed') AttributeError: name not allowed------------------------------------------------------------------------------------ -----------------------------
模拟实例属性的私有化:第一部分
下例程序把上一个例子通用化了,让每个子类都有自己的私有变量名列表,这些变量名无法通过其实例进行赋值:
class PrivateExc(Exception): pass class Privacy: def __setattr__(self,attrname,value): if attrname in self.privates: raise PrivateExc(attrname,self) else: self.__dict__[attrname] = value class Test1(Privacy): privates = ['age'] class Test2(Privacy): privates = ['name','pay'] def __init__(self): self.__dict__['name'] = 'Tom' if __name__ == '__main__': x = Test1() y = Test2() x.name = 'Bob' y.name = 'Sue' #这句话会报异常,因为name属性是Test2的私有变量 y.age = 30 x.age = 40 #同理,这句话会报异常实际上,这是Python实现【属性私有性】(也就是无法在类外对属性名进行修改)的首选方法。
下例是已经见过的__init__构造函数和__add__重载方法:
>>> class adder: def __init__(self,value = 0): self.data = value def __add__(self,other): self.data += other >>> x = adder() >>> print(x) <__main__.adder object at 0x0330DE10> >>> x <__main__.adder object at 0x0330DE10>实例对象的默认显示既无用也不好看。但是,编写或继承字符串表示方法允许我们定制显示:
>>> class addrepr(adder): def __repr__(self): return 'addrepr(%s)'%self.data >>> x = addrepr(2) >>> x+1 >>> x addrepr(3) >>> print(x) addrepr(3) >>> str(x),repr(x) ('addrepr(3)', 'addrepr(3)')那么,为什么要有两个显示方法呢?概括地讲,是为了进行用户友好地显示。具体来说:
>>> class addstr(adder): def __str__(self): return '[value:%s]'%self.data >>> x = addstr(3) >>> x+1 >>> x <__main__.addstr object at 0x0330DE10> >>> print(x) [value:4] >>> str(x),repr(x) ('[value:4]', '<__main__.addstr object at 0x0330DE10>')正是由于这一点,如果想让所有环境都有统一的显示,__repr__是最佳选择。不过,通过分别定义这两个方法,就可以在不同的环境内支持不同显示。
右侧加法和原处加法:__radd__和__iadd__
前面例子中出现的__add__方法不支持+运算符右侧使用实例对象。要实现这类表达式,而支持可互换的运算符,可以一并编写__radd__方法。只有当+右侧是类实例,而左边不是类实例的时候,Python才会调用__radd__。在其他情况下,则由左侧对象调用__add__方法。
>>> class Commuter: def __init__(self,val): self.val = val def __add__(self,other): print('add',self.val,other) return self.val + other def __radd__(self,other): print('radd',self.val,other) return other + self.val >>> >>> x = Commuter(88) >>> y = Commuter(99) >>> x +1 add 88 1 89 >>> 1+y radd 99 1 100 >>> x+y add 88 <__main__.Commuter object at 0x0330DD50> radd 99 88 187当不同类的实例混合出现在表达式时,Python优先选择左侧的那个类。当我们把两个实例相加的时候,Python运行__add__,它反过来通过简化左边的运算数来触发__radd__。
>>> class Commuter: def __init__(self,val): self.val = val def __add__(self,other): if isinstance(other,Commuter): other = other.val return Commuter(self.val + other) def __radd__(self,other): return Commuter(other+self.val) def __str__(self): return '<Commuter:%s>'%self.val >>> x = Commuter(88) >>> y = Commuter(99) >>> print(x+10) <Commuter:98> >>> print(10+y) <Commuter:109> >>> z = x+y >>> z <__main__.Commuter object at 0x037264D0> >>> print(z) <Commuter:187> >>> print(z+z) <Commuter:374>------------------------------------------------------------------------------------ -----------------------------
>>> class Number: def __init__(self,val): self.val = val def __add__(self,other): self.val += other return self >>> x = Number(5) >>> x+=1 >>> x.val 6 >>> class Number: def __init__(self,val): self.val = val def __iadd__(self,other): self.val += other return self >>> x = Number(5) >>> x+=1 >>> x.val 6每个二元运算都有类似的右侧和原处重载方法,它们以相同的方式工作,例如,__mul__,__rmul__,__imul__。
Call表达式:__call__
当调用实例时,使用__call__方法。如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。这样可以让类实例的外观和用法类似于函数。
>>> class Callee: def __call__(self,*pargs,**kargs): print('Called:',pargs,kargs) >>> C = Callee() >>> C(1,2,3) Called: (1, 2, 3) {} >>> C(1,2,3,x=4,y=5) Called: (1, 2, 3) {'x': 4, 'y': 5}确切地说,之间介绍的【参数】传递方式,__call__方法都支持。
比较:__lt__、__gt__和其他方法
类可以定义方法来捕获所有的6种比较运算符:<、>、<=、>=、==和!=。限制如下:
1.与前面讨论的__add__/__radd__对不同,比较方法没有右端形式。相反,当只有一个运算数支持比较的时候,使用其对应方法(例如,__lt__与__gt__互为对应)。
2.比较运算符没有隐式关系。例如,==并不意味着!=是假的,因此,__eq__和__ne__应该定义为确保两个运算符都正确地使用
注意:Python2.6中与此等效的__cmp__方法在Python3中已经移除!
看如下一个示例:
>>> class C: data = 'spam' def __gt__(self,other): return self.data>other def __lt__(self,other): return self.data<other >>> X = C() >>> print(X>'ham') True >>> print(X<'ham') False >>> print('ham'<X) True========================================================================
布尔测试:__bool__和__len__
在布尔环境中,Python首先尝试__bool__来获取一个直接的布尔值,然后,如果没有该方法,就尝试__len__类根据对象的长度确定一个真值。
>>> class Truth: def __bool__(self): return True >>> X = Truth() >>> if X: print('yes') yes
>>> class Truth: def __bool__(self): return False >>> X = Truth() >>> bool(X) False如果没有这个方法,Python会退而求其次求其长度,因为一个非空对象看做是真:
>>> class Truth(): def __len__(self): return 0 >>> X = Truth() >>> if not X: print('no') no如果两个方法都有,Python会优先调用__bool__方法。
>>> class Truth: pass >>> X = Truth() >>> bool(X) True========================================================================
每当实例产生时,就会调用__init__构造函数。每当实例空间被回收时(在垃圾收集时),它的对立面__del__,也就是析构函数,就会自动执行:
>>> class Life: def __init__(self,name = 'unknown'): print('Hello',name) self.name = name def __del__(self): print('GoodBye',self.name) >>> brian = Life('Brian') Hello Brian >>> brian = 'gavin' GoodBye Brian在这里,当Brian赋值为字符串时,我们就会失去Life实例的最后一个引用,因此会触发其析构函数。