python的裝飾器

昀溪發表於2018-08-18

 

概要

  1. 什麼是裝飾器
  2. 怎麼使用裝飾器
  3. 使用場景
  4. 如何給裝飾器傳遞引數

1. 什麼是裝飾器

能夠對其他函式的功能進行增強。也就是函式程式碼功能重用。它是一個設計模式。需要注意的是:

  1. 裝飾器本身是一個函式
  2. 增強被裝飾函式的功能,同時被增強的函式還不能感受到自己被增強了或者說被修了
  3. 裝飾器需要接受一個函式物件作為引數以對其進行包裝
  4. 不會改變被呼叫函式的呼叫方式

2. 怎麼使用裝飾器

要理解裝飾器你需要理解如下內容:

  1. 函式即變數  def 定義一個函式就等於把函式體賦值給了一個變數(函式名)
  2. 高階函式 滿足兩個條件 把一個函式名當做形參傳入另外一個函式、返回值中包含函式名
  3. 巢狀函式 函式中巢狀函式

裝飾器=高階函式+函式巢狀

2.1 函式巢狀

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

def fun3():
    print("in the fun3")
    def fun4():
        print("in the fun4")
    fun4()


if __name__ == '__main__':
    fun3()

執行結果

在fun3中定義一個fun4函式,執行fun3,同時也會執行fun4。這個比較好理解就是函式中巢狀一個函式,對於巢狀的函式(這裡是fun4)你可以在你呼叫的函式(這裡是fun3)中執行,也可以把這個fun4返回。

2.2 高階函式

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

def fun1():
    print("Hello fun1")


def fun2(fun):
    print("World fun2")
    # 這裡是執行的傳入的函式本身
    fun()
    # 返回回去
    return fun

# 執行fun2同時把fun1當做引數傳遞進去
fun2(fun1)

上面滿足高階函式的2個條件。但是如果我只是想給fun1增加功能,那麼我這樣執行顯然成功了,可是我改變了呼叫方式,也就是說我必須執行fun2(fun1)才行,而我程式中很多地方寫的都是fun1(),那如果我這樣改起來自己麻煩同時如果別人調我的fun1(),那別人也要改。如何不改變呼叫方式呢?透過下面的方式呼叫,不過這不再是單純的給fun1增加功能,因為最後相當於重新定義了fun1。

# 如何不改變呼叫方式也增加新功能呢?就這樣。這裡相當於把之前的fun1重新定義了
fun1 = fun2(fun1)

2.3 裝飾器如何使用

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

import time

def timer(func):
    def wapper(*args, **kwargs):
        start_time = time.time()
        func()
        stop_time = time.time()
        print("耗時:", stop_time - start_time)
    return wapper

@timer
def run():
    print("To do job.")
    time.sleep(2)
    print("Job is done.")

run()

我這裡是給run函式增加一個功能計算該函式的執行時間。上面就是裝飾器的用法。增加了功能、沒有改變原來的呼叫方式,timer函式隨處可以引用。如果上面的功能透過高階函式如何實現呢?你看下面的程式碼幾乎完全相同,只是run()函式上面去掉了@timer,以及後面的執行語句前面有加了一個語句,正像上面所說執行完 run = timer(run) 其實就等於重新定了run()。

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

import time

def timer(func):
    def wapper(*args, **kwargs):
        start_time = time.time()
        func()
        stop_time = time.time()
        print("耗時:", stop_time - start_time)
    return wapper


def run():
    print("To do job.")
    time.sleep(2)
    print("Job is done.")

run = timer(run)
run()

加上 @timer 之後,執行 run()就等於執行 timer函式中的wapper。因為:如果不加 @timer 你可以這麼用,其實這麼用的效果和加 @timer 然後呼叫run()是一樣的。

加上 @timer 之後,執行過程是什麼樣的呢?

  1. 從上到下執行,遇到 def timer(func) 函式直譯器知道是定義了一個函式,因為此時沒有呼叫所以直譯器繼續向下執行
  2. 遇到@timer,直譯器執行 def timer(func) 並把 run() 函式傳遞給 def timer(func) 中的 func,然後發現定義了一個函式 def wapper,它此時也不執行,繼續向下,遇到return wapper 則返回wapper函式
  3. 此時裝飾完畢其實也就是對run()函式進行了重新賦值,繼續向下,遇到run()這條語句,也就是我們直接呼叫,這時候它會去執行之前返回的wapper函式,此時run()就是wapper()函式  理解為 run = timer(run)
  4. 執行wapper函式的第一條語句 start_time = time.time()
  5. 執行wapper函式的第二條語句 func() 而這時候的func()就是之前傳遞進來的run()函式,此時執行run()函式的語句,直到執行完成返回
  6. 執行執行wapper函式第三條語句 stop_time = time.time()
  7. 執行執行wapper函式第四條語句  print("耗時:", stop_time - start_time)

2.4 裝飾器如何傳遞引數

我們可能注意到 def wapper(*args, **kwargs) 其實這裡就可以傳遞引數。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


def printName(func):
    def wapper(*args, **kwargs):
        func(**kwargs)
    return wapper

@printName
def run(name):
    print("姓名為:", name)

def main():
    run(name="chen")

if __name__ == '__main__':
    main()

可能有些人對 **kwargs 有些迷惑,其實它就是一個字典形式,你傳進來的引數必須是 KEY=VALUE形式,就行我在main()函式中的那樣 run(name="chen"),如果還不明白我換一種寫法如下圖:

2.5 類中如何使用裝飾器

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""

"""

import sys
import time


def timer(tagName):
    import time

    def wapper(func):
        def aa(self, *args, **kwargs):
            start = time.time()
            func(self)
            end = time.time()
            consume = end - start
            if consume > 60:
                min, sec = divmod(consume, 60)
                print(" - %s 執行耗時:%s 分 %s 秒。" % (tagName, str(min), str(sec)))
            else:
                print(" - %s 執行耗時:%s 秒。" % (tagName, str(consume)))
        return aa

    return wapper


class AA:
    def __init__(self):
        pass

    @timer(tagName="runTask方法")
    def runTask(self):
        time.sleep(2)


if __name__ == "__main__":
    try:
        aa = AA()
        aa.runTask()
    except Exception as err:
        print(err)
    finally:
        sys.exit()

3. 例項

我這裡模擬一個場景就是網站登入,我們需要給所有頁面增加一個驗證功能。

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

"""
模擬網站登入,也就是給某些頁面加驗證功能
"""

users = {"Tom": "123"}


# 驗證功能
def auth(func):
    def wrapper(*args, **kwargs):
        username = input("Input your username: ")
        password = input("Input your password: ")
        if username.strip() in users and password.strip() == users[username]:
            func(username=username)
            # 如果你需要接收被裝飾函式的返回值就要
            # res = func(username=username)
        else:
            exit("Invalid username or password.")
    return wrapper


# 下面是模擬兩個頁面

def indexPage():
    print("Welcome to Index.")


# 給home頁面加驗證
@auth
def homePage(**kwargs):
    print("Welcome: ", kwargs["username"], "to Home.")
    return "You are Home."


def main():
    indexPage()
    homePage()


if __name__ == '__main__':
    main()

執行結果

現在的需求變了,我又增加了一個頁面,同時這個頁面需要另外一種驗證方式,怎麼辦?最笨的方法是再寫一個裝飾器用於其他驗證方式,有沒有更好的辦法呢?就是設定裝飾器引數。

#!/usr/bin/env python
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

"""
模擬網站登入,也就是給某些頁面加驗證功能
"""

users = {"Tom": "123"}

def authG_two(auth_type="local"):
    """
    這個裝飾器帶了引數,所以它就不能寫fun,而必須是明確的引數。而且這裡和之前比又多了一層,
    因為最外層不是傳遞的函式進來,而是引數。這個是如何執行的呢?

    @authG_two(auth_type="local")
    def fun1():
        pass

    fun1 = authG_two(auth_type="local") 然後返回 wrapper,這時候 fun1 = wrapper(fun1),然後在進入到  authG_two() 執行 wrapper()方法
    根據auth_type返回 對應的函式,如果是local 最終則是  fun1 = localAuth()並且 localAuth()裡的func函式就是之前傳遞進去的func函式。
    """
    def wrapper(func):
        # 本地驗證方式
        def localAuth(*args, **kwargs):
            username = input("Input your username: ")
            password = input("Input your password: ")
            if username.strip() in users and password.strip() == users[username]:
                print("Local authentication succeed.")
                func(username=username)
                # 如果你需要接收被裝飾函式的返回值就要
                # res = func(username=username)
            else:
                exit("Invalid username or password.")

        # LDAP驗證方式
        def ldapAuth(*args, **kwargs):
            username = input("Input your username: ")
            password = input("Input your password: ")
            func(username=username)
            print("LDAP authentication succeed.")

        if auth_type == "local":
            return localAuth
        else:
            return ldapAuth
    return wrapper


"""
下面三個模仿三個板塊的主頁
"""
def indexPage():
    print("Welcome to Index.")


@authG_two()
def homePage(**kwargs):
    """
    這裡使用新的裝飾器,它後面帶括號,不設定參數列示使用預設引數設定。
    """
    print("Welcome: ", kwargs["username"], "to Home.")
    return "You are Home."


# 新增BBS頁面
@authG_two(auth_type="ldap")
def bbsPage(**kwargs):
    """
    這裡使用新的裝飾器,它後面帶括號,這就說明那個裝飾器需要帶引數,驗證方式為LDAP
    """
    print("Welcome to BBS.")


def main():
    indexPage()
    homePage()
    bbsPage()


if __name__ == '__main__':
    main()

執行結果

 

相關文章