继续介绍运算符重载的知识。
========================================================================
属性引用:__getattr__和__setattr__
__getattr__方法是拦截属性点号运算。确切地说,当通过对【未定义(即不存在)】的属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果Python可通过其继承树搜索流程找到这个属性,该方法就不会被调用。因为有这种情况,所以__getattr__可以作为钩子来通过通用的方式响应属性请求。如下例:
>>> class empty:在这里,empty类和其实例X本身没有属性,所以对X.age的存取会转至__getattr__方法,self则赋值为实例(X),而attrname则赋值为未定义的属性名称字符串('age')。这个类传回一个实际值作为X.age点号表达式的结果(40),让age看起来像实际的属性。
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
而对于请求X.name属性,程序引发了异常。
与此相关的重载方法__setattr__会拦截所有属性的赋值语句。如果定义了这个方法,self.attr = value 会变成self.__setattr__('attr',value)。这一点技巧性很高,因为在__setattr__中对任何self属性做赋值,都会再调用__setattr__,导致了无穷递归循环。如果想使用这个方法,要确定时通过对属性字典做索引运算来赋值任何实例属性的,也就是使用self.__dict__['name'] = x ,而不是self.name = x.看下例:
>>> 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):实际上,这是Python实现【属性私有性】(也就是无法在类外对属性名进行修改)的首选方法。
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 #同理,这句话会报异常
不过,属性私有性的更完整的的解决方案将会在以后讲解,之后将会使用【类装饰器】来更加通用地拦截和验证属性。
========================================================================
__repr__和__str__会返回字符串表达形式
下例是已经见过的__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)')
1.打印操作会首先尝试__str__和str内置函数(print运行的内部等价形式),它通常应该返回一个用户友好的显示。
2.__repr__用于所有其他环境中:用于交互模式下提示回应以及repr函数。
总而言之,__repr__用于任何地方,除了当定义一个__str__的时候,使用print和str。然而要注意,如果没有定义__str__,打印还是使用__repr__,但反过来并不成立——例如,交互式相应模式,只是使用__repr__,并且根本不要尝试__str__:
>>> class addstr(adder):正是由于这一点,如果想让所有环境都有统一的显示,__repr__是最佳选择。不过,通过分别定义这两个方法,就可以在不同的环境内支持不同显示。
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>')
但是要记得:__str__和__repr__都必须返回字符串,其他的类型会出错。
========================================================================
右侧加法和原处加法:__radd__和__iadd__
前面例子中出现的__add__方法不支持+运算符右侧使用实例对象。要实现这类表达式,而支持可互换的运算符,可以一并编写__radd__方法。只有当+右侧是类实例,而左边不是类实例的时候,Python才会调用__radd__。在其他情况下,则由左侧对象调用__add__方法。
>>> class Commuter:当不同类的实例混合出现在表达式时,Python优先选择左侧的那个类。当我们把两个实例相加的时候,Python运行__add__,它反过来通过简化左边的运算数来触发__radd__。
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
在实际的应用中,类型可能需要在结果中传播,类型测试可能需要辨别它是否能够安全地转换并由此避免嵌套。例如,下面的代码如果没有isinstance测试,当两个实例相加并且__add__触发__radd__的时候,我们最终得到一个Commuter,其val是另一个Commuter:
>>> 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>
原处加法
编写一个__iadd__方法可以实现+=原处扩展相加。当然,__add__也可以完成类似的功能,如果没有__iadd__的时候,Python会调用__add__:
>>> class Number:每个二元运算都有类似的右侧和原处重载方法,它们以相同的方式工作,例如,__mul__,__rmul__,__imul__。
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
========================================================================
Call表达式:__call__
当调用实例时,使用__call__方法。如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。这样可以让类实例的外观和用法类似于函数。
>>> class Callee:确切地说,之间介绍的【参数】传递方式,__call__方法都支持。
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}
当需要为函数的API编写接口时,__call__就变得很有用:这可以编写遵循所需要的函数来调用接口对象,同时又能保留状态信息。事实上,__call__方法是除了__init__构造函数以及__str__和__repr__显示格式方法外,第三个最常用的运算符重载方法了。
这个方法最常用于Tkinter GUI工具箱中,可以把函数注册成事件处理器(也就是回调函数callback),但这个知识点在这里不做过多讲解。
========================================================================
比较:__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():如果两个方法都有,Python会优先调用__bool__方法。
def __len__(self):
return 0
>>> X = Truth()
>>> if not X:
print('no')
no
最后,如果没有定义真的方法,对象毫无疑义地看做为真:
>>> class Truth:========================================================================
pass
>>> X = Truth()
>>> bool(X)
True
对象析构函数:__del__
每当实例产生时,就会调用__init__构造函数。每当实例空间被回收时(在垃圾收集时),它的对立面__del__,也就是析构函数,就会自动执行:
>>> class Life:在这里,当Brian赋值为字符串时,我们就会失去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