1. 程式人生 > 實用技巧 >Python快速入門神器 python 裝飾器

Python快速入門神器 python 裝飾器

https://zhuanlan.zhihu.com/c_1189883314197168128

45、ORM框架SQLAlchemy

https://zhuanlan.zhihu.com/p/120953101

43、資料備份、pymysql模組

https://zhuanlan.zhihu.com/p/115504709

15、裝飾器

程式設計師Egon老溼 目錄:
  • 一 裝飾器介紹
    • 1.1 為何要用裝飾器
    • 1.2 什麼是裝飾器
  • 二 裝飾器的實現
    • 2.1 無參裝飾器的實現
    • 2.2 有參裝飾器的實現
  • 視訊連結

一 裝飾器介紹

1.1 為何要用裝飾器


軟體的設計應該遵循開放封閉原則,即對擴充套件是開放的,而對修改是封閉的。對擴充套件開放,意味著有新的需求或變化時,可以對現有程式碼進行擴充套件,以適應新的情況。對修改封閉,意味著物件一旦設計完成,就可以獨立完成其工作,而不要對其進行修改。

軟體包含的所有功能的原始碼以及呼叫方式,都應該避免修改,否則一旦改錯,則極有可能產生連鎖反應,最終導致程式崩潰,而對於上線後的軟體,新需求或者變化又層出不窮,我們必須為程式提供擴充套件的可能性,這就用到了裝飾器。

1.2 什麼是裝飾器


’裝飾’代指為被裝飾物件新增新的功能,’器’代指器具/工具,裝飾器與被裝飾的物件均可以是任意可呼叫物件。概括地講,裝飾器的作用就是在不修改被裝飾物件原始碼和呼叫方式的前提下為被裝飾物件新增額外的功能。裝飾器經常用於有切面需求的場景,比如:插入日誌、效能測試、事務處理、快取、許可權校驗等應用場景,裝飾器是解決這類問題的絕佳設計,有了裝飾器,就可以抽離出大量與函式功能本身無關的雷同程式碼並繼續重用。

提示:可呼叫物件有函式,方法或者類,此處我們單以本章主題函式為例,來介紹函式裝飾器,並且被裝飾的物件也是函式。

二 裝飾器的實現

函式裝飾器分為:無參裝飾器和有參裝飾兩種,二者的實現原理一樣,都是’函式巢狀+閉包+函式物件’的組合使用的產物。


2.1 無參裝飾器的實現

如果想為下述函式新增統計其執行時間的功能

import time

def index():
    time.sleep(3)
    print('Welcome to the index page’)
    return 200

index() #函式執行

遵循不修改被裝飾物件原始碼的原則,我們想到的解決方法可能是這樣

start_time=time.time()
index() #函式執行
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))

考慮到還有可能要統計其他函式的執行時間,於是我們將其做成一個單獨的工具,函式體需要外部傳入被裝飾的函式從而進行呼叫,我們可以使用引數的形式傳入

def wrapper(func): # 通過引數接收外部的值
    start_time=time.time()
    res=func()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    return res

但之後函式的呼叫方式都需要統一改成

wrapper(index)
wrapper(其他函式)

這便違反了不能修改被裝飾物件呼叫方式的原則,於是我們換一種為函式體傳值的方式,即將值包給函式,如下

def timer(func):
    def wrapper(): # 引用外部作用域的變數func
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

這樣我們便可以在不修改被裝飾函式原始碼和呼叫方式的前提下為其加上統計時間的功能,只不過需要事先執行一次timer將被裝飾的函式傳入,返回一個閉包函式wrapper重新賦值給變數名 /函式名index,如下

index=timer(index)  #得到index=wrapper,wrapper攜帶對外作用域的引用:func=原始的index
index() # 執行的是wrapper(),在wrapper的函式體內再執行最原始的index

至此我們便實現了一個無參裝飾器timer,可以在不修改被裝飾物件index原始碼和呼叫方式的前提下為其加上新功能。但我們忽略了若被裝飾的函式是一個有參函式,便會丟擲異常

def home(name):
    time.sleep(5)
    print('Welcome to the home page',name)

home=timer(home)
home('egon')
#丟擲異常
TypeError: wrapper() takes 0 positional arguments but 1 was given

之所以會丟擲異常,是因為home(‘egon’)呼叫的其實是wrapper(‘egon’),而函式wrapper沒有引數。wrapper函式接收的引數其實是給最原始的func用的,為了能滿足被裝飾函式引數的所有情況,便用上args+*kwargs組合(見4.3小節),於是修正裝飾器timer如下

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

此時我們就可以用timer來裝飾帶引數或不帶引數的函數了,但是為了簡潔而優雅地使用裝飾器,Python提供了專門的裝飾器語法來取代index=timer(index)的形式,需要在被裝飾物件的正上方單獨一行新增@timer,當直譯器解釋到@timer時就會呼叫timer函式,且把它正下方的函式名當做實參傳入,然後將返回的結果重新賦值給原函式名

@timer # index=timer(index)
def index():
    time.sleep(3)
    print('Welcome to the index page')
    return 200
@timer # index=timer(home)
          def home(name):
    time.sleep(5)
    print('Welcome to the home page’,name)

如果我們有多個裝飾器,可以疊加多個

@deco3
@deco2
@deco1
def index():
    pass

疊加多個裝飾器也無特殊之處,上述程式碼語義如下:

index=deco3(deco2(deco1(index)))

2.2 有參裝飾器的實現

瞭解無參裝飾器的實現原理後,我們可以再實現一個用來為被裝飾物件新增認證功能的裝飾器,實現的基本形式如下

def deco(func):
    def wrapper(*args,**kwargs):
        編寫基於檔案的認證,認證通過則執行res=func(*args,**kwargs),並返回res
    return wrapper

如果我們想提供多種不同的認證方式以供選擇,單從wrapper函式的實現角度改寫如下

def deco(func):
        def wrapper(*args,**kwargs):
            if driver == 'file':
                編寫基於檔案的認證,認證通過則執行res=func(*args,**kwargs),並返回res
            elif driver == 'mysql':
                編寫基於mysql認證,認證通過則執行res=func(*args,**kwargs),並返回res
        return wrapper

函式wrapper需要一個driver引數,而函式deco與wrapper的引數都有其特定的功能,不能用來接受其他類別的引數,可以在deco的外部再包一層函式auth,用來專門接受額外的引數,這樣便保證了在auth函式內無論多少層都可以引用到

def auth(driver):
    def deco(func):
        ……
    return deco

此時我們就實現了一個有參裝飾器,使用方式如下

先呼叫auth_type(driver='file'),得到@decodeco是一個閉包函式包含了對外部作用域名字driver的引用@deco的語法意義與無參裝飾器一樣
@auth(driver='file') 
def index():     
    pass
@auth(driver='mysql') 
def home():
    pass  

可以使用help(函式名)來檢視函式的文件註釋,本質就是檢視函式的doc屬性,但對於被裝飾之後的函式,檢視文件註釋

@timer
def home(name):
    '''
    home page function
    :param name: str
    :return: None
    '''
    time.sleep(5)
    print('Welcome to the home page',name)

print(help(home))
'''
列印結果:

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

None

在被裝飾之後home=wrapper,檢視home.name也可以發現home的函式名確實是wrapper,想要保留原函式的文件和函式名屬性,需要修正裝飾器

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    wrapper.__doc__=func.__doc__
    wrapper.__name__=func.__name__
    return wrapper

按照上述方式來實現保留原函式屬性過於麻煩,functools模組下提供一個裝飾器wraps專門用來幫我們實現這件事,用法如下

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

視訊連結:

簡單裝飾器的實現

https://www.bilibili.com/video/av73342471?p=47​www.bilibili.com

裝飾器修訂

https://www.bilibili.com/video/av73342471?p=48​www.bilibili.com

wraps補充

python快速入門(一)_嗶哩嗶哩 (゜-゜)つロ 乾杯~-bilibili​www.bilibili.com