裝飾器詳解

裝飾器(Decorator)本質是函數,功能是爲其餘函數添加附加功能,定義一個裝飾器須要知足如下兩個原則:python

  • 不修改被修飾函數源代碼(開放封閉原則)
  • 不修改被修飾函數的調用方式

裝飾器 = 高階函數 + 函數嵌套 + 閉包數據庫

1. 高階函數

高階函數定義:閉包

  • 函數接收的參數是一個函數
  • 函數的返回值是一個函數

知足以上任意一個條件就是高階函數。app

def calc():
    print('calc 運行完畢~')
f = calc
f()

因爲函數也是對象,所以函數也能夠賦值給一個變量,調用這個變量就是調用函數。函數對象有一個 __name__屬性,用於查看函數的名字:框架

calc.__name__
f.__name__

'calc'
'calc'

需求:函數

在不修改 calc 源代碼的前提下,爲 calc函數添加一個代碼計時功能(計算 calc運行時間)。網站

1.1 把函數當作參數傳給另外一個函數

使用高階函數模擬裝飾器實現calc計時功能。設計

import time

def timmer(func):
    start_time = time.time()
    func()
    stop_time = time.time()
    print('函數 %s 運行時間爲:%s' % (func.__name__, stop_time - start_time))
    
def calc():
    time.sleep(2)
    print('calc 運行完畢~')

timmer(calc)
calc 運行完畢~
calc 運行時間爲:2.0011146068573

雖然爲 calc 增長了計時功能,但 calc 原來的執行方式是 calc(),而如今是調用高階函數timmer(calc),改變了調用方式。日誌

1.2 函數的返回值是函數

import time

def timmer(func):       # func: calc
    start_time = time.time()
    func()              # func(): calc()
    stop_time = time.time()
    print('函數 %s 運行時間爲:%s' % (func.__name__, stop_time - start_time))
    return func         # func: calc (<function calc at 0x00000000052D6D90>) 內存地址
    
def calc():
    time.sleep(2)
    print('calc 運行完畢~')

calc = timmer(calc)    # calc = timmer(calc): calc = <function calc at 0x00000000052D6D90>
calc()
calc 運行完畢~
calc 的運行時間是:2.0001144409179688
calc 運行完畢~

沒有改變 calc 的調用方式,但也沒爲其添加新功能。code

1.3 總結

使用高階函數實現裝飾器功能 :

  • 函數接收的參數是一個函數名
    • 做用:在不修改函數源代碼的前提下,爲函數添加新功能。
    • 不足:會改變函數的調用方式
  • 函數返回值是一個函數名
    • 做用:不修改函數調用方式
    • 不足:不能添加新功能

2. 函數嵌套

函數中嵌套另外一個函數

def father(name):
    print('I am %s' % name)
    def son():
        print('My father is %s' % name)
        def grandson():
            print('My grandfather is %s' % name)
        grandson()
    son()
father('tom')
I am tom
My father is tom
My grandfather is tom

3. 閉包

閉包也是函數嵌套,若是在一個內部函數裏調用外部做用域(不是全局做用域)的變量,那麼這個內部函數就是閉包(closure)

def father(name):
    print('I am %s' % name)
    def son():
        name = 'john'
        def grandson():
            print('My father is %s' % name)
        grandson()
    son()
father('tom')
I am tom
My father is john

內部函數 grandson 調用了它的外部函數 son 的變量 name='john',那麼 grandson就是一個閉包。

4. 無參數裝飾器

無參數裝飾器 = 高階函數 + 函數嵌套

4.1 基本框架

# 實現一個裝飾器的基本框架
def timmer(func):
    def wrapper():
        func()
    return wrapper

4.2 加上參數

def timmer(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
    return wrapper

4.3 加上功能

import time

def timmer(func):
    def wrapper(*args, **kwargs):
        """計時功能"""
        start_time = time.time()
        func(*args, **kwargs)
        stop_time = time.time()
        print('函數 %s 運行時間:%s' % (func, stop_time - start_time))
    return wrapper

4.4 加上返回值

import time

def timmer(func):
    def wrapper(*args, **kargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('函數 %s 運行時間:%s' % (func, stop_time - start_time))
        return res
    return wrapper

4.5 使用裝飾器

def calc():
    time.sleep(2)
    print('calc 運行完畢~')
    return 'calc 返回值'
calc = timmer(calc)
calc()

4.6 語法糖 @

@timmer        # 至關於 calc = timmer(calc)
def calc():
    time.sleep(2)
    print('calc 運行完畢~')
calc()

4.7 示例

使用無參數裝飾器,爲 calc添加計時功能(統計 calc代碼運行時間)

import timmer

def timmer(func):               # func: calc
    """裝飾器函數"""
    def wrapper(*args, **kwargs):       # args:<class 'tuple'>:('rose', 18, 'female')
        start_time = time.time()        # kwargs={}
        res = func(*args, **kwargs)     # res: 'calc 返回值'
        stop_time = time.time()
        print('函數 %s 運行時間:%s' % (func.__name__, stop_time - start_time))
        return res       # func: calc (<function calc at 0x00000000052D6D90>) 內存地址
    return wrapper

@timmer                 # @timmer: calc=timmer(calc)  ——>calc = <function wrapper at 0x00000000052D6D90>
def calc(name, age, gender):
    """被修飾函數"""
    time.sleep(2)
    print('calc 運行完畢~')
    print('名字:%s,年齡:%d,性別:%s' % (name, age, gender))
    return 'calc 返回值'

s = calc('rose', 18, 'female')      # 至關於執行 s = wrapper('rose', 18, 'female')
print(s)
calc 運行完畢
名字:rose,年齡:18,性別:female
函數 calc 運行時間:2.0001144409179688
calc 返回值

因爲 timmer() 是一個裝飾器函數,返回一個函數 wrapper。因此 calc()函數仍然存在,只是如今同名的 calc 變量指向了新的函數,因而調用 calc() 執行的是wrapper()函數。

5. 有參數裝飾器

有參數裝飾器 = 高階函數 + 函數嵌套 + 閉包

若是裝飾器本山須要傳入參數,就須要再編寫一個 decorator的高階函數。好比給 calc函數添加一個日誌功能,可以打印日誌,其基本框架以下:

def log(text):
    def timmer(func):
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
        return wrapper
    return timmer

@log('文本內容')
def calc():
    pass

示例:

import timmer

def log(text):          # text: '文本內容'
    def timmer(func):               # func: calc
        """裝飾器函數"""
        def wrapper(*args, **kwargs):   # args:<class 'tuple'>:('rose', 18, 'female')
            start_time = time.time()        # kwargs={}
            res = func(*args, **kwargs)     # res: 'calc 返回值'
            stop_time = time.time()
            print('函數 %s 運行時間:%s' % (func.__name__, stop_time - start_time))
            return res     # func: calc (<function calc at 0x00000000052D6D90>) 內存地址
        return wrapper
    return timmer

@log('文本內容')     # 至關於 calc = log('自定義文本')(calc)   ——> timmer(calc)  ——> calc=wrapper
def calc(name, age, gender):
    """被修飾函數"""
    time.sleep(2)
    print('calc 運行完畢~')
    print('名字:%s,年齡:%d,性別:%s' % (name, age, gender))
    return 'calc 返回值'

s = calc('rose', 18, 'female')      # 至關於執行 s = wrapper('rose', 18, 'female')
print(s)

與兩層嵌套效果的decorator相比,三層效果是這樣的:

calc = log('自定義文本')(calc)       # 即 calc = wrapper

首先執行 log('自定義文本'),返回timmer函數,再調用返回參數(timmer(calc)),參數是 calc,返回值是wrapper函數,最後再調用wrapper()

6. 對象屬性

函數也是對象,也有__name__屬性(返回函數名)。但通過裝飾的calc函數,它的__name__

從原來的calc變成了wrapper

>>> calc.__name__
'wrapper'

有些須要依賴函數簽名的代碼由於__name__改變,而出現某些錯誤,因此須要將原calc__name__屬性複製到wrapper()函數中。

import functools
....
@functools.wraps(func)
def wrapper(*args, **kwargs)
...

7. 實例

實例 1

請設計一個decorator,它可做用於任何函數上,並打印該函數的執行時間:

import time
import functools


def metric(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = fn(*args, **kwargs)
        stop_time =time.time()
        print('%s executed in %s ms' % (fn.__name__, stop_time - start_time))
        return res
    return wrapper


@metric         # fast=metric(calc)
def fast(x, y):
    time.sleep(0.0012)
    return x + y


@metric
def slow(x, y, z):
    time.sleep(0.1234)
    return x * y * z

f = fast(11, 22)
s = slow(11, 22, 33)
print(f, s)
fast executed in 0.0019998550415039062 ms
slow executed in 0.1240072250366211 ms
33 7986

實例 2

實現一個購物網站基本功能,其功能以下:

  • 提示用戶輸入用戶名和密碼
  • 用戶我的界面和購物車不須要登陸(保持會話)
import time
import functools

# 模擬存儲用戶名、密碼數據庫
user_list=[
    {'name':'alex','passwd':'123'},
    {'name':'linhaifeng','passwd':'123'},
    {'name':'wupeiqi','passwd':'123'},
    {'name':'yuanhao','passwd':'123'},
]

current_dic = {'username': None, 'login': False}   # 用於保存登陸記錄,None,False 爲沒有登陸

def auth_func(func):
    """裝飾器函數"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 僅供 home、shopping 調用(由於不須要再輸入用戶名和密碼)
        if current_dic['username'] and current_dic['login']:
            res = func(*args, **kwargs)
            return res
        
        username = input('用戶名:').strip()
        passwd = input('密碼:').strip()
        # 遍歷循環用戶名、密碼數據庫,比對用戶輸入的用戶名和密碼
        for user_dic in user_list:
            if username == user_dic['name'] and passwd == user_dic['passwd']:
                # 用戶保持會話,即保持用戶登陸狀態,賦值
                current_dic['username'] = username
                current_dic['login'] = True
                res = func(*args, **kwargs)
                return res
        else:
            print('用戶名或密碼錯誤')
    return wrapper

@auth_func
def index():
    """主頁(須要登陸)"""
    print('歡迎來到xxx主頁!')
    
@auth_func    
def home(name):
    """用戶登陸成功後的界面"""
    print('歡迎回家 %s' % name)

@auth_func
def shopping_car(name):
    """購物車"""
    print('%s購物車裏有:[%s,%s,%s]' % (name, '遊艇', '車子', '飛機'))
    
print('before-->',current_dic) 
index()
print('after-->',current_dic)
home('rose')
shopping_car('rose')
before--> {'username': None, 'login': False}
用戶名:alex
密碼:123
歡迎來到京東主頁!
after--> {'username': 'alex', 'login': True}
歡迎回家 rose
rose購物車裏有:[遊艇,車子,飛機]

帶參數裝飾器(須要認證類型):

import time
import functools

user_list=[
    {'name':'alex','passwd':'123'},
    {'name':'linhaifeng','passwd':'123'},
    {'name':'wupeiqi','passwd':'123'},
    {'name':'yuanhao','passwd':'123'},
]
current_dic={'username':None,'login':False}

def auth(auth_type='filedb'):
    def auth_func(func):
        @funtools.wraps(func)
        def wrapper(*args,**kwargs):
            print('認證類型是',auth_type)
            if auth_type == 'filedb':
                if current_dic['username'] and current_dic['login']:
                    res = func(*args, **kwargs)
                    return res
                
                username=input('用戶名:').strip()
                passwd=input('密碼:').strip()
                for user_dic in user_list:
                    if username == user_dic['name'] and passwd == user_dic['passwd']:
                        current_dic['username']=username
                        current_dic['login']=True
                        res = func(*args, **kwargs)
                        return res
                else:
                    print('用戶名或者密碼錯誤')
            elif auth_type == 'ldap':
                print('ldap 認證類型')
                res = func(*args, **kwargs)
                return res
            else:
                print('不知道什麼認證方式')
                res = func(*args, **kwargs)
                return res

        return wrapper
    return auth_func

# 至關於 auth_func = auth(auth_type='filedb')(auth_func)
@auth(auth_type='filedb') 
def index():
    print('歡迎來到xxx主頁')

@auth(auth_type='ldap')
def home(name):
    print('歡迎回家%s' %name)
#
@auth(auth_type='sssssss')
def shopping_car(name):
    print('%s的購物車裏有[%s,%s,%s]' %(name,'奶茶','妹妹','娃娃'))

# print('before-->',current_dic)
# index()
# print('after--->',current_dic)
# home('產品經理')
shopping_car('產品經理')
相關文章
相關標籤/搜索