動態

詳情 返回 返回

Python筆記三之閉包與裝飾器 - 動態 詳情

本文首發於公眾號:Hunter後端

原文鏈接:Python筆記三之閉包與裝飾器

這一篇筆記介紹 Python 裏面的裝飾器。

在介紹裝飾器前,首先提出這樣一個需求,我想統計某個函數的執行時間,假設這個函數如下:

import time

def add(x, y):
    time.sleep(1)
    return x + y

想要統計 add 函數的執行時間,可以如何操作,在一般情況下,可能會想到如下操作:

start_time = time.time()
add(1, 2)
end_time = time.time()
print("函數執行時間為:", end_time - start_time)

而如果我們想要統計很多個函數的執行時間,然後打印出來,應該如何操作呢?

這裏就可以用上 Python 裏裝飾器的操作。

本篇筆記目錄如下:

  1. 閉包

    1. 閉包
    2. 閉包實現計數器

      1. 自由變量
  2. 裝飾器
  3. 裝飾器代碼示例裝飾器原理
  4. 裝飾器加參數
  5. 多重裝飾器
  6. 裝飾器類

1、閉包

在介紹裝飾器前,先來理解一下閉包的概念。

1. 閉包

我們知道,一個函數內部的變量是局部變量,在函數執行結束之後,函數內部的變量就會被銷燬,而閉包,則可以使我們能夠讀取函數內部變量。

比如下面這個示例:

def outer_func():
    msg = "outer info"

    def inner_func():
        print(msg)
        return msg

    return inner_func


func = outer_func()
func()

關於閉包,2023.11.13 百度百科的釋義如下:

閉包就是能夠讀取其他函數內部變量的函數。例如在javascript中,只有函數內部的子函數才能讀取局部變量,所以閉包可以理解成“定義在一個函數內部的函數“。在本質上,閉包是將函數內部和函數外部連接起來的橋樑。

所以閉包的作用可以是避免全局變量可能帶來的維護問題,又能夠長久的保存變量。

但是同時,基於這個特性,閉包函數內部的局部變量因為會保持在內存中,不會在調用後被自動清除,所以需要注意其可能帶來的內存泄漏的問題。

2. 閉包實現計數器

下面我們使用閉包來實現一個計數器的功能:

def create_counter():
    count = 0

    def add_counter():
        nonlocal count
        count += 1
        return count
    return add_counter

f = create_counter()
print(f())
print(f())
print(f())

這裏使用 nolocalcount 變量進行了聲明,作用是聲明該變量只在函數局部內起作用,也就是 create_counter() 內,所以在 add_counter() 外聲明 count 變量之後,在 add_counter() 內可以保存其相應的狀態,也就是這裏我們的計數功能。

nolocal 關鍵字是專門定義在閉包內使用的。

相對應的 global 字段時定義的全局變量,這裏不多做介紹了。

自由變量

自由變量的含義是指未綁定到本地作用域的變量,比如上面的示例裏,countadd_counter() 函數裏就是一個自由變量,因為它在外層函數 create_counter() 裏定義,但沒有在內層的 add_counter() 中定義。

至於為什麼在 add_counter() 裏對 count 變量進行 nolocal 的聲明,是因為修飾的對象類型是 int,與之類似的還有 strtuple,他們都屬於不可變類型。

而如果我們閉包的內外部函數裏的對象是 list,dict 這種可變類型,那麼則不需要使用 nolocal 來進行修飾,比如下面的操作:

def create_counter():
    count_dict = [0]  
    
    def add_counter():        
        count_dict[0] += 1        
        return count_dict[0]    
        
    return add_counter

2、 裝飾器

裝飾器的作用是在不修改被裝飾函數的情況下,給被裝飾的函數添加額外的功能。

而裝飾器就是基於閉包的操作,不過外層函數傳入的參數是被裝飾的函數,且在 Python 裏,使用裝飾器的方式是在被裝飾函數前加一行,使用 @ 符號來調用。

最簡單的裝飾器的操作如下:

def decorator(func):
    print("calling decorator ...")
    return func


@decorator
def test():
    print("calling test ...")

我們在下面的操作中使用一個示例介紹如何基於閉包使用裝飾器。

3、裝飾器代碼示例

前面我們介紹了一個需求場景,需要統計函數的執行時間,基於這個需求,我們就可以使用裝飾器的操作來完成,以下是代碼示例:

import time

def time_decorator(func):

    def inner_func(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        total_time = time.time() - start_time
        print("func 耗時:", total_time)
        return result
    return inner_func


@time_decorator
def add(x, y):
    time.sleep(1)
    return x + y

add(1, 7)

裝飾器原理

我們使用 @ 加上裝飾器函數名稱,即表示調用這個裝飾器,然後將被裝飾的函數,上面的示例是 add() 函數,作為參數傳入裝飾器,然後在內部函數 inner_func() 中添加額外的功能,這裏是統計函數運行時間,然後將其返回。

將裝飾器的操作扁平化操作,就和前面閉包示例計數器的使用是一致的:

def add(x, y):
    time.sleep(1)
    return x + y

func = time_decorator(add)
func(1, 2)

所以,在加了裝飾器的函數運行中,實際上運行的是裝飾器的內部函數,我們可以通過打印函數的名稱來進行驗證:

print(add.__name__)  # inner_func

如果想要保存原函數的基本信息,比如函數名稱,我們可以給裝飾器的內部函數加上裝飾器自動複製函數信息,functools.wraps,使用示例如下:

import time
import functools

def time_decorator(func):

    @functools.wraps(func)
    def inner_func(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        total_time = time.time() - start_time
        print("func 耗時:", total_time)
        return result
    return inner_func

@time_decorator
def add(x, y):
    time.sleep(1)
    return x + y
   
print(add.__name__)  # add

這樣打印的就是原始函數的函數名稱了。

4、裝飾器加參數

如果我們想調用裝飾器的時候,給裝飾器加一個參數,比如這裏的 time_decorator,想加一個默認的時間參數(這個想要實現的功能可能並沒有實際意義,純粹是為了實現給裝飾器加默認參數這個功能),調用的時候就是:

@time_decorator(default_time=2)

那麼裝飾器的定義則如下所示:

def time_decorator(default_time=2):
    def decorator(func):
        def inner_func(*args, **kwargs):
            start_time = time.time()
            time.sleep(default_time)
            result = func(*args, **kwargs)
            total_time = time.time() - start_time
            print("func 耗時:", total_time)
            return result
        return inner_func
    return decorator


@time_decorator(2)
def add(x, y):
    time.sleep(1)
    return x + y

add(1, 8)

如果調用裝飾器的時候想使用默認參數,直接不賦值即可:

@time_decorator()
def add(x, y):
    time.sleep(1)
    return x + y

5、多重裝飾器

如果我們想要調用多個裝飾器來裝飾一個函數,其執行順序是怎麼要的呢,我們可以用下面的例子做個實驗。

比如我們要做一個漢堡,最外層兩片面包,中間夾兩片青菜,最中間是一片肉,可以如下操作:

def bread_decorator(func):

    def inner(*args, **kwargs):
        print("先加片面包")
        func(*args, **kwargs)
        print("再加片面包")
    return inner


def vegetable_decorator(func):

    def inner(*args, **kwargs):
        print("先加片蔬菜")
        func(*args, **kwargs)
        print("再加片蔬菜")
    return inner


@bread_decorator
@vegetable_decorator
def make_hamburger():
    print("加片肉")


make_hamburger()

輸出的結果為:

先加片面包
先加片蔬菜
加片肉
再加片蔬菜
再加片面包

所以這裏裝飾器的執行時按照順序從上到下執行的。

我們可以嘗試將裝飾器的調用拉平,用到的其實就是設計模式裏的裝飾器模式了(設計模式的幾種類型我回頭會更新一個系列),我們先將 make_hamburger() 的函數重新定義,然後調用,bread_decorator()vege_decorator() 還是保持不變:

def make_hamburger():    
    print("加片肉")

food = vegetable_decorator(make_hamburger)
food = bread_decorator(food)
food()

執行的結果和前面使用裝飾器的方式調用是一致的。

6、裝飾器類

前面介紹的是用函數作為裝飾器,我們還可以設計一個類用作裝飾器,示例如下:

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

    def __call__(self, *args, **kwargs):

        start_time = time.time()
        result = self.func(*args, **kwargs)
        print(f"函數 {self.func.__name__} 運行時間為:{time.time() - start_time}")
        return result


@TimeLogDecorator
def add(x, y):
    time.sleep(1)
    return x + y


result = add(1, 6)

在類的 __call__ 方法寫入我們在函數裝飾器的內部函數裏的內容即可實現裝飾器的功能。

如果想要給類裝飾器帶參數的話,示例如下:

class TimeLogDecoratorArg:
    def __init__(self, base_gap_time):
        self.base_gap_time = base_gap_time

    def __call__(self, func):

        def inner_func(*args, **kwargs):
            start_time = time.time()
            time.sleep(self.base_gap_time)
            result = func(*args, **kwargs)
            print(f"函數 {func.__name__} 運行時間為:{time.time() - start_time}")
            return result
        return inner_func


@TimeLogDecoratorArg(2)
def add(x, y):
    time.sleep(1)
    return x + y

如果想獲取更多相關文章,可掃碼關注閲讀:

user avatar ciel717 頭像 u_17037082 頭像 monkeynik 頭像 itwhat 頭像 xingchendahai_68d7dff410962 頭像 tssc 頭像 yuzhoustayhungry 頭像 dalideshoushudao 頭像 aitibao_shichangyingxiao 頭像 mangrandechangjinglu 頭像 bianchengdandan 頭像 wangqingsheng 頭像
點贊 24 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.