继续介绍运算符重载的知识。
运算符重载(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 "", line 1, in
X.name
File "", 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 "", line 1, in
X.name = 'Gavin'
File "", 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 ''%self.val
>>> x = Commuter(88)
>>> y = Commuter(99)
>>> print(x+10)
>>> print(10+y)
>>> z = x+y
>>> z
<__main__.Commuter object at 0x037264D0>
>>> print(z)
>>> print(z+z)
------------------------------------------------------------------------------------
-----------------------------
>>> 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>> X = C()
>>> print(X>'ham')
True
>>> print(X<'ham')
False
>>> print('ham'
========================================================================
布尔测试:__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实例的最后一个引用,因此会触发其析构函数。