Python 簡明教程 — 22,Python 閉包與裝飾器

微信公眾號:碼農充電站pro
個人主頁://codeshellme.github.io

當你選擇了一種語言,意味著你還選擇了一組技術、一個社區。

目錄

在這裡插入圖片描述

本節我們來介紹閉包裝飾器

閉包與裝飾器是函數的高級用法,其實在介紹完Python 函數我們就可以介紹本節的內容,但由於Python中的也可以用來實現裝飾器,所以我們等到介紹完了Python 類再來統一介紹閉包與裝飾器。

裝飾器使用的是閉包的特性,我們先來介紹閉包,再來介紹裝飾器。

1,什麼是閉包

Python 的函數內部還允許嵌套函數,也就是一個函數中還定義了另一個函數。如下:

def fun_1():

    def fun_2():
        return 'hello'

    s = fun_2()

    return s

s = fun_1()
print(s)    # 'hello'

在上面的程式碼中,我們在函數fun_1 的內部又定義了一個函數fun_2,這就是函數嵌套

我們在學習函數的時候,還知道,Python 函數可以作為函數參數函數返回值

因此,我們可以將上面程式碼中的函數fun_2作為函數fun_1的返回值,如下:

def fun_1():
    def fun_2():
        return 'hello'

    return fun_2

此時,函數fun_1 返回了一個函數,我們這樣使用fun_1

fun = fun_1()    # fun 是一個函數
s = fun()        # 調用函數 fun
print(s)         # s 就是 'hello'

我們再來改進函數fun_1,如下:

def fun_1(s):
    s1 = 'hello ' + s

    def fun_2():
        return s1 

    return fun_2

上面的程式碼中,內部函數fun_2返回了變數s1,而s1是函數fun_2外部變數,這種內部函數能夠使用外部變數,並且內部函數作為外部函數返回值,就是閉包

編寫閉包時都有一定的套路,也就是,閉包需要有一個外部函數包含一個內部函數,並且外部函數的返回值是內部函數

2,用閉包實現一個計數器

我們來實現一個計數器的功能,先寫一個框架,如下:

def counter():

    # 定義內部函數
    def add_one():
        pass
    
    # 返回內部函數
    return add_one

再來實現計數的功能,如下:

def counter():
    # 用於計數
    l = [0]
    
    # 定義內部函數
    def add_one():
        l[0] += 1
        return l[0] # 返回數字
    
    # 返回內部函數
    return add_one

上面的程式碼中,我們使用了一個列表l[0]來記錄累加數,在內部函數add_one中對l[0]進行累加。

我們這樣使用這個計數器:

c = counter()

print(c())   # 1
print(c())   # 2
print(c())   # 3

我們還可以使這個計數器能夠設置累加的初始值,就是為counter 函數設置一個參數,如下:

def counter(start):
    l = [start]

    def add_one():
        l[0] += 1
        return l[0] 
        
    return add_one

這樣我們就可以使用counter 來生成不同的累加器(從不同的初始值開始累加)。我們這樣使用該計數器:

c1 = counter(1)   # c1 從 1 開始累加
print(c1())       # 2
print(c1())       # 3
print(c1())       # 4

c5 = counter(5)   # c5 從 5 開始累加
print(c5())       # 6
print(c5())       # 7
print(c5())       # 8

c11 開始累加,c55 開始累加,兩個互不干擾。

3,什麼是裝飾器

裝飾器閉包的一種進階應用。裝飾器從字面上理解就是用來裝飾包裝的。裝飾器一般用來在不修改函數內部程式碼的情況下,為一個函數添加額外的新功能。

裝飾器雖然功能強大,但也不是萬能的,它也有自己適用場景:

  • 快取
  • 身份認證
  • 記錄函數運行時間
  • 輸入的合理性判斷

比如,我們有一個函數,如下:

def hello():
    print('hello world.')

如果我們想計算這個函數的運行時間,最直接的想法就是修改該函數,如下:

import time

def hello():
    s = time.time()

    print('hello world.')

    e = time.time()
    print('fun:%s time used:%s' % (hello.__name__. e - s))

# 調用函數
hello()

其中,time 模組是Python 中的內置模組,用於時間相關計算。

每個函數都有一個__name__ 屬性,其值為函數的名字。不管我們是直接查看一個函數的__name__ 屬性,還是將一個函數賦值給一個變數後,再查看這個變數的__name__ 屬性,它們的值都是一樣的(都是原來函數的名字):

print(hello.__name__)  # hello
f = hello              # 調用 f() 和 hello() 的效果是一樣的
print(f.__name__)      # hello

但是,如果我們要為很多的函數添加這樣的功能,要是都使用這種辦法,那會相當的麻煩,這時候使用裝飾器就非常的合適。

最簡單的裝飾器

裝飾器應用的就是閉包的特性,所以編寫裝飾器的套路與閉包是一樣的,就是有一個外部函數和一個內部函數,外部函數的返回值是內部函數。

我們先編寫一個框架:

def timer(func):
    def wrapper():
        pass
      
    return wrapper

再來實現計時功能:

import time

def timer(func):
    def wrapper():
        s = time.time()
        ret = func()
        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

def hello():
    print('hello world.')

該裝飾器的名字是timer,其接受一個函數類型的參數funcfunc 就是要修飾的函數。

func 的函數原型要與內部函數wrapper 的原型一致(這是固定的寫法),即函數參數相同,函數返回值也相同。

英文 wrapper 就是裝飾的意思。

其實timer 就是一個高階函數,其參數是一個函數類型,返回值也是一個函數。我們可以這樣使用timer 裝飾器:

hello = timer(hello)
hello()

以上程式碼中,hello 函數作為參數傳遞給了timer 裝飾器,返回結果用hello 變數接收,最後調用hello()。這就是裝飾器的原本用法。

只不過,Python 提供了一種語法糖,使得裝飾器的使用方法更加簡單優雅。如下:

@timer
def hello():
    print('hello world.')

hello()

直接在原函數hello 的上方寫一個語法糖@timer,其實這個作用就相當於hello = timer(hello)

用類實現裝飾器

在上面的程式碼中,是用函數(也就是timer 函數)來實現的裝飾器,我們也可以用來實現裝飾器。

實現裝飾器,主要依賴的是__init__ 方法和__call__ 方法。

我們知道,實現__call__ 方法的類,其對象可以像函數一樣被調用。

用類來實現timer 裝飾器,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self):
        s = time.time()
        ret = self.func()
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello():
    print('hello world.')

print(hello())

其中,構造方法__init__接收一個函數類型的參數func,然後,__call__方法就相當於wrapper 函數。

用類實現的裝飾器的使用方法,與用函數實現的裝飾器的使用方法是一樣的。

4,被修飾的函數帶有參數

如果hello 函數帶有參數,如下:

def hello(s):
    print('hello %s.' % s)

那麼裝飾器應該像下面這樣:

import time

def timer(func):
    def wrapper(args):
        s = time.time()

        ret = func(args)

        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

@timer
def hello(s):
    print('hello %s.' % s)

hello('python')

timer 函數的參數依然是要被修飾的函數,wrapper 函數的原型與hello 函數保持一致。

用類來實現,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self, args):
        s = time.time()
        ret = self.func(args)
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello(s):
    print('hello %s.' % s)

print(hello('python'))

不定長參數裝飾器

如果hello 函數的參數是不定長的,timer 應該是如下這樣:

import time

def timer(func):
    def wrapper(*args, **kw):
        s = time.time()

        ret = func(*args, **kw)

        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

@timer
def hello(s1, s2): # 帶有兩個參數
    print('hello %s %s.' % (s1, s2))

@timer
def hello_java():  # 沒有參數
    print('hello java.')

hello('python2', 'python3')
hello_java()

這樣的裝飾器timer,可以修飾帶有任意參數的函數。

用類來實現,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kw):
        s = time.time()
        ret = self.func(*args, **kw)
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello(s1, s2): # 帶有兩個參數
    print('hello %s %s.' % (s1, s2))

@timer
def hello_java():  # 沒有參數
    print('hello java.')

hello('python2', 'python3')
hello_java()

5,裝飾器帶有參數

如果裝飾器也需要帶有參數,那麼則需要在原來的timer 函數的外層再嵌套一層函數TimerTimer 也帶有參數,如下:

import time

def Timer(flag):
    def timer(func):
        def wrapper(*args, **kw):
            s = time.time()
            ret = func(*args, **kw)
            e = time.time()
            print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
            
            return ret

        return wrapper

    return timer

@Timer(1)
def hello(s1, s2): # 帶有兩個參數
    print('hello %s %s.' % (s1, s2))

@Timer(2)
def hello_java():  # 沒有參數
    print('hello java.')

hello('python2', 'python3')
hello_java()

從上面的程式碼中可以看到,timer 的結構沒有改變,只是在wrapper 的內部使用了flag 變數,然後timer 的外層多了一層TimerTimer 的返回值是timer,我們最終使用的裝飾器是Timer

我們通過函數.__name__ 來查看函數的__name__ 值:

print(hello.__name__)       # wrapper
print(hello_java.__name__)  # wrapper

可以發現hellohello_java__name__ 值都是wrapper(即內部函數wrapper 的名字),而不是hellohello_java,這並不符合我們的需要,因為我們的初衷只是想增加hellohello_java的功能,但並不想改變它們的函數名字。

6,使用 @functools.wraps

我們可以使用functools模組的wraps裝飾器來修飾wrapper 函數,以解決這個問題,如下:

import time
import functools

def Timer(flag):
    def timer(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            s = time.time()
            ret = func(*args, **kw)
            e = time.time()
            print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
            
            return ret

        return wrapper

    return timer

@Timer(1)
def hello(s1, s2): # 帶有兩個參數
    print('hello %s %s.' % (s1, s2))

@Timer(2)
def hello_java():  # 沒有參數
    print('hello java.')

此時,再查看hellohello_java__name__值,分別是hellohello_java

7,裝飾器可以疊加使用

裝飾器也可以疊加使用,如下:

@decorator1
@decorator2
@decorator3
def func():
    ...

上面程式碼的所用相當於:

decorator1(decorator2(decorator3(func)))

8,一個較通用的裝飾器模板

編寫裝飾器有一定的套路,根據上文的介紹,我們可以歸納出一個較通用的裝飾器模板:

def func_name(func_args):

    def decorator(func):
    
        @functools.wraps(func)
        def wrapper(*args, **kw):
            # 在這裡可以使用func_args,*args,**kw
            # 邏輯處理
            ...
            ret = func(*args, **kw)
            # 邏輯處理
            ...
            
            return ret

        return wrapper

    return decorator


# 使用裝飾器 func_name
@func_name(func_args)
def func_a(*args, **kw):
    pass

在上面的模板中:

  • func_name 是裝飾器的名字,該裝飾器可以接收參數 func_args
  • 內部函數 decorator 的參數 func,是一個函數類型的參數,就是將來要修飾的函數
  • func 的參數列表可以是任意的,因為我們使用的是*args, **kw
  • 內部函數wrapper 的原型(即參數與返回值)要與 被修飾的函數func 保持統一
  • @functools.wraps 的作用是保留被裝飾的原函數的一些元資訊(比如__name__ 屬性)

與裝飾器相關的模組有functoolswrapt,可以使用這兩個模組來優化完善你寫的裝飾器,感興趣的小夥伴可以自己拓展學習。

(完。)


推薦閱讀:

Python 簡明教程 — 17,Python 模組與包

Python 簡明教程 — 18,Python 面向對象

Python 簡明教程 — 19,Python 類與對象

Python 簡明教程 — 20,Python 類中的屬性與方法

Python 簡明教程 — 21,Python 繼承與多態


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro