python裝飾器3:進階

函數裝飾器裝飾方法

函數裝飾器裝飾普通函數已經很容易理解了:html

@decorator
def func():...

#等價於
def func():...
func = decorator(func)

若是裝飾器是帶參裝飾器,那麼等價的形式大概是這樣的(和裝飾器的編碼有關,但最廣泛的編碼形式以下):app

@decorator(x, y, z)
def func():...

# 等價於
def func():...
func = decorator(x, y, z)(func)

這樣的函數裝飾器也能夠去裝飾類中的方法。看下面的方法裝飾形式:函數

class cls:
    @decorator
    def method(self,arg1,arg2):
        ...

它等價於:ui

class cls:
    def method(self,arg1,arg2):
        ...
    method = decorator(method)

在decorator的編碼中,仍然像普通的函數裝飾器同樣編寫便可。例如:編碼

def decorator(F):
    @wraps(F)
    def wrapper(*args, **kwargs):
        ...    # args[0] = self_instance
               # args[1]開始纔是手動傳給method的參數
    return wrapper

但必需要考慮到method的第一個參數self,因此包裝器wrapper()的第一個參數也是self。code

如此一來,函數裝飾器既能夠裝飾函數,又能夠裝飾方法。htm

下面是一個示例:對象

from functools import wraps

def decorator(F):
    @wraps(F)
    def wrapper(*args, **kwargs):
        result = F(*args, **kwargs)
        print(args)
        return result
    return wrapper

@decorator
def func(x,y):
    return x + y

print(func(3, 4))

print("-" * 30)

class cls:
    @decorator
    def method(self, x, y):
        return x + y

c = cls()
print(c.method(3, 4))

輸出結果:blog

(3, 4)
7
------------------------------
(<__main__.cls object at 0x01DF1C50>, 3, 4)
7

讓類稱爲裝飾器

不只函數能夠做爲裝飾器,類也能夠做爲裝飾器去裝飾其它對象。get

如何讓類做爲裝飾器

要讓類做爲裝飾器,先看裝飾的形式:

class Decorator:
    ...

@Decorator
def func():
    ...

func(arg1, arg2)

若是成功裝飾,那麼它等價於:

def func(): ...
func = Decorator(func)

func(arg1, arg2)

這和函數裝飾器看上去是同樣的,但區別在於Decorator這裏是一個類,而不是函數,且Decorator(func)表示的是建立一個Decorator類的實例對象,因此這裏賦值符號左邊的func是一個對象。全部後面的func(arg1, arg2)是調用對象,而不是調用函數。

要讓實例對象成爲可調用對象,它必須實現__call__方法,因此應該在Decorator類中定義一個__call__。並且每次調用實例對象的時候,都是在調用__call__,這裏的__call__對等於函數裝飾器中的包裝器wrapper,因此它的參數和邏輯應當和wrapper同樣。

以下:

class Decorator():
    def __call__(self, *args, **kwargs):
        ...

再看func = Decorator(func),func是Decorator類建立實例的參數,因此Decorator類還必須實現一個__init__方法,接受func做爲參數:

class Decorator:
    def __init__(self, func):
        ...
    def __call__(self, *args, **kwargs):
        ...

元數據問題

這樣的裝飾器已經能正常工做了,可是會丟失func的元數據信息。因此,必須使用functools的wraps()保留func的元數據:

from functools import wraps

class Decorator:
    def __init__(self, func):
        wraps(func)(self)
        ...
    def __call__(self, *args, **kwargs):
        ...

爲何是wraps(func)(self)?這裏顯然不能@wraps(func)的方式裝飾包裝器,因此只能使用wraps()的原始函數形式。在wraps()裝飾函數包裝器wrapper的時候,@wraps(func)等價於wrapper = wraps(func)(wrapper),因此這裏wraps(func)(self)的做用也是很明顯的:保留func的元數據,並裝飾self。被裝飾的self是什麼?是Decorator的實例對象,由於Decorator類實現了__call__,因此self是可調用的,因此這裏的self相似於函數裝飾器返回的wrapper函數(實際上self是Decorator(func)返回的各個實例對象)。

類做爲裝飾器的參數問題

雖然self是Decorator的可調用實例對象,可是上面的代碼中self並不具備func屬性,也就是說沒法從self去調用func()函數,這彷佛使得整個過程都崩塌了:廢了老大的勁去解決各類裝飾器上的問題,結果卻不能調用被裝飾的函數。

有兩種方式能夠解決這個問題:

  1. __init__中使用self.func = func保留func對象做爲裝飾器的一個屬性
  2. 在使用wraps()後直接在包裝器__call__中使用__wrapped__調用原始func函數

這兩種方式實際上是等價的,由於self.func__wrapped__都指向原始的函數。

def __init__(self,func):
    wraps(func)(self)
    self.func = func
def __call__(self, *args, **kwargs):
    result = self.func(*args, **kwargs)

#-------------------------------

def __init__(self, func):
    wraps(func)(self)
def __call__(self, *args, **kwargs):
    result = self.__wrapped__(*args, **kwargs)

但這兩種方式都有缺陷,缺陷在於裝飾類中方法時。(注:在裝飾普通函數、類方法的時候,上面的方式不會出錯)

class cls:
    @decorator
    def method(self, x, y):...

由於self.func__wrapped__裝飾cls中的方法時指向的都是cls的類變量(只不過這個屬性是裝飾器類decorator的實例對象而已),做爲類變量,它沒法保存cls的實例對象,也就是說method(self, x, y)的self對裝飾器是不可見的。

用一個示例解釋更容易:

import types
from functools import wraps

# 做爲裝飾器的類
class decorator:
    def __init__(self,func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print("(1): ",self)      # (1)
        print("(2): ",self.func) # (2)
        print("(3): ",args)      # (3)
        return self.func(*args, **kwargs)

class cls:
    @decorator
    def method(self, x, y):
        return x + y

c = cls()
print("(4): ",c.method)      # (4)
print(c.method(3, 4))

輸出結果:

(4):  <__main__.decorator object at 0x03261630>
(1):  <__main__.decorator object at 0x03261630>
(2):  <function cls.method at 0x032C2738>
(3):  (3, 4)
Traceback (most recent call last):  File "g:/pycode/scope.py", line 21, in <module>
    print(c.method(3, 4))
  File "g:/pycode/scope.py", line 12, in __call__    return self.func(*args, **kwargs)
TypeError: method() missing 1 required positionalargument: 'y'

注意觀察上面__call__中輸出的幾個對象:

  • self對應的是decorator的實例對象method,而非cls的實例對象c,看輸出結果的前兩行便可知
  • self.func指向的是原始方法method,它是類變量,是類方法(函數),是裝飾器賦予它做爲函數的。也就是說,self.func指向的不是對象方法,而是類方法,類方法不會自動傳遞實例對象
  • args中保存的參數列表是(3, 4),可是cls.method中多了一個self位置參數,使得3賦值給了self,4被賦值給了x,y成了多餘的,因此最後報錯須要位置參數y。

若是將上面的method()的定義修改一下,把self去掉,將會正確執行:

class cls:
    @decorator
    def method(x, y):
        return x + y

執行結果:

(4):  <__main__.decorator object at 0x03151630>
(1):  <__main__.decorator object at 0x03151630>
(2):  <function cls.method at 0x031B2738>
(3):  (3, 4)
7

所以參數問題必須解決。解決方案是進行判斷:若是是經過實例對象觸發的方法調用(即c.method()),就將外部函數經過types.MethodType()連接到這個實例對象中,不然就返回原始self(由於它指向被裝飾的原始對象)。

這須要藉助描述符來實現,關於這一段的解釋,我以爲直接看代碼自行腦部更佳。

class decorator:
    def __init__(self,func):
        wraps(func)(self)
        self.func = func
    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)
    def __get__(self, instance, owner):
        # 若是不是經過對象來調用的
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

class cls:
    @decorator
    def method(self, x, y):
        return x + y

c = cls()
print(c.method(3, 4))  # 調用__get__後調用__call__

對於__wrapped__也同樣可行:

class decorator():
    def __init__(self, func):
        wraps(func)(self)
    def __call__(self, *args, **kwargs):
        return self.__wrapped__(*args, **kwargs)
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

裝飾時是否帶參數

若是要讓做爲裝飾器的類在裝飾時帶參數,就像函數裝飾器帶參同樣decorator(x,y,z)(func),能夠將參數定義在__init__上進行處理,而後在__call__中封裝一層。

class Decorator:
    def __init__(self, *args, **kwargs):
        ... do something with args ...
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper

和函數裝飾器同樣,若是想要達到下面這種既能無參裝飾,又能帶參裝飾:

@Decorator         # 無參裝飾
@Decorator(x,y,z)  # 帶參裝飾
@Decorator()       # 帶參裝飾,只不過沒給參數

能夠直接在__init__上進行參數有無的判斷:

import types
from functools import wraps, partial

class Decorator:
    def __init__(self, func=None, arg1=1, arg2=2, arg3=3):
        # 帶參裝飾器
        if func is None:
            self.func = partial(Decorator, arg1=arg1, arg2=arg2, arg3=arg3)
        else:       # 無參裝飾器
            wraps(func)(self)
            self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

這樣的限制是裝飾器若是帶參數時,必須使用keyword方式指定參數。例如:

# 帶參裝飾普通函數,使用keywords參數方式
@Decorator(arg1=1, arg2=3, arg3=5)
def func(x, y):
    return x + y

print(func(11, 22))

print('-' * 30)

# 無參裝飾普通函數
@Decorator
def func1(x, y):
    return x + y

print(func1(111, 22))

print('-' * 30)

# 無參裝飾方法
class cls:
    @Decorator
    def method(self, x, y):
        return x + y

c = cls()
print(c.method(3, 4))

print('-' * 30)

# 帶參裝飾方法
class cls1:
    @Decorator(arg1=1, arg2=3, arg3=5)
    def method(self, x, y):
        return x + y

cc = cls1()
print(cc.method(3, 4))

總結:類做爲裝飾器的通用格式

若是不考慮裝飾時是否帶參數的問題,根據上面的一大堆分析,類做爲裝飾器時的通用代碼格式以下:

import types
from functools import wraps

class Decorator:
    def __init__(self, func):
        wraps(func)(self)
        # self.func = func

    def __call__(self, *args, **kwargs):
        # return self.func(*args, **kwargs)
        # return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

至於選擇self.func的方式,仍是self.__wrapped__的方式,隨意。

若是須要考慮裝飾時帶參數問題,那麼參考上一小節內容。

選擇類誰做爲裝飾器?

函數能夠做爲裝飾器,類也能夠做爲裝飾器。它們也都能處理處理各類需求,可是類做爲裝飾器的時候解釋了好大一堆,很是麻煩。因此,若是能夠的話,選擇函數做爲裝飾器通常會是最佳方案。

相關文章
相關標籤/搜索