怎麼樣去理解 Python 中的裝飾器

Zheaoli發表於2017-12-13

怎麼樣去理解 Python 中的裝飾器

首先,本垃圾文件工程師又來了。開始日常的水文寫作。起因是看到這個問題如何理解Python裝飾器?,正好不久前給人講過這些,本垃圾於是又開始新的一輪辣雞文章寫作行為了。

預備知識

首先要理解裝飾器,首先要先理解在 Python 中很重要的一個概念就是:“函式是 First Class Member” 。這句話再翻譯一下,函式是一種特殊型別的變數,可以和其餘變數一樣,作為引數傳遞給函式,也可以作為返回值返回。


def abc():
    print("abc")

def abc1(func):
    func()

abc1(abc)
複製程式碼

這段程式碼的輸出就是我們在函式 abc 中輸出的 abc 字串。過程很簡單,我們將函式 abc 作為一個引數傳遞給 abc1 ,然後,在 abc1 中呼叫傳入的函式

再來看一段程式碼


def abc1():
    def abc():
        print("abc")
    return abc
abc1()()

複製程式碼

這段程式碼輸出和之前的一樣,這裡我們將在 abc1 內部定義的函式 abc 作為一個變數返回,然後我們在呼叫 abc1 獲取到返回值後,繼續呼叫返回的函式。

好了,我們再來做一個思考題,實現一個函式 add ,達到 add(m)(n) 等價於 m+n 的效果。這題如果把之前的 First-Class Member 這一概念理清楚後,我們便能很清楚的寫出來了

def add(m):
    def temp(n):
        return m+n
    return temp
print(add(1)(2))
複製程式碼

嗯,這裡輸出就是 3 。

正文

看了前面的預備知識後,我們便可以開始今天的主題了

先來看一個需求吧

現在我們有一個函式


def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
複製程式碼

現在我們要給這個函式加上一些程式碼,來計算這個函式的執行時間。

我們大概一想,寫出了這樣的程式碼

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
複製程式碼

先且不論,這樣計算時間是不是準確的,現在我們要給如下很多函式加上一個時間計算的功能

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop1(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop2(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
複製程式碼

我們初略一想,嗯,Ctrl+C,Ctrl+V。emmmm 好了,現在你們不覺得這段程式碼特別髒麼?我們想讓他變得乾淨點怎麼辦?

我們想了想,按照之前說的 First-Class Member 的概念。然後寫出瞭如下的程式碼

import time
def time_count(func,a,b):
    time_flag=time.time()
    temp_result=func(a,b)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)
複製程式碼

嗯,看起來像那麼回事,好了好了,我們現在新的問題又來了,我們現在是假設,我們所有函式都只有兩個引數傳入,那麼現在如果想支援任意引數的傳入怎麼辦?我們眉頭一皺,寫下了如下的程式碼


import time
def time_count(func,*args,**kwargs):
    time_flag=time.time()
    temp_result=func(*args,**kwargs)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

複製程式碼

好了,現在看起來,有點像模像樣了,但是我們再想想,這段程式碼實際上改變了我們的函式呼叫方式,比如我們直接執行 range_loop(a,b) 還是沒有辦法獲取到函式執行時間。那麼現在我們如果不想改變函式的呼叫方式,又想獲取到函式的執行時間怎麼辦?

很簡單嘛,替換一下不就好了


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop=time_count(range_loop)
range_loop1=time_count(range_loop1)
range_loop2=time_count(range_loop2)
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)
複製程式碼

emmmm,這樣看起來感覺舒服多了?既沒有改變原有的執行方式,也輸出了函式執行時間。

但是。。。你們不覺得手動替換太噁心了麼???喵喵喵???還有什麼可以簡化下的麼??

好了,Python 知道我們是愛吃糖的孩子,給我們提供了一個新的語法糖,這也是今天的男一號,Decorator 裝飾器

說說 Decorator

我們前面已經實現了,在不改變函式特性的情況下,給原有的程式碼新增一點功能,但是我們也覺得這樣手動的替換,太噁心了,是的 Python 官方也覺得這樣很噁心,所以新的語法糖來了

我們上面的程式碼可以寫成這樣了


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
@time_count    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)

複製程式碼

哇,寫到這裡,你是不是恍然大悟!まさか???是的,其實 @ 符號其實是一個語法糖,他將我們之前的手動替換的過程交給了環境執行。好了用人話描述下,@ 的作用是將被包裹的函式作為一個變數傳遞給裝飾函式/類,將裝飾函式/類返回的值替換原本的函式。

@decorator
def abc():
    pass
複製程式碼

如同前面所講的一樣,實際上是發生了一個特殊的替換過程 abc=decorator(abc) ,好了我們來做幾個題來練習下吧?


def decorator(func):
    return 1
@decorator
def abc():
    pass
abc()
複製程式碼

這段程式碼會發生什麼?答:會丟擲異常。為啥啊?答:因為裝飾的時候發生了替換,abc=decorator(abc) ,替換後 abc 的值為 1 。整數預設不能作為一個函式進行呼叫。


def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap

def decorator(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

def decorator1(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

@time_count
@decorator
@decorator1    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
複製程式碼

這段程式碼怎麼替換的?答:time_count(decorator(decorator1(range_loop)))

嗯,現在是不是對裝飾器什麼的有了基本的瞭解?

擴充套件一下

現在,我想修改下前面寫的 time_count 函式,讓他支援傳入一個 flag 引數,當 flagTrue 的時候,輸出函式執行時間,為 False 的時候不輸出時間

我們一步步來,我們先假設新的函式叫做 time_count_plus

我們想實現的效果是這樣的

@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
複製程式碼

嗯,我們看了下,首先我們呼叫了 time_count_plus(flag=True) 一次,將它返回的值作為一個裝飾函式來替換 range_loop ,OK 那麼我們首先 time_count_plus 要接收一個引數,返回一個函式對吧

def time_count_plus(flag=True):
    def wrap1(func):
        pass
    return wrap1
複製程式碼

好了,現在返回了一個函式來作為裝飾函式,然後我們說了 @ 其實觸發了一次替換過程,好那麼我們現在的替換是不是 range_loop=time_count_plus(flag=True)(range_loop) 好了,現在大家應該很清楚了,我們在 wrap1 裡面是不是還應該有一個函式並返回?

嗯,最終的程式碼如下

def time_count_plus(flag=True):
    def wrap1(func):
        def wrap2(*args,**kwargs):
            if flag:
                time_flag=time.time()
                temp_result=func(*args,**kwargs)
                print(time.time()-time_flag)
            else:
                temp_result=func(*args,**kwargs)
            return temp_result
        return wrap2
    return wrap1
@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

複製程式碼

是不是這樣就清楚多啦!

擴充套件兩下

好了,我們現在有新的需求來了

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b

複製程式碼

現在我們有字串 a , a 的值可能為 +-*/ 那麼現在,我們想根據 a 的值來呼叫對應的函式怎麼辦?

我們煎蛋一想,嗯,邏輯判斷嘛


m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
a=input('請輸入 + - * / 中的任意一個\n')
if a=='+':
    print(add(m,n))
elif a=='-':
    print(sub(m-n))
elif a=='*':
    print(mul(m,n))
elif a=='/':
    print(div(m,n))
複製程式碼

但是這段程式碼,if else 是不是太多了點?我們仔細一想,用一下 First-Class Member 的特性,然後配合 dict 實現操作符和函式之間的關聯。

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
func_dict={"+":add,"-":sub,"*":mul,"/":div}
a=input('請輸入 + - * / 中的任意一個\n')
func_dict[a](m,n)
複製程式碼

emmmm,看起來不錯啊,但是我們註冊的過程能不能再簡化一點? 嗯,這個時候裝飾器語法特性就能用上了

m=3
n=2
func_dict={}
def register(operator):
    def wrap(func):
        func_dict[operator]=func
        return func
    return wrap
@register(operator="+")
def add(a,b):
    return a+b
@register(operator="-")
def sub(a,b):
    return a-b
@register(operator="*")
def mul(a,b):
    return a*b
@register(operator="/")
def div(a,b):
    return a/b

a=input('請輸入 + - * / 中的任意一個\n')
func_dict[a](m,n)
複製程式碼

嗯,還記得我們前面說的使用 @ 語法的時候,實際上是觸發了一個替換的過程麼?這裡就是利用這一特性,在裝飾器觸發的時候,註冊函式對映,這樣我們直接根據 'a' 的值來獲取函式處理資料。另外請注意一點,我們這裡沒有必要修改原函式,所以我們沒有必要寫第三層的函式。

如果有熟悉 Flask 同學就知道,在呼叫 route 方法註冊路由的時候,也是使用了這一特性 ,可以參考另外一篇很久前寫的垃圾水文 菜鳥閱讀 Flask 原始碼系列(1):Flask的router初探

總結

其實全文下來,大家應該能知道這樣一點東西。Python 中的裝飾器其實是 First-Class Member 概念的更進一層應用,我們將函式傳遞給其餘函式,包裹上新的功能後再行返回。@ 其實只是將這樣一個過程進行了簡化而已。在 Python 中,裝飾器無處不在,很多官方庫中的實現也依賴於裝飾器,比如很久之前寫過這樣一篇垃圾水文 Python 描述符入門指北

嗯,今天就先寫到這裡吧!

相關文章