python裝飾器1:函數裝飾器詳解

先混個眼熟

誰能夠做爲裝飾器(能夠將誰編寫成裝飾器):html

  1. 函數
  2. 方法
  3. 實現了__call__的可調用類

裝飾器能夠去裝飾誰(誰能夠被裝飾):python

  1. 函數
  2. 方法

基礎:函數裝飾器的表現方式

假如你已經定義了一個函數funcA(),在準備定義函數funcB()的時候,若是寫成下面的格式:app

@funcA
def funcB():...

表示用函數funcA()裝飾函數funcB()。固然,也能夠認爲是funcA包裝函數funcB。它等價於:函數

def funcB():...

funcB = funcA(funcB)

也就是說,將函數funcB做爲函數funcA的參數,funcA會從新返回另外一個可調用的對象(好比函數)並賦值給funcB。編碼

因此,funcA要想做爲函數裝飾器,須要接收函數做爲參數,而且返回另外一個可調用對象(如函數)。例如:code

def funcA(F):
    ...
    ...
    return Callable

注意,函數裝飾器返回的可調用對象並不必定是原始的函數F,能夠是任意其它可調用對象,好比另外一個函數。但最終,這個返回的可調用對象都會被賦值給被裝飾的函數變量(上例中的funcB)。htm

函數能夠同時被多個裝飾器裝飾,後面的裝飾器之前面的裝飾器處理結果爲基礎進行處理:對象

@decorator1
@decorator2
def func():...

# 等價於
func = decorator1(decorator2(func))

當調用被裝飾後的funcB時,將自動將funcB進行裝飾,並調用裝飾後的對象。因此,下面是等價的調用方式:blog

funcB()          # 調用裝飾後的funcB
funcA(funcB)()

瞭解完函數裝飾器的表現後,大概也能猜到了,裝飾器函數能夠用來擴展、加強另一個函數。實際上,內置函數中staticmethod()、classmethod()和property()都是裝飾器函數,能夠用來裝飾其它函數,在後面會學到它們的用法。字符串

兩個簡單的例子

例如,函數f()返回一些字符串,如今要將它的返回結果轉換爲大寫字母。能夠定義一個函數裝飾器來加強函數f()。

def toupper(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@toupper
def f(x: str):    # 等價於f = toupper(f)
    return x

res = f("abcd")
print(res)

上面toupper()裝飾f()後,調用f("abcd")的時候,等價於執行toupper(f)("abcd"),參數"abcd"傳遞給裝飾器中的wrapper()中的*args,在wrapper中又執行了f("abcd"),使得本來屬於f()的整個過程都完整了,最後返回result.upper(),這部分是對函數f()的擴展部分。

注意,上面的封裝函數wrapper()中使用了*args **kwargs,是爲了確保任意參數的函數都能正確執行下去。

再好比要計算一個函數autodown()的執行時長,能夠額外定義一個函數裝飾器timecount()。

import time

# 函數裝飾器
def timecount(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

# 裝飾函數
@timecount
def autodown(n: int):
    while n > 0:
        n -= 1

# 調用被裝飾的函數
autodown(100000)
autodown(1000000)
autodown(10000000)

執行結果:

autodown 0.004986763000488281
autodown 0.05684685707092285
autodown 0.5336081981658936

上面wrapper()中的return是多餘的,是由於這裏裝飾的autodown()函數自身沒有返回值。但卻不該該省略這個return,由於timecount()能夠去裝飾其它可能有返回值的函數。

@functools.wraps

前面的裝飾器代碼邏輯上沒有什麼問題,可是卻存在隱藏的問題:函數的元數據信息丟了。好比doc、註解等。

好比下面的代碼:

@timecount
def autodown(n: int):
    ''' some docs '''
    while n > 0:
        n -= 1

print(autodown.__name__)
print(autodown.__doc__)
print(autodown.__annotations__)

執行結果爲:

wrapper
None
{}

因此,必需要將被裝飾函數的元數據保留下來。可使用functools模塊中的wraps()裝飾一下裝飾器中的wrapper()函數。以下:

import time
from functools import wraps

def timecount(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

如今,再去查看autodown函數的元數據信息,將會獲得被保留下來的內容:

autodown
 some doc
{'n': <class 'int'>}

因此,wraps()的簡單用法是:向wraps()中傳遞的func參數,那麼func的元數據就會被保留下來。

上面@wraps(func)裝飾wrapper的過程等價於:

def wrapper(*args, **kwargs):...
wrapper = wraps(func)(wrapper)

請注意這一點,由於在將類做爲裝飾器的時候,常常會在__init__(self, func)裏這樣使用:

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

解除裝飾

函數被裝飾後,如何再去訪問未被裝飾狀態下的這個函數?@wraps還有一個重要的特性,能夠經過被裝飾對象的__wrapped__屬性來直接訪問被裝飾對象。例如:

autodown.__wrapped__(1000000)

new_autodown = autodown.__wrapped__
new_autodown(1000000)

上面的調用不會去調用裝飾後的函數,因此不會輸出執行時長。

注意,若是函數被多個裝飾器裝飾,那麼經過__wrapped__,將只會解除第一個裝飾過程。例如:

@decorator1
@decorator2
@decorator3
def f():...

當訪問f.__wrapped__()的時候,只有decorator1被解除,剩餘的全部裝飾器仍然有效。注意,python 3.3以前是略過全部裝飾器。

下面是一個多裝飾的示例:

from functools import wraps


def decorator1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator1")
        return func(*args, **kwargs)
    return wrapper


def decorator2(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator2")
        return func(*args, **kwargs)
    return wrapper


def decorator3(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator3")
        return func(*args, **kwargs)
    return wrapper


@decorator1
@decorator2
@decorator3
def addNum(x, y):
    return x+y

返回結果:

in decorator1
in decorator2
in decorator3
5
in decorator2
in decorator3
5

若是不使用functools的@wraps的__wrapped__,想要手動去引用原始函數,須要作的工做可能會很是多。因此,若有須要,直接使用__wrapped__去調用未被裝飾的函數比較好。

另外,並非全部裝飾器中都使用了@wraps

帶參數的函數裝飾器

函數裝飾器也是能夠帶上參數的。

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

它等價於:

func = decorator(x,y,z)(func)

它並非"天生"就這樣等價的,而是根據編碼規範編寫裝飾器的時候,一般會這樣。其實帶參數的函數裝飾器寫起來有點繞:先定義一個帶有參數的外層函數,它是外在的函數裝飾器,這個函數內包含了真正的裝飾器函數,而這個內部的函數裝飾器的內部又包含了被裝飾的函數封裝。也就是函數嵌套了一次又一次。

因此,結構大概是這樣的:

def out_decorator(some_args):
    ...SOME CODE...
    def real_decorator(func):
        ...SOME CODE...
        def wrapper(*args, **kwargs):
            ...SOME CODE WITH func...
        return wrapper
    return real_decorator

# 等價於func = out_decorator(some_args)(func)
@out_decorator(some_args)
def func():...

下面是一個簡單的例子:

from functools import wraps

def out_decorator(x, y, z):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(x)
            print(y)
            print(z)
            return func(*args, **kwargs)
        return wrapper
    return decorator


@out_decorator("xx", "yy", "zz")
def addNum(x, y):
    return x+y

print(addNum(2, 3))

參數隨意的裝飾器

根據前面介紹的兩種狀況,裝飾器能夠帶參數、不帶參數,因此有兩種裝飾的方式,要麼是下面的(1),要麼是下面的(2)。

@decorator         # (1)
@decorator(x,y,z)  # (2)

因此,根據不一樣的裝飾方式,須要編寫是否帶參數的不一樣裝飾器。

可是如今想要編寫一個將上面兩種參數方式統一塊兒來的裝飾器。

可能第一想法是讓裝飾器參數默認化:

def out_decorator(arg1=X, arg2=Y...):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return decorator

如今能夠用下面兩種方式來裝飾:

@out_decorator()
@out_decorator(arg1,arg2)

雖然上面兩種裝飾方式會正確進行,但這並不是合理作法,由於下面這種最通用的裝飾方式會錯誤:

@out_decorator

爲了解決這個問題,回顧下前面裝飾器是如何等價的:

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

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

上面第二種方式中,out_decorator(x,y,z)纔是真正返回的內部裝飾器。因此,能夠修改下裝飾器的編寫方式,將func也做爲out_decorator()的其中一個參數:

from functools import wraps,partial

def decorator(func=None, arg1=X, arg2=Y):
    # 若是func爲None,說明觸發的帶參裝飾器
    # 直接返回partial()封裝後的裝飾器函數
    if func is None:
        decorator_new = partial(decorator, arg1=arg1, arg2=arg2)
        return decorator_new
        #return partial(decorator, arg1=arg1, arg2=arg2)
    
    # 下面是裝飾器的完整裝飾內容
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
    return wrapper

上面使用了functools模塊中的partial()函數,它能夠返回一個新的將某些參數"凍結"後的函數,使得新的函數無需指定這些已被"凍結"的參數,從而減小參數的數量。若是不知道這個函數,參考partial()用法說明

如今,能夠統一下面3種裝飾方式:

@decorator()
@decorator(arg1=x,arg2=y)
@decorator

前兩種裝飾方式,等價的調用方式是decorator()(func)decorator(arg1=x,arg2=y)(func),它們的func都爲None,因此都會經過partial()返回一般的裝飾方式@decorator所等價的形式。

須要注意的是,由於上面的參數結構中包含了func=None做爲第一個參數,因此帶參數裝飾時,必須使用keyword格式來傳遞參數,不能使用位置參數。

下面是一個簡單的示例:

from functools import wraps, partial


def decorator(func=None, x=1, y=2, z=3):
    if func is None:
        return partial(decorator, x=x, y=y, z=z)

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("x: ", x)
        print("y: ", y)
        print("z: ", z)
        return func(*args, **kwargs)
    return wrapper

下面3種裝飾方式均可以:

@decorator
def addNum(a, b):
    return a + b
print(addNum(2, 3))

print("=" * 40)

@decorator()
def addNum(a, b):
    return a + b
print(addNum(2, 3))

print("=" * 40)

# 必須使用關鍵字參數進行裝飾
@decorator(x="xx", y="yy", z="zz")
def addNum(a, b):
    return a + b
print(addNum(2, 3))

返回結果:

x:  1
y:  2
z:  3
5
====================
x:  1
y:  2
z:  3
5
====================
x:  xx
y:  yy
z:  zz
5
相關文章
相關標籤/搜索