Python 2 - 高級用法 - 裝飾器

https://xiaozhuanlan.com/gopy
https://xiaozhuanlan.com/topic/9820573614

Python 2 - 高級用法 - 裝飾器

一談到 裝飾器,就離不開閉包

閉包

閉包就是能夠讀取其他函數內部變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。

作用域

瞭解 閉包之前,先來看一下作用域

作用域是程序運行時變量可被訪問的範圍,定義在函數內的變量是局部變量,局部變量的作用範圍只能是函數範圍內,它不能在函數外引用。當函數結束時,變量也會跟隨函數結束而變得不可以被訪問。

source


## 作用域
def test_1():
    test = 1

test_1()
print(test)
/gopy # python Python/Python2/1-1.py
Traceback (most recent call last):
  File "Python/Python2/1-1.py", line 10, in <module>
    print(test)
NameError: name 'test' is not defined

當你在 test_1()函數外部嘗試訪問作用域中的變量 test時,此時 Python解釋器會向你報告錯誤信息: NameError: name 'test' is not defined。說明此時的 test變量並不能在test_1()外部被訪問到。

閉包

你在函數 A 中定義了 函數 B,並將 B 作爲 A 的返回值返回, B 又使用了 A 中定義了的 變量,此時,可以形成了閉包。

# 閉包的形式
def A():
     a = 1
     def B():
          print(a)
     return B

下面的例子說明了閉包
test()函數 和 temp = []變量離開了定義它的函數 test_1()後,依然可以被 test()所保留。

source

## 閉包
def test_1():
    temp = []
    t = 1
    # 定義在函數內部的函數

    def test():
        temp.append(t)  # 使用了上層 test_1 中定義的局部變量:temp
        print(f"{temp}.append({t})")
    return test  # 注意,這裏返回的是 test 函數


func = test_1()  # 這裏獲取 test_1 中定義的 test

func()  # 這裏可以看到 test_1 的局部變量 temp 變成了 [1]
func()  # 說明可以通過 test 去訪問 temp 變量 和 t 變量
func()

# 輸出
/gopy # python Python/Python2/1-2.py
[1].append(1)
[1, 1].append(1)
[1, 1, 1].append(1)

所以,函數 test()是如何知道變量 tempt的呢?原來在 test的屬性中存在了一個用來存儲相關內容的列表 __closure__

for i in func.__closure__:
    print(i.cell_contents)

這下我們可以看到 閉包 通過 __closure__將 函數 test與 變量 tempt綁定在了一起。

1
[1, 1, 1]

裝飾器

瞭解了閉包,裝飾器就很好理解了。裝飾器其實是閉包的一種特殊形式。

原始工作

現在有一個工作方法work,用來輸出工作狀態i am working...並返回 100.

source

## 工作中
def work():
    work_time = 100
    print('i am working ...')
    return work_time
gopy # python Python/Python2/1-3.py
i am working ...

這時,突然你想增加一個功能,統計work的實際工作時間,最簡單的方法是更改 work 的源碼,增加計算時間的代碼。

## 計算時間的工作
import time

def work_1():
    work_time = 100
    start = time.time()
    print('i am working ...')
    stop = time.time()
    print(f'working time is : {stop - start}')
    return work_time
i am working ...
working time is : 2.5272369384765625e-05

這只是只有一個 work的情況,如果有幾十個上百個 work的話,也要每個都去改嗎?這時,裝飾器上線了!

## 裝飾器:計算時間
import time

def work_time_cal(func):  # 定義裝飾器,func 爲變量
    def wrap():  # 裝飾器內部閉包函數
        start = time.time()
        result =func()  # 運行 func
        stop = time.time()
        print(f'working time is : {stop - start}')
        return result
    return wrap  # 返回閉包函數

## 工作中
@work_time_cal  ## python 中的裝飾器語法爲 @ + 裝飾器名稱
def work_2():
    work_time = 100
    print('i am working 2 ...')
    return work_time

我們打印下work_2

print(work_2)
# <function work_time_cal.<locals>.wrap at 0x10314e510>

果然,work_2竟然變成了work_time_cal.wrap!說明 裝飾器 @ 起作用了,那我們來看下它的 __closure__中綁定的是什麼?

print(work_2.__closure__[0].cell_contents)
# <function work_2 at 0x10314e6a8>

原來是我們的 work_2。也就是 @work_time_calwork_2作爲 func傳入了 wrap。最後 work_2查看的時候變成了 wrap

那這個時候,我們運行 work_2的話,其實運行的是 wrap()閉包。而 wrap計算了開始結束時間,並輸出,然後返回了 work_2的結果。

 def wrap():  # 裝飾器內部閉包函數
        start = time.time()
        result =func()  # 運行 func
        stop = time.time()
        print(f'working time is : {stop - start}')
        return result
i am working 2 ...
working time is : 5.7220458984375e-06

效果是不是與在函數中直接修改是一樣的?所以 裝飾器是一種在不改變原有函數內部代碼的情況下,增加功能的一種方法。

現在,我們也發現了,其實 @ + 裝飾器的作用是,將 下面的函數名作爲參數,傳到了裝飾器中,作爲裝飾器的參數。

類裝飾器

類具有一個方法__call__,只要實現了這個方法的,那麼這個類的實例都是可以被調用的。python 中萬物皆對象,所以我們來看下平時使用的方法有沒有 __call__呢,他也時可調用的。

>>> def test():
...           print()
... 
>>> dir(test)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> test.__call__
<method-wrapper '__call__' of function object at 0x7fd009e71bf8>

我們在 test中的屬性中看到了__call__方法,他是一個 method-wrapper 類型。那麼我們調用test()的時候,實際上調用的也是 test.__call__()。所以通過 __call__提供了類作爲裝飾器的方法。

類裝飾器的實現實際上是通過 __init__在創建的時候傳入被裝飾函數作爲參數,創建類的實例,然後類具有的 __call__作爲實際運行的方法。

class WorkTimeCal12(object):
    def __init__(self, func):
        self.func = func

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

@WorkTimeCal12
def work_12():
    return 100

這裏的 @WorkTimeCal12 其實執行的是 WorkTimeCal12(work_12),最後獲得的實例 self.func = work_12。
所以我們來看下 被裝飾後的work_12是什麼類型呢?

>>> print(work_12)
<test.WorkTimeCal12 object at 0x7fd009e9b2b0>

是 WorkTimeCal12 d的實例對象,所以運行的是這個對象的__call__。它即等同於以下代碼:

def work_13():
    return 100

worktimecal12 = WorkTimeCal12(work_13)
worktimecal12()

帶參數的類裝飾器類型

被裝飾方法不帶參數,類裝飾器帶參數

# 類裝飾器,裝飾器帶參數,方法不帶參數

class WorkTimeCal13(object):
    def __init__(self, func):
        self.func = func

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

@WorkTimeCal13
def work_13():
    return 100

work_13()
print(work_13)

多個裝飾器的疊加使用與運行次序

多個裝飾器的疊加使用

# 多個裝飾器疊加

def test1(func):
    def wrap():
        print("i am test 1 begin")
        rev = func()
        print("i am test 1 end")
        return rev
    return wrap


def test2(func):
    def wrap():
        print("i am test 2 begin")
        rev = func()
        print("i am test 2 end")
        return rev
    return wrap

@test1
@test2
def test_3():
    print("i am test 3")

test_3()
<function test1.<locals>.wrap at 0x1074b5a60>
i am test 1 begin
i am test 2 begin
i am test 3
i am test 2 end
i am test 1 end

多個裝飾器的運行次序是什麼呢?

通過結果可以看到,實際的運行是這樣的:

test_1 begin
    test_2 begin
         test_3 begin - end
    test_2 end
test_1 end

所以,疊加裝飾器,實際上是離方法最近的裝飾器 test_2,在裝飾完方法 test_3後,返回的 test_2.wrap又被 test_1裝飾,返回了 test_1.wrap

由此可知,疊加裝飾器的運行次序實際上是由最外層的裝飾器依次向內部運行,最後執行被裝飾的方法。

functools.wrap

被裝飾的方法被修改了成了裝飾器方法的返回值,那麼如何保留之前的方法屬性呢?答案就是 functools.wrap裝飾器

# functools.wrap
import functools

def testfunc(func):
    @functools.wraps(func)
    def wrap():
        return func()
    return wrap

@testfunc
def test_wrap():
    print('test wrap')

print(test_wrap)
test_wrap()
<function test_wrap at 0x10fc9dbf8>
test wrap

那麼這個名稱相同的函數,還是原來的函數麼?

print(test_wrap)
#<function test_wrap at 0x103d47bf8>

print(test_wrap.__closure__[0].cell_contents)
#<function test_wrap at 0x103d47b70>

它們並不相同,原來是名字修改成了被裝飾函數名, __closure__存儲的還是被裝飾函數。

其他類型的裝飾器

帶有確定參數的裝飾器

注意,固定參數和不定參數的區別只是形式問題,只要正常傳入即可,結構都是相同的

裝飾器無參數,被裝飾的函數有固定的參數

source

## 裝飾器:計算時間 - 被裝飾的函數有參數:固定參數,裝飾器無參數
def work_time_cal_2(func):
    def wrap(param1, param2):  # 裝飾器內部閉包函數,帶兩個固定參數
        start = time.time()
        result = func(param1, param2)  # 運行 func
        stop = time.time()
        print(f'working time is : {stop - start}')
        return result
    return wrap  # 返回閉包函數

@work_time_cal_2
def work_2(param1, param2):
    print(f"params is {param1} and {param2}")
    return 100

work_2(123, 245)

#params is 123 and 245
#working time is : 1.2874603271484375e-05

裝飾器有固定參數,被裝飾的函數沒有參數

## 裝飾器:計算時間 - 被裝飾的函數沒有參數,裝飾器有參數:固定參數
def work_time_cal_4(param1):
    print(f"work time cal 4's params is : {param1}")
    def wrap_func(func): # 裝飾器內部閉包函數 1 : 參數是 func
        def wrap():  # 裝飾器內部閉包函數 2 : 運行的是具體的 func
            start = time.time()
            result = func()  # 運行 func
            stop = time.time()
            print(f'working time is : {stop - start}')
            return result
        return wrap  # 返回閉包函數 2
    return wrap_func  # 返回閉包函數 1

@work_time_cal_4(1)
def work_4():
    return 100

work_4()

#work time cal 4's params is : 1
#working time is : 9.5367431640625e-07

裝飾器有固定參數,被裝飾的函數也有固定參數

## 裝飾器:計算時間 - 被裝飾的函數有參數:固定參數,裝飾器有參數:固定參數
def work_time_cal_6(param1):
    print(f"work time cal 6's params is : {param1}")
    def wrap_func(func): # 裝飾器內部閉包函數 1 : 參數是 func
        def wrap(in1, in2):  # 裝飾器內部閉包函數 2 : 運行的是具體的 func
            print(f"work time cal 6-wrap's params is {in1} and {in2}")
            start = time.time()
            result = func(in1, in2)  # 運行 func
            stop = time.time()
            print(f'working time is : {stop - start}')
            return result
        return wrap  # 返回閉包函數 2
    return wrap_func  # 返回閉包函數 1

@work_time_cal_6(1)
def work_6(in1, in2):
    return in1 + in2

work_6(1, 2)

#work time cal 6's params is : 1
#work time cal 6-wrap's params is 1 and 2
#working time is : 1.1920928955078125e-06

帶有不確定參數的裝飾器

裝飾器無參數,被裝飾的函數有固定參數

## 裝飾器:計算時間 - 被裝飾的函數有參數:不固定參數,裝飾器無參數
def work_time_cal_3(func):
    def wrap(*args, **kwargs):  # 裝飾器內部閉包函數,帶不固定參數
        start = time.time()
        result = func(*args, **kwargs)  # 運行 func
        stop = time.time()
        print(f'working time is : {stop - start}')
        return result

    return wrap  # 返回閉包函數

@work_time_cal_3
def work_3(*args, **kwargs):
    print(f"params is {args} and {kwargs}")
    return 100
#params is (1, 2, 3, 4, 5) and {'p': -1, 'p1': -2}
#working time is : 1.3113021850585938e-05

裝飾器有不固定參數,被裝飾的函數無參數

## 裝飾器:計算時間 - 被裝飾的函數無參數,裝飾器有參數:不固定參數
def work_time_cal_5(*args, **kwargs):
    print(f"work time cal 5's params is : {args} and {kwargs}")
    def wrap_func(func): # 裝飾器內部閉包函數 1 : 參數是 func
        def wrap():  # 裝飾器內部閉包函數 2 : 運行的是具體的 func
            start = time.time()
            result = func()  # 運行 func
            stop = time.time()
            print(f'working time is : {stop - start}')
            return result
        return wrap  # 返回閉包函數 2
    return wrap_func  # 返回閉包函數 1

@work_time_cal_5(1, 2, 3, 4, p=1, p1=2)
def work_5():
    return 100

work_5()
#work time cal 5's params is : (1, 2, 3, 4) and {'p': 1, 'p1': 2}
#working time is : 7.152557373046875e-07

類方法裝飾器

類方法裝飾器不帶參數,類方法不帶參數

# 裝飾器:類方法裝飾器,類方法不帶參數
def work_time_cal_8(func):  # 帶有 self 來表示 類
    def wrap(self):  # 其實這裏 self 也是參數,只不過是 類的 self,他也可以叫別的名字,只要位於第一位參數即可
        start = time.time()
        result = func(self)  # 運行 func 時也是調用 func(self)
        stop = time.time()
        print(f'class working time is : {stop - start}')
        return result
    return wrap

class A(object):
    def __init__(self):
        pass

    @work_time_cal_8
    def work_8(self):
        return 100

a=A()
a.work_8()

類方法裝飾器不帶參數,類方法帶參數:固定參數


# 裝飾器:類方法裝飾器,類方法帶參數
def work_time_cal_9(func):  # 帶有 self 來表示 類
    def wrap(self, param1, param2):  # 其實這裏 self 也是參數,只不過是 類的 self,他也可以叫別的名字,只要位於第一位參數即可
        print(f'work time cal 9-wrap param is  : {param1} and {param2}')
        start = time.time()
        result = func(self, param1, param2)  # 運行 func 時也是調用 func(self),後面跟參數即可
        stop = time.time()
        print(f'class working time is : {stop - start}')
        return result
    return wrap

class B(object):
    def __init__(self):
        pass

    @work_time_cal_9
    def work_9(self, param1, param2):
        return 100

a=B()
a.work_9(1, 2)

裝飾器方法帶參數,類方法不帶參數

# 裝飾器:裝飾器方法帶參數,類方法不帶參數
def work_time_cal_10(param1, param2):  # 帶有 self 來表示 類
    print(f"work time cal 10's param is {param1} and {param2}")
    def wrap_class(func):
        def wrap(self):  # 其實這裏 self 也是參數
            start = time.time()
            result = func(self)  # 運行 func 時也是調用 func(self)
            stop = time.time()
            print(f'class working time is : {stop - start}')
            return result
        return wrap
    return wrap_class


class C(object):
    def __init__(self):
        pass

    @work_time_cal_10(1, 2)
    def work_10(self):
        return 100

c=C()
c.work_10()

裝飾器方法帶參數,類方法帶參數,都是不定參數

# 裝飾器:裝飾器方法帶參數,類方法帶參數,都是不定參數
def work_time_cal_11(*args, **kwargs):  # 帶有 self 來表示 類
    print(f"work time cal 10's param is {args} and {kwargs}")
    def wrap_class(func):
        def wrap(self, *args1, **kwargs1):  # 其實這裏 self 也是參數
            start = time.time()
            result = func(self, *args1, **kwargs1)  # 運行 func 時也是調用 func(self)
            stop = time.time()
            print(f'class working time is : {stop - start}')
            return result
        return wrap
    return wrap_class

class D(object):
    def __init__(self):
        pass

    @work_time_cal_11(1, 2, p=1, p1=2)
    def work_11(self, *args, **kwargs):
        print(f"work 11's params is {args} and {kwargs}")
        return 100

d = D()
d.work_11(3, 4, p=123, p1=234)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章