1. 程式人生 > >Python——運算子過載(2)

Python——運算子過載(2)

繼續介紹運算子過載的知識。

========================================================================

屬性引用:__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看起來像實際的屬性。

而對於請求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):
    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實現【屬性私有性】(也就是無法在類外對屬性名進行修改)的首選方法。

不過,屬性私有性的更完整的的解決方案將會在以後講解,之後將會使用【類裝飾器】來更加通用地攔截和驗證屬性。
========================================================================
__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例項的最後一個引用,因此會觸發其解構函式。