1. 程式人生 > >Python——編寫類裝飾器

Python——編寫類裝飾器

編寫類裝飾器

類裝飾器類似於函式裝飾器的概念,但它應用於類,它們可以用於管理類自身,或者用來攔截例項建立呼叫以管理例項。

-------------------------------------------------------------------------------------------------------------------------------------

單體類
由於類裝飾器可以攔截例項建立呼叫,所以它們可以用來管理一個類的所有例項,或者擴充套件這些例項的介面。
下面的類裝飾器實現了傳統的單體編碼模式,即最多隻有一個類的一個例項存在。

instances = {} # 全域性變數,管理例項
def getInstance(aClass, *args):
    if aClass not in instances:
        instances[aClass] = aClass(*args)
    return instances[aClass]     #每一個類只能存在一個例項

def singleton(aClass):
    def onCall(*args):
        return getInstance(aClass,*args)
    return onCall
為了使用它,裝飾用來強化單體模型的類:
@singleton        # Person = singleton(Person)
class Person:
    def __init__(self,name,hours,rate):
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):
        return self.hours * self.rate

@singleton        # Spam = singleton(Spam)
class Spam:
    def __init__(self,val):
        self.attr = val
        
bob = Person('Bob',40,10)
print(bob.name,bob.pay())

sue = Person('Sue',50,20)
print(sue.name,sue.pay())

X = Spam(42)
Y = Spam(99)
print(X.attr,Y.attr)
現在,當Person或Spam類稍後用來建立一個例項的時候,裝飾器提供的包裝邏輯層把例項構建呼叫指向了onCall,它反過來呼叫getInstance,以針對每個類管理並分享一個單個例項,而不管進行了多少次構建呼叫。
程式輸出如下:
Bob 400
Bob 400
42 42
在這裡,我們使用全域性的字典instances來儲存例項,還有一個更好的解決方案就是使用Python3中的nonlocal關鍵字,它可以為每個類提供一個封閉的作用域,如下:
def singleton(aClass):
	instance = None
	def onCall(*args):
		nonlocal instance
		if instance == None:
			instance = aClass(*args)
		return instance
	return onCall
當然,我們也可以用類來編寫這個裝飾器——如下程式碼對每個類使用一個例項,而不是使用一個封閉作用域或全域性表:
class singleton:
	def __init__(self,aClass):
		self.aClass = aClass
		self.instance = None
	def __call__(self,*args):
		if self.instance == None:
			self.instance = self.aClass(*args)
		return self.instance
-------------------------------------------------------------------------------------------------------------------------------------

跟蹤物件介面
類裝飾器的另一個常用場景是每個產生例項的介面。類裝飾器基本上可以在例項上安裝一個包裝器邏輯層,來以某種方式管理其對介面的訪問。
前面,我們知道可以用__getattr__運算子過載方法作為包裝嵌入到例項的整個物件介面的方法,以便實現委託編碼模式。__getattr__用於攔截未定義的屬性名的訪問。如下例子所示:

class Wrapper:
	def __init__(self,obj):
		self.wrapped = obj
	def __getattr__(self,attrname):
		print('Trace:',attrname)
		return getattr(self.wrapped,attrname)

	
>>> x = Wrapper([1,2,3])
>>> x.append(4)
Trace: append
>>> x.wrapped
[1, 2, 3, 4]
>>>
>>> x = Wrapper({'a':1,'b':2})
>>> list(x.keys())
Trace: keys
['b', 'a']
在這段程式碼中,Wrapper類攔截了對任何包裝物件的屬性的訪問,打印出一條跟蹤資訊,並且使用內建函式getattr來終止對包裝物件的請求。

類裝飾器為編寫這種__getattr__技術來包裝一個完整介面提供了一個替代的、方便的方法。如下:
def Tracer(aClass):
    class Wrapper:
        def __init__(self,*args,**kargs):
            self.fetches = 0
            self.wrapped = aClass(*args,**kargs)
        def __getattr__(self,attrname):
            print('Trace:'+attrname)
            self.fetches += 1
            return getattr(self.wrapped,attrname)
    return Wrapper

@Tracer
class Spam:
    def display(self):
        print('Spam!'*8)

@Tracer
class Person:
    def __init__(self,name,hours,rate):
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):
        return self.hours * self.rate

food = Spam()
food.display()
print([food.fetches])

bob = Person('Bob',40,50)
print(bob.name)
print(bob.pay())

print('')
sue = Person('Sue',rate=100,hours = 60)
print(sue.name)
print(sue.pay())

print(bob.name)
print(bob.pay())
print([bob.fetches,sue.fetches])
通過攔截例項建立呼叫,這裡的類裝飾器允許我們跟蹤整個物件介面,例如,對其任何屬性的訪問。

Spam和Person類的例項上的屬性獲取都會呼叫Wrapper類中的__getattr__邏輯,由於food和bob確實都是Wrapper的例項,得益於裝飾器的例項建立呼叫重定向,輸出如下:
Trace:display
Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam!
[1]
Trace:name
Bob
Trace:pay
2000

Trace:name
Sue
Trace:pay
6000
Trace:name
Bob
Trace:pay
2000
[4, 2]
========================================================================================

示例:實現私有屬性
如下的類裝飾器實現了一個用於類例項屬性的Private宣告,也就是說,屬性儲存在一個例項上,或者從其一個類繼承而來。不接受從裝飾的類的外部對這樣的屬性的獲取和修改訪問,但是,仍然允許類自身在其方法中自由地訪問那些名稱。類似於Java中的private屬性。

traceMe = False
def trace(*args):
    if traceMe:
        print('['+ ' '.join(map(str,args))+ ']')

def Private(*privates):
    def onDecorator(aClass):
        class onInstance:
            def __init__(self,*args,**kargs):
                self.wrapped = aClass(*args,**kargs)
            def __getattr__(self,attr):
                trace('get:',attr)
                if attr in privates:
                    raise TypeError('private attribute fetch:'+attr)
                else:
                    return getattr(self.wrapped,attr)
            def __setattr__(self,attr,value):
                trace('set:',attr,value)
                if attr == 'wrapped': # 這裡捕捉對wrapped的賦值
                    self.__dict__[attr] = value
                elif attr in privates:
                    raise TypeError('private attribute change:'+attr)
                else: # 這裡捕捉對wrapped.attr的賦值
                    setattr(self.wrapped,attr,value)
        return onInstance
    return onDecorator

if __name__ == '__main__':
    traceMe = True

    @Private('data','size')
    class Doubler:
        def __init__(self,label,start):
            self.label = label
            self.data = start
        def size(self):
            return len(self.data)
        def double(self):
            for i in range(self.size()):
                self.data[i] = self.data[i] * 2
        def display(self):
            print('%s => %s'%(self.label,self.data))

    X = Doubler('X is',[1,2,3])
    Y = Doubler('Y is',[-10,-20,-30])

    print(X.label)
    X.display()
    X.double()
    X.display()

    print(Y.label)
    Y.display()
    Y.double()
    Y.label = 'Spam'
    Y.display()

    # 這些訪問都會引發異常
    """
    print(X.size())
    print(X.data)

    X.data = [1,1,1]
    X.size = lambda S:0
    print(Y.data)
    print(Y.size())
這個示例運用了裝飾器引數等語法,稍微有些複雜,執行結果如下:
[set: wrapped <__main__.Doubler object at 0x03421F10>]
[set: wrapped <__main__.Doubler object at 0x031B7470>]
[get: label]
X is
[get: display]
X is => [1, 2, 3]
[get: double]
[get: display]
X is => [2, 4, 6]
[get: label]
Y is
[get: display]
Y is => [-10, -20, -30]
[get: double]
[set: label Spam]
[get: display]
Spam => [-20, -40, -60]