Python基礎15-函數閉包與裝飾器

目錄

裝飾器概念

裝飾器的實現

修改被裝飾函數的代碼(非裝飾器實現)

修改被裝飾函數的調用方式(非裝飾器實現)

裝飾器的實現(不完整的實現level1)

裝飾器實現的語法糖(不完整的實現level2)

函數閉包加上返回值和參數(基本完整的裝飾器實現)

過渡內容

帶參數的裝飾器


裝飾器概念

裝飾器本質就是函數,功能是爲其他函數添加附加功能。裝飾器的原則如下

  1. 不修改被裝飾函數的源代碼
  2. 不修改被裝飾函數的調用方式

裝飾器的實現

要實現裝飾器,就是遵循裝飾器的原則來實現裝飾器,具體做法就是 裝飾器=高階函數+函數嵌套/閉包。

我們用一個例子來實現以下功能。有個函數foo,執行時間較長,我們在不修改foo函數的源代碼、不修改foo函數的調用方式的前提下,顯示foo函數執行的時間。這個需求就用裝飾器來實現,我們一點一點接近裝飾器的完整實現。

import time


def foo():
    """
    模擬foo函數運行了3秒左右
    :return: 
    """
    time.sleep(3)
    print('from foo')
    pass


# foo函數的調用方式就是直接調用
foo()

修改被裝飾函數的代碼(非裝飾器實現)

我們可以選擇在foo函數開始執行的時候記下時間,在foo結束執行的時候再記下時間,從而得到foo函數的執行時間。但是這樣就必須修改foo函數的源代碼,這在大型項目裏面很可能會引發連鎖反應,導致其他調用的地方出錯。

import time


def foo():
    """
    模擬foo函數運行了3秒左右
    :return:
    """
    start_time = time.time()
    time.sleep(3)
    print('from foo')
    stop_time = time.time()
    print("foo執行時間 %s" % (stop_time - start_time))
    pass


# foo函數的調用方式就是直接調用
foo()
# from foo
# foo執行時間 3.0006065368652344

修改被裝飾函數的調用方式(非裝飾器實現)

我們還可以保持foo函數不修改,將foo作爲參數傳給裝飾器函數timer,由timer來記錄foo的執行時間。這個樣保證了foo代碼不變,但是foo函數的調用方式發生了變化。

import time


def timer(func):
    start_time = time.time()
    func()
    stop_time = time.time()
    print("running time: %s" % (stop_time - start_time))
    return func


def foo():
    """
    模擬foo函數運行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


timer(foo)
# from foo
# running time: 3.013805627822876

裝飾器的實現(不完整的實現level1)

有沒有既不修改被裝飾函數的源代碼、又不修改被裝飾函數的調用方式,這樣兩全其美的實現呢?那就是裝飾器。裝飾器函數:將被裝飾函數作爲入參,這就需要定義高階函數;將附加了裝飾功能的函數作爲返回值返回,同時保證被裝飾函數在裝飾器裏面不被調用,這就需要函數嵌套/閉包。最後,要保證調用方式不變,那麼就將返回值賦給被裝飾函數。

具體到被裝飾函數foo和裝飾器timer的例子。就是foo要作爲timer的入參,timer裏面定義嵌套函數wrapper,wrapper被調用時foo和修飾功能都被執行,將wrapper作爲返回值返回,返回值付給foo。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        stop_time = time.time()
        print("運行時間 %s" % (stop_time - start_time))
        pass

    return wrapper


def foo():
    """
    模擬foo函數運行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


foo = timer(foo)
foo()
# from foo
# 運行時間 3.012805223464966

裝飾器實現的語法糖(不完整的實現level2)

上面的實現還有一個問題,那麼就是每次都要經過裝飾器返回值付給被修飾函數的過程。這個Python在語法成名予以解決。那就是把  @裝飾器  放在被裝飾函數定義的前一行,這樣就裝飾了。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        stop_time = time.time()
        print("運行時間 %s" % (stop_time - start_time))
        pass

    return wrapper


@timer
def foo():
    """
    模擬foo函數運行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


foo()
# from foo
# 運行時間 3.012805461883545

函數閉包加上返回值和參數(基本完整的裝飾器實現)

上面實現的裝飾器還不夠完整。如果被裝飾的函數還有入參和返回值,那麼上面的實現方式是不行的。我們來解決這個問題。

首先,如果被裝飾函數有入參,在執行裝飾器返回的嵌套函數裏面也要有相應的入參。爲了保證裝飾器適應性任意多個入參,這個嵌套函數應該有邊長的參數,因此wrapper的入參是*args,**kwargs。調用func的時候也應當相應的加參數*args,**kwargs。注意,這兩個*args,**kwargs的含義是不一樣的。wrapper的參數是函數定義,回憶函數基礎那篇博客變長參數的部分,這裏的args和kwargs分別是元組和字典。func調用的地方是函數調用的入參,是解壓序列,將args元組解壓序列成位置參數,將kwargs解壓序列成關鍵字參數。因此,在裝飾器實現的時候,都應保證wrapper和func的參數是*args,**kwargs。

其次,如果被裝飾函數有返回值,在執行裝飾器返回的嵌套函數裏面也要有相應的返回值。解決辦法就是將返回值記錄下來。在適當的地方返回。

import time


def timer(func):
    def wrapper(*args, **kwargs):  # 這裏的*args, **kwargs是函數定義,表示可邊長參數
        start_time = time.time()
        res = func(*args, **kwargs)  # 這裏的*args, **kwargs是函數調用的入參,將wrapper的參數解壓序列
        stop_time = time.time()
        print("運行時間 %s" % (stop_time - start_time))
        return res
        pass

    return wrapper


@timer
def foo(os, db, app):
    """
    模擬foo函數運行了3秒左右
    :return:
    """
    time.sleep(0.2)
    res = 'os->%s,db->%s,app->%s' % (os, db, app)
    print('from foo')
    return res
    pass


RES = foo('linux', db='mysql', app='apache')
print(RES)
# from foo
# 運行時間 0.20280051231384277
# os->linux,db->mysql,app->apache

過渡內容

這一部分爲後面做鋪墊,例子完全來自我所看視頻教程的內容。需求是,在首頁index、個人中心home和購物車shopping_car執行前進行身份驗證,不能修改這三個函數的調用方式。

def index():
    print('welcome to front page')
    pass


def home(name):
    print('welcome home%s' % name)
    pass


def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# welcome to front page
# welcome homekevin
# kevin's shopping car has [food,drink,toy]

用全局變量來模擬數據庫記錄和當前用戶session。代碼如下。

# 用戶列表,模擬數據庫存儲用戶信息
user_dic = [
    {'username': 'alice', 'password': '123'},
    {'username': 'kevin', 'password': '123'},
    {'username': 'bob', 'password': '123'},
    {'username': 'charlie', 'password': '123'},
]

# 當前用戶,模擬session
curr_user = {'username': None, 'login': False}


def auth_func(func):
    def auth_check(*args, **kwargs):
        # 如果有session,直接執行功能
        if curr_user['username'] and curr_user['login']:
            res = func(*args, **kwargs)
            return res
        # 否則驗證身份
        username = input('username:').strip()
        password = input('password:').strip()
        for user in user_dic:
            if username == user['username'] and password == user['password']:
                # 身份驗證成功,更新session,並執行功能
                curr_user['username'] = username
                curr_user['login'] = True
                res = func(*args, **kwargs)
                return res
            pass
        # 驗證不成功,報錯
        print('wrong username or password!')
        pass

    return auth_check


@auth_func
def index():
    print('welcome to front page')
    pass


@auth_func
def home(name):
    print('welcome home, %s' % name)
    pass


@auth_func
def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# username:kevin
# password:123
# welcome to front page
# welcome home, kevin
# kevin's shopping car has [food,drink,toy]

帶參數的裝飾器

接上面過渡內容,如果我們的驗證方式有不同,index用數據庫,home和shopping_car用其他的驗證方式,那怎麼辦呢?可以用帶有參數的裝飾器。我們在上面auth_func外再包一層auth,再調用裝飾器的時候,調用auth執行的結果。代碼如下。

# 用戶列表,模擬數據庫存儲用戶信息
user_dic = [
    {'username': 'alice', 'password': '123'},
    {'username': 'kevin', 'password': '123'},
    {'username': 'bob', 'password': '123'},
    {'username': 'charlie', 'password': '123'},
]

# 當前用戶,模擬session
curr_user = {'username': None, 'login': False}


def auth(auth_type='filedb'):
    def auth_func(func):
        def auth_check(*args, **kwargs):
            # 這裏通過auth_type可以做更多的選擇
            if auth_type == 'filedb':
                # 如果有session,直接執行功能
                if curr_user['username'] and curr_user['login']:
                    res = func(*args, **kwargs)
                    return res
                # 否則驗證身份
                username = input('username:').strip()
                password = input('password:').strip()
                for user in user_dic:
                    if username == user['username'] and password == user['password']:
                        # 身份驗證成功,更新session,並執行功能
                        curr_user['username'] = username
                        curr_user['login'] = True
                        res = func(*args, **kwargs)
                        return res
                    pass
                # 驗證不成功,報錯
                print('wrong username or password!')
            elif auth_type == 'ldap':
                print('ldap check')
                res = func(*args, **kwargs)
                return res
            else:
                print('what the fuck!')
            pass

        return auth_check

    # 將auth_func返回
    return auth_func


# @ auth調用的結果
# 也就相當於@auth_func
@auth()
def index():
    print('welcome to front page')
    pass


# @ auth調用的結果
# 也就相當於@auth_func
@auth(auth_type='ldap')
def home(name):
    print('welcome home, %s' % name)
    pass


# @ auth調用的結果
# 也就相當於@auth_func
@auth(auth_type='kerbers')
def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# username:kevin
# password:123
# welcome to front page
# ldap check
# welcome home, kevin
# what the fuck!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章