1. 程式人生 > >Python自動化開發學習4-裝飾器

Python自動化開發學習4-裝飾器

python

裝飾器

通過裝飾器可以為函數添加附加功能。通過修改函數本身也可以實現增加功能,但是通過裝飾器還有下面2個好處。裝飾器的2個原則:

  1. 不改變被裝飾函數的源代碼

  2. 不改變被裝飾函數的調用方式

遵循了上面2個原則,我們可以在為函數添加附加功能的時候,不必去破壞已有的穩定的代碼和代碼調用方式。並且易於回退。

先定義一個函數:

import time
def sleep():
    "運行後等待2秒"
    print("wait for 2 seconds")
    time.sleep(2) 
    print("END")
sleep()

現在我希望為這個函數附加一個功能,計算這個函數運行的時間,但是我不希望修改sleep函數的源代碼,所以我們再定義一個函數來附加這個功能

import time
def sub_a(func):
    "計算參數的函數的運行時間"
    t = time.time()  # 記錄當前時間戳
    func()
    print(time.time() - t)  # 現在的時間戳和之前的時間戳的差值就是中間這個函數的運行時間
def sleep():
    "運行後等待2秒"
    print("wait for 2 seconds")
    time.sleep(2) 
    print("END")
sub_a(sleep)

我們未修改sleep函數,而是定義了一個新函數。通過這個新的函數執行附加的代碼並且用參數調用執行被裝飾的函數。

但是這裏我們的調用方式變了,現在需要調用新的函數,並且要使用原來的調用方式sleep()來調用。

在之前的基礎上,我們給裝飾器外面再套上一層函數。先看代碼,下面再分析實現過程。

import time
def sub_b(func):
    def sub_a():
        "計算參數的函數的運行時間"
        t = time.time()
        func()
        print(time.time() - t)
    return sub_a  # 返回sub_a函數的內存地址
def sleep():
    "運行後等待2秒"
    print("wait for 2 seconds")
    time.sleep(2) 
    print("END")
sleep = sub_b(sleep)
sleep()

sleep = sub_b(sleep)語句賦值後,sleep的值是sub_b函數的返回值,也就是sub_a的內存地址,而且sub_a裏的func也已經賦了sleep的內存地址(在調用sub_b的時候作為參數傳入了sleep的內存地址)。

最後我們再sleep()實際調用的是sub_a(),而sub_a裏的func()執行的是sleep()。附加代碼和sleep的源代碼都執行了,並且最後的調用方式也沒變。

這裏再插入解釋一個小問題,sub_a現在只是sub_b函數裏的一個子函數,局部變量。也就是調用完sub_b之後,sub_a是會被釋放的。

但是由於sub_b最後的return sub_a返回了sub_a的內存地址,並且在sleep=sub_b(sleep),將這個內存地址又賦值給了sleep。所以在sub_b調用結束後,sub_a還再被sleep占用著,所以並沒有釋放sub_a。

至此不改變調用方式的原則也實現了。

再看看參數,之前使用的函數都沒有參數,如果為sleep()函數加一個參數,那麽代碼就報錯。所以目前的裝飾器只能裝飾不帶參數的函數,要適應各種參數類型的函數,我們需要使用不固定參數

import time
def sub_b(func):
    def sub_a(*args,**kwargs):  # 這裏無腦的加上不固定參數
        "計算參數的函數的運行時間"
        t = time.time()
        func(*args,**kwargs)  # 將所有參數都傳給被裝飾函數
        print(time.time() - t)
    return sub_a
def sleep(n):  # 現在我們換一個有參數的函數來裝飾它
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    print("END")
sleep = sub_b(sleep)
sleep(2)

簡單無腦的加上不固定參數就行了,具體是什麽參數,具體有多少參數我們不用關心,只要全部傳過去就行了。

如此,參數也不是問題了。還有一個返回值的問題,將sleep修改成一個有返回值的函數。又會有問題,這次不報錯,但是返回值是None。

再修改,調用被裝飾函數時,將返回值保存。裝飾器最後再return這個值給裝飾器。

import time
def sub_b(func):
    def sub_a(*args,**kwargs):
        "計算參數的函數的運行時間"
        t = time.time()
        res = func(*args,**kwargs)  # 將被裝飾函數的返回值保存
        print(time.time() - t)
        return res  # 將之前保存的值在這裏返回,作為裝飾器的返回值
    return sub_a
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
sleep = sub_b(sleep)
print(sleep(2))

現在返回值也有了。東西都全了,但是和別人的裝飾器還有一點點不一樣。也就差了兩句。別人的裝飾器大概這下面這個樣子的。

import time
def sub_b(func):
    def sub_a(*args,**kwargs):
        "計算參數的函數的運行時間"
        t = time.time()
        res = func(*args,**kwargs)
        print(time.time() - t)
        return res
    return sub_a
@sub_b  # 添加這一句
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
#sleep = sub_b(sleep)  # 去掉這一句
print(sleep(2))

這裏使用了@語句,並且放到了被裝飾函數前面。替代了原本在被裝飾函數後面的賦值語句。這兩句效果是一樣的。

但是@語句明顯代碼更加簡潔,並且位置是在被裝飾函數前,更加易於閱讀。

不過後面還會繼續使用賦值的形式,我覺得更好理解裝飾器的實現方式。

裝飾器實際也是一個函數,那麽函數就可以帶參數,通過參數,可以使函數有更多的選項。現在我們要給裝飾器也加上參數。

之前實現不改變調用方式的時候,我們為裝飾器套了一層。現在為了讓裝飾器能夠帶上參數,我們需要再套一層,具體原理也不是很明白,先看三層的代碼:

import time
def sub_c():  # 先不寫參數,將來參數可以填這裏了
    def sub_b(func):
        def sub_a(*args,**kwargs):
            "計算參數的函數的運行時間"
            t = time.time()
            res = func(*args,**kwargs)
            print(time.time() - t)
            return res
        return sub_a
    return sub_b
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
sleep = sub_c()(sleep)
print(sleep(2))

先看一下賦值的意義,sleep=sub_c()(sleep)。sub_c()的值就是他的返回值sub_b,也就是sub_b函數的內存地址。後面再跟上(sleep),就是執行sub_b(sleep)。也就是說sleep=sub_b(sleep)。如此剝去了最外面一層,就和上面兩層的賦值一樣了。

所以三層裝飾器用的時候,sub_c後面多了一個(),因為這裏我們要的是最外層函數的返回值。而二層裝飾器用的時候,後面是沒有()的,這裏要的是函數代碼塊的內存地址。

最後,我們就可以方便的為裝飾器帶上參數了。我們加上可以控制浮點小數位數的參數。

import time
def sub_c(n):
    def sub_b(func):
        def sub_a(*args,**kwargs):
            "計算參數的函數的運行時間"
            t = time.time()
            res = func(*args,**kwargs)
            print(round((time.time() - t),n))
            return res
        return sub_a
    return sub_b
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
sleep = sub_c(8)(sleep)
print(sleep(2))

最後還是要用@的形式

總結

最後是2個裝飾器最終的模板,按下面說的調整一下,就算不理解應該也能套用了。

  1. 將最外層的函數名改成你的裝飾器的名字,為你的裝飾器起一個合適的名字

  2. 內層的函數名無所謂

  3. 最內層的代碼塊做替換成你自己的代碼,res=func(*args,**kwargs)和最後的return res這兩句不用修改

兩層裝飾器模板:

import time
def run_time(func):  # 最外層的函數名字換成你裝飾器的名字
    def wrapper(*args,**kwargs):
        "計算參數的函數的運行時間"
        t = time.time()  # 這裏替換成你的代碼段
        res = func(*args,**kwargs)  # 這句不變
        print(time.time() - t)  # 這裏換成你的代碼段
        return res  # 這句不變
    return wrapper
@run_time
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
print(sleep(2))

三層裝飾器模板:

import time
def run_time(n):  # 最外層的函數名字換成你裝飾器的名字
    def decorator(func):  # 內層的函數名字無所謂
        def wrapper(*args,**kwargs):
            "計算參數的函數的運行時間"
            t = time.time()  # 這裏替換成你的代碼段
            res = func(*args,**kwargs)  # 這句不變
            print(round((time.time() - t),n))  # 這裏替換成你的代碼段
            return res  # 這句不變
        return wrapper
    return decorator
@run_time(8)
def sleep(n):
    "運行後等待n秒"
    print("wait for %d seconds"%n)
    time.sleep(n) 
    return "END"
print(sleep(2))


Python自動化開發學習4-裝飾器