Python 元編程 - 裝飾器

Python 中提供了一個叫裝飾器的特性,用於在不改變原始對象的情況下,增加新功能或行爲。

這也屬於 Python "元編程" 的一部分,在編譯時一個對象去試圖修改另一個對象的信息,實現 "控制一切" 目的。

本篇文章作爲裝飾器的基礎篇,在閱讀後應該瞭解如下內容:

  • 裝飾器的原理?
  • 裝飾器如何包裹有參數的函數?
  • 裝飾器本身需要參數怎麼辦?
  • 被裝飾器修飾的函數還是原函數嗎,怎麼解決?
  • 裝飾器嵌套時的順序?
  • 裝飾器常見的應用場景?

裝飾器原理

在具體裝飾器的內容前,先來回顧下 Python 中的基本概念:

1. Python 中,一切都是對象,函數自然也不例外

python 中的對象都會在內存中用於屬於自己的一塊區域。在操作具體的對象時,需要通過 “變量” ,變量本身僅是一個指針,指向對象的內存地址。

函數作爲對象的一種,自然也可以被變量引用。

def hello(name: str):
    print('hello', name)
hello('Ethan')

alias_func_name = hello
alias_func_name('Michael')
# hello Ethan
# hello Michael

alias_func_name 作爲函數的引用,當然也可以作爲函數被使用。

2. 函數接受的參數和返回值都可以是函數

def inc(x):
    return x + 1

def dec(x):
    return x - 1

def operate(func, x):
    result = func(x)
    return result
    
operate(inc,3)
# 4
operate(dec,3)
# 2

這裏 operate 中接受函數作爲參數,並在其內部進行調用。

3. 嵌套函數

def increment():
    def inner_increment(number):
        return 1 + number
    return inner_increment()

print(increment(100)) # 101

在 increment 內部,實現對 number add 1 的操作。

回頭再來看下裝飾器的實現:

# def decorator
def decorator_func(func):
    print('enter decorator..')
    def wrapper():
        print('Step1: enter wrapper func.')
        return func()
    return wrapper

# def target func
def normal_func():
    print("Step2: I'm a normal function.")

# use decorator 
normal_func = decorator_func(normal_func)
normal_func()

decorator_func(func) 中,參數 func 表示想要調用的函數,wrapper 爲嵌套函數,作爲裝飾器的返回值。

wrapper 內部會調用目標函數 func 並附加自己的行爲,最後將 func 執行結果作爲返回值。

究其根本,是在目標函數外部套上了一層 wrapper 函數,達到在不改變原始函數本身的情況下,增加一些功能或者行爲。

通常使用時,使用 @decorator_func 來簡化調用過程的兩行代碼。

將自定義調用裝飾器的兩行代碼刪掉,使用常規裝飾器的寫法加在 normal_func 的定義處,但卻不調用 normal_func,可以發現一個有趣的現象:

# def decorator
def decorator_func(func):
    print('enter decorator..')
    def wrapper():
        print('Step1: enter wrapper func.')
        return func()
    return wrapper

# def target func
@decorator_func
def normal_func():
    print("Step2: I'm a normal function.")

發現 enter decorator.. 在沒有調用的情況下被打印到控制檯。

這就說明,此時 normal_func 已經變成了 wrapper 函數。

@decorator_func 其實隱含了 normal_func = decorator_func(normal_func) 這一行代碼。

對帶有參數的函數使用裝飾器

假設這裏 normal_func 需要接受參數怎麼辦?

很簡單,由於是通過嵌套函數來調用目標函數,直接在 wrapper 中增加參數就可以了。

# def decorator
def decorator_func(func):
    def wrapper(*args, **kwargs):
        print('Step1: enter wrapper func.')
        return func(*args, **kwargs)
    return wrapper


# def target func
def normal_func(*args, **kwargs):
    print("Step2: I'm a normal function.")
    print(args)
    print(kwargs)

# use decorator
normal_func = decorator_func(normal_func)
normal_func(1, 2, 3, name='zhang', sex='boy')

使用 *args, **kwargs 是考慮到該 decorator 可以被多個不同的函數使用,而每個函數的參數可能不同。

裝飾器本身需要參數

在裝飾器本身也需要參數時,可以將其嵌套在另一個函數中,實現參數的傳遞。

# def decorator
def decorator_with_args(*args, **kwargs):
    print('Step1: enter wrapper with args func.')
    print(args)
    print(kwargs)

    def decorator_func(func):
        def wrapper(*args, **kwargs):
            print('Step2: enter wrapper func.')
            return func(*args, **kwargs)
        return wrapper
    return decorator_func


# def target func
def normal_func(*args, **kwargs):
    print("Step3: I'm a normal function.")
    print(args)
    print(kwargs)

normal_func = decorator_with_args('first args')(normal_func)
normal_func('hello')

# use @ to replace the above three lines of code
@decorator_with_args('first args')
def normal_func(*args, **kwargs):
    print("Step3: I'm a normal function.")
    print(args)
    print(kwargs)

來分析下 decorator_with_args 函數:

  • 由於 decorator_with_args 接受了任意數量的參數,同時由於 decorator_funcwrapper 作爲其內部嵌套函數,自然可以訪問其內部的作用域的變量。這樣就實現了裝飾器參數的自定義。
  • decorator_func 是正常的裝飾器,對目標函數的行爲進行包裝。進而需要傳遞目標函數作爲參數。

在使用時:

@decorator_with_args('first args') 實際上做的內容,就是 normal_func = decorator_with_args('first args')(normal_func) 的內容:

  1. decorator_with_args('first args') 返回 decorator_func 裝飾器。
  2. decorator_func 接受的正常函數對象作爲參數,返回包裝的 wrapper 對象。
  3. 最後將 wrapper 函數重命名至原來的函數,使其在調用時保持一致。

保留原函數信息

在使用裝飾器時,看起來原函數並沒有被改變,但它的元信息卻改變了 - 此時的原函數實際是包裹後的 wrapper 函數。

help(normal_func)
print(normal_func.__name__)

# wrapper(*args, **kwargs)
# wrapper

如果想要保留原函數的元信息,可通過內置的 @functools.wraps(func) 實現:

@functools.wraps(func) 的作用是通過 update_wrapperpartial 將目標函數的元信息拷貝至 wrapper 函數。

# def decorator
def decorator_with_args(*args, **kwargs):
    print('Step1: enter wrapper with args func.')
    print(args)
    print(kwargs)

    def decorator_func(func):
    	@functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('Step2: enter wrapper func.')
            return func(*args, **kwargs)
        return wrapper
    return decorator_func

裝飾器嵌套

Python 支持對一個函數同時增加多個裝飾器,那麼添加的順序是怎樣的呢?

# def decorator
def decorator_func_1(func):
    print('Step1: enter decorator_func_1..')

    def wrapper():
        print('Step2: enter wrapper1 func.')
        return func()
    return wrapper


def decorator_func_2(func):
    print('Step1: enter decorator_func_2..')

    def wrapper():
        print('Step2: enter wrapper2 func.')
        return func()
    return wrapper

@decorator_func_2
@decorator_func_1
def noraml_func():
    pass

看一下 console 的結果:

Step1: enter decorator_func_1..
Step1: enter decorator_func_2..

fun_1 在前說明, 在對原函數包裝時,採用就近原則,從下到上。

接着,調用 noraml_func 函數:

Step1: enter decorator_func_1..
Step1: enter decorator_func_2..
Step2: enter wrapper2 func.
Step2: enter wrapper1 func.

可以發現,wrapper2 內容在前,說明在調用過程中由上到下。

上面嵌套的寫法,等價於 normal_func = decorator_func_2(decorator_func_1(normal_func)),就是正常函數的調用過程。

對應執行順序:

  1. 在定義時,先 decorator_func_1 後 decorator_func_2.
  2. 在調用時,先 decorator_func_2 後 decorator_func_1.

應用場景

日誌記錄

在一些情況下,需要對函數執行的效率進行統計或者記錄一些內容,但又不想改變函數本身的內容,這時裝飾器是一個很好的手段。

import timeit
def timer(func):
    def wrapper(n):
        start = timeit.default_timer()
        result = func(n)
        stop = timeit.default_timer()
        print('Time: ', stop - start)
        return result
    return wrappe

作爲緩存

裝飾器另外很好的應用場景是充當緩存,如 lru 會將函數入參和返回值作爲當緩存,以計算斐波那契數列爲例, 當 n 值大小爲 30,執行效率已經有很大差別。

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)
        
@functools.lru_cache(128)
def fib_cache(n):
    if n < 2:
        return 1
    else:
        return fib_cache(n - 1) + fib_cache(n - 2)
        
Time:  0.2855725
Time:  3.899999999995574e-05

總結

在這一篇中,我們知道:

裝飾器的本質,就是利用 Python 中的嵌套函數的特點,將目標函數包裹在內嵌函數中,然後將嵌套函數 wrapper作爲返回值返回,從而達到修飾原函數的目的。

而且由於返回的是 wrapper 函數,自然函數的元信息肯定不再是原函數的內容。

對於一個函數被多個裝飾器修飾的情況:

  • 在包裝時,採用就近原則,從近點開始包裝。
  • 在被調用時,採用就遠原則,從遠點開始執行。

這自然也符合棧的調用過程。

參考

https://www.programiz.com/python-programming/decorator

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