Python——運算子過載(2)
繼續介紹運算子過載的知識。
========================================================================
屬性引用:__getattr__和__setattr__
__getattr__方法是攔截屬性點號運算。確切地說,當通過對【未定義(即不存在)】的屬性名稱和例項進行點號運算時,就會用屬性名稱作為字串呼叫這個方法。如果Python可通過其繼承樹搜尋流程找到這個屬性,該方法就不會被呼叫。因為有這種情況,所以__getattr__可以作為鉤子來通過通用的方式響應屬性請求。如下例:
在這裡,empty類和其例項X本身沒有屬性,所以對X.age的存取會轉至__getattr__方法,self則賦值為例項(X),而attrname則賦值為未定義的屬性名稱字串('age')。這個類傳回一個實際值作為X.age點號表示式的結果(40),讓age看起來像實際的屬性。>>> 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
而對於請求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
模擬例項屬性的私有化:第一部分
下例程式把上一個例子通用化了,讓每個子類都有自己的私有變數名列表,這些變數名無法通過其例項進行賦值:
實際上,這是Python實現【屬性私有性】(也就是無法在類外對屬性名進行修改)的首選方法。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 #同理,這句話會報異常
不過,屬性私有性的更完整的的解決方案將會在以後講解,之後將會使用【類裝飾器】來更加通用地攔截和驗證屬性。
========================================================================
__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):
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__是最佳選擇。不過,通過分別定義這兩個方法,就可以在不同的環境內支援不同顯示。但是要記得:__str__和__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__。在實際的應用中,型別可能需要在結果中傳播,型別測試可能需要辨別它是否能夠安全地轉換並由此避免巢狀。例如,下面的程式碼如果沒有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:
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__方法都支援。當需要為函式的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():
def __len__(self):
return 0
>>> X = Truth()
>>> if not X:
print('no')
no
如果兩個方法都有,Python會優先呼叫__bool__方法。最後,如果沒有定義真的方法,物件毫無疑義地看做為真:
>>> class Truth:
pass
>>> X = Truth()
>>> bool(X)
True
========================================================================物件解構函式:__del__
每當例項產生時,就會呼叫__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例項的最後一個引用,因此會觸發其解構函式。