基礎知識
運算符重載:意味着你在自己定義的類方法中攔截內置的操作,當類的實例出現在內置操作中,Python自動調用你的方法,並返回相應自定義操作的結果。
運算符重載一般應用於模擬內置對象,使得自己定義的對象更像是底層對象。
常用運算符重載方法
方法 | 重載 | 調用 |
---|---|---|
__init__ | 構造函數 | 對象建立:X=Class(args) |
__del__ | 析構函數 | X對象回收,del [object] |
__add__ | 運算符+ | 如果沒有定義__iadd__,在x+y,x+=y時調用 |
__or__ | 或運算符| | 如果沒有定義__ior__,X|Y,X|=Y時調用 |
__repr__,__str__ | 打印,轉換 | print(X),repr(X),str(X) |
__call__ | 函數調用 | X(*arg,**kargs) |
__getattr__ | 點號運算符 | X.undefined |
__setattr__ | 屬性賦值語句 | X.[atrribute]=value |
__delattr__ | 屬性刪除 | del X.[atrribute] |
__getattribute__ | 屬性獲取 | X.[atrribute] |
__getitem__ | 索引運算 | X[key],X[i:j],沒有__iter__時的for循環和其它迭代器 |
__setitem__ | 索引賦值語句 | X[key]=value,X[i:j]=sequence |
__delitem__ | 索引和分片刪除 | del X[key],del X[i:j] |
__len__ | 長度 | len(X) |
__bool__ | 布爾測試 | bool(X),真值測試(在python2.6中爲__nonzero__) |
__lt__,__gt__,__le__,__ge__,__eq__,__ne__ | 比較測試 | X < Y,X>Y,X<=Y,X>=Y,X==Y,X!=Y |
__radd__ | 右側加法 | other+X |
__iadd__ | 增強加法 | X+=Y |
__iter__,__next__ | 迭代環境 | 在迭代環境中使用 |
__contains__ | 成員關係測試 | item in X |
__index__ | 整數值 | hex(X),bin(X),oct(X),O(X) |
__enter__,__exit__ | 環境管理器 | with obj as var: |
__get__,__set__,__delete__ | 描述符屬性 | X.attr,X.attr=value,del X.attr |
__new__ | 創建 | 在__init__之前創建對象 |
所有重載方法的名稱前後都由兩個下劃線組成以區分其它方法。一些自定義的類沒有重載方法時,可能不支持相關運算符的操作,也可能是繼承了Python內的內置類
索引和分片:__getitem__和__setitem__
__getitem__是攔截索引和分片相關的函數:
>>> class Indexer:
... def __init__(self):
... self.data=[1,2,3,4,5,6]
... def __getitem__(self,index):
... return self.data[index]
...
...
>>> lxm=Indexer()
>>> lxm[4]
5
>>> for i in range(5):
... print(lxm[i])
...
1
2
3
4
5
>>> lxm[1:4]
[2, 3, 4]
>>> lxm[2:4]
[3, 4]
>>>
而__setitem__方法是攔截索引賦值和分片賦值的方法:
>>> class Indexer:
... def __init__(self):
... self.data=[1,2,3,4,5,6]
... def __getitem__(self,index):
... return self.data[index]
... def __setitem__(self,index,value):
... print("setitem used")
... print(index)
... print(value)
... self.data[index] = [i*value for i in self.data[index] ]
...
>>> lxm=Indexer()
>>> lxm[3:5]
[4, 5]
>>> lxm[3:5]=4 #調用相應的setitem方法來修改self.data中的數據。
setitem used
slice(3, 5, None)
4
>>> lxm[3:5]
[16, 20]
>>>
在迭代環境for,成員關係測試in,列表解析,內置函數map,列表和元組賦值運算以及類型構造方法也會自動調用__getitem__方法。
儘管__getitem__方法可以實現迭代,但是在Python中,所有迭代環境都會最先嚐試__iter__方法,再嘗試__getitem__方法。
迭代器對象:__iter__和__next__
之前介紹過迭代器,用戶可以通過自定義類中的方法來實現一個全新的迭代器。
>>> class iter:
... def __init__(self,start,end):
... self.start=start-1
... self.end=end
... def __iter__(self):
... print("call __iter__ function")
... return self
... def __next__(self):
... print("call __next__ function")
... if self.end == self.start:
... raise StopIteration
... self.start += 1
... return self.start**3
...
>>> lxm=iter(4,10)
>>> for i in lxm:
... print(i)
...
call __iter__ function #在for循環開始時,會調用iter方法
call __next__ function #每一次迭代都會調用對象的next方法
64
call __next__ function
125
call __next__ function
216
call __next__ function
343
call __next__ function
512
call __next__ function
729
call __next__ function
1000
call __next__ function #在next方法中,拋出了StopIteration異常
>>>
如果這時,繼續對迭代器進行迭代:
>>> for i in lxm:
... print(i)
...
call __iter__ function #對象中的迭代器還是使用的上次迭代完成後的對象
call __next__ function
>>> lxm.start #屬性並沒有重新初始化
10
>>>
要達到可以使用多個迭代器的對象,那麼就要重新編寫__iter__方法。
>>> class iter:
... def __init__(self,start,end):
... self.start=start
... self.value=start-1
... self.end=end
... def __iter__(self):
... print("call __iter__ function")
... return iter(self.start,self.end) #每次迭代開始時,都會返回一個新的迭代器
... def __next__(self):
... print("call __next__ function")
... if self.end == self.value:
... raise StopIteration
... self.value += 1
... return self.value**3
...
>>> lxm=iter(4,6)
>>> for i in lxm:
... print(i)
...
call __iter__ function
call __next__ function
64
call __next__ function
125
call __next__ function
216
call __next__ function
>>> for i in lxm: #可以開始多次迭代
... print(i)
...
call __iter__ function
call __next__ function
64
call __next__ function
125
call __next__ function
216
call __next__ function
>>>
成員關係:__contain__,__iter__和__getitem__
成員關係判斷中都都會使用in語句,實際上in語句也會間接的運用迭代協議,接着上面的例子看到,變量lxm中有64,125,216三個整數,那麼對變量lxm進行成員關係判斷時可以看到:
>>> 111 in lxm #間接的都調用了迭代環境中的函數。
call __iter__ function
call __next__ function
call __next__ function
call __next__ function
call __next__ function
False
>>> 64 in lxm
call __iter__ function
call __next__ function
True
>>>
有時候運算符的重載往往是多個層級的,類可以提供特定的方法來重載運算符,或者可以退而求其次的選項來選擇更通用的運算符重載方法:
- Python2.6中的比較使用__lt__等這樣特殊的比較方法(如果有的話)或者使用通用的__cmp__。
- 布爾測試會先找__bool__方法,如果沒有就會找__len__方法
- 成員關係測試中,Python最先找的方法是__contains__方法,即__contains__方法最先攔擊關係測試,如果沒有該方法,纔會進行__iter__方法,最後纔是__getitem方法,可以看出__getitem__方法更爲通用。
屬性引用:__getattr__和__setattr__
__getattr__方法是攔截屬性點號運算。更確切地說,當通過對未定義(不存在)屬性名稱和實例進行點號運算時,就會用屬性名稱作爲字符串調用該方法。通常用作響應一些不存在的屬性調用時,使用:
>>> class empty:
... def __getattr__(self,attrname):
... print(self,attrname)
... return "No this attrname"
...
>>> lxm=empty()
>>> lxm.aaa # 對不存在的屬性執行點運算,會直接調用getattr
<__main__.empty object at 0x7f482be85128> aaa
'No this attrname'
>>> lxm.aaa=123 #定義了該屬性後,調用就不會去調用getattr屬性
>>> lxm.aaa
123
>>>
__setattr__會攔截所有屬性的賦值語句,如果定義了這個方法,賦值語句self.attr=value,就會變成self.__setattr__(‘attr’,value):
注意到任何對該類的屬性賦值都會調用_setattr_方法,所以在該方法內部對實例賦值時,就不能使用通常的賦值語句self.attr=value,否則會讓函數產生循環遞歸,最後導致堆棧溢出,而要使用對屬性字典做索引運算來賦值任何實例的屬性。(self._dict_[‘name’]=X)
>>> class setattribute:
... def __setattr__(self,attr,value):
... print(attr,value)
... if attr == 'age' and value >100:
... self.__dict__['age']="Old man"
... else:
... self.__dict__[attr]=value
...
>>> lxm=setattribute()
>>> lxm.name='liximin' #使用賦值語句時,會調用setattr函數
name liximin
>>> lxm.name
'liximin'
>>> lxm.age=200
age 200
>>> lxm.age
'Old man'
>>>
其它屬性管理工具
- __getattribute__方法可以攔截所有屬性點運算的獲取,而不僅僅是未定義的屬性。使用時更要注意避免循環遞歸。
- Property內置函數允許我們把方法和特定類屬性上的獲取和設置操作關聯起來。
- 描述符提供了一個協議,把一個類的__get__和__set__方法對特定類的屬性訪問聯繫起來。
這些方法與工具並不是我們開發python應用程序常用的,都是在開發python工具或者偏底層的程序工具時有廣泛使用。
__repr__和__str__會返回字符串表達形式
這兩個函數,在打印對象或者將對象轉化成爲字符串時,都會調用。而這兩個方法的不同就在於,終端用戶使用__str__方法而程序員在開發期間則使用底層的__repr__來顯示。實際上,__str__只是覆蓋了__repr__以得到用戶友好的顯示環境。
>>> class printclass:
... def __str__(self):
... print('str function called')
... return "Hello str function"
... def __repr__(self):
... print('repr function called')
... return "Hello Python repr"
...
>>> lxm=printclass()
>>> lxm
repr function called
Hello Python repr
>>> print(lxm) #調用str函數
str function called
Hello str function
>>> class printclass2:
... #def __str__(self):
... # print('str function called')
... # return "Hello str function"
... def __repr__(self):
... print('repr function called')
... return "Hello Python repr"
...
>>> lxm2=printclass2()
>>> lxm2 #交互模式下直接調用repr函數
repr function called
Hello Python repr
>>> print(lxm2) #print函數在沒有定義str的情況下,會去調用repr函數
repr function called
Hello Python repr
>>>
需要注意的是,str和repr函數都必須返回字符串,如果返回其它類型的數據並不會自動轉化,會產生錯誤。在實際應用中除了__init__函數,就是__str__函數用得最多了。
右側加法和原處加法:__radd__和__iadd__
從技術上講,如果真要實現加法,交換加數位置,結果不變,那麼再實現__add__方法的同時也要實現右側加法__radd__。當類實例在加號右側會調用radd函數,而在加號左側就會調用add方法。在原處加法運算符x+=1則由方法__iadd__攔截。
>>> class sumclass:
... def __init__(self,value):
... self.value=value
... def __add__(self,value):
... print('add function called',self.value,value)
... return self.value+value
... def __radd__(self,value):
... print('radd function called',self.value,value)
... return self.value+value
...
>>> lxm1=sumclass(12)
>>> lxm2=sumclass(44)
>>> lxm1+3
add function called 12 3
15
>>> lxm2+99
add function called 44 99
143
>>> 55+lxm1
radd function called 12 55
67
>>> lxm1+lxm2
add function called 12 <__main__.sumclass object at 0x7f482be85550> #首先調用add函數,add函數在做加法時
radd function called 44 12 #右側是實例對象,那麼就會調用radd函數,最後返回結果
56
>>>
Call表達式:__call__
當調用一個實例時,會使用__call__方法。(當創建一個實例時,會調用__init__方法),這讓實例的使用方法從編碼外觀上來看就像調用了一個函數。
>>> class Callclass:
... def __call__(self): #這裏的call函數比較簡單,並沒有傳遞任何參數,只是簡單的類似於函數的調用。
... print('call function called')
...
>>> lxm=Callclass()
>>> lxm()
call function called
>>>
對象析構函數:__del__
當實例的內存空間被回收時,它會執行析構函數,由於以下原因,析構函數在Python編程中很少使用:
- Python在執行回收時由於是自動回收內存空間,所以對於回收空間來說,是不需要析構函數的。
- 無法準確的預測何時執行析構函數,當前對象可能在其他地方使用,而導致不會執行析構函數。
小結
本章主要介紹了類中運算符重載的一些方法,這些方法讓類的編寫有更高的靈活性。很多工具都會使用這些重載函數,理解這些內容,對工具的使用有重要意義。在定義的類自然的映射到運算符操作時,可以使用重載,來提高代碼的可讀性和靈活性,如果沒有自然的邏輯映射關係,那麼就用普通的類方法來進行處理。