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

碼農充電站發表於2020-07-05

微信公眾號:碼農充電站pro
個人主頁:https://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

相關文章