Python裝飾器(Decorator)完全指南-高級篇

引言

通過前面兩篇文章(前兩篇文章見基礎篇, 進階篇),讀者們已經瞭解了到了python中的裝飾器背後的實現邏輯,如何理解python中以@標記的帶裝飾器函數,以及如何構造帶參數函數的裝飾器和動態生成裝飾器。在接下來這篇文章中,我們將應用之前的知識來研究一個比較複雜的問題——如何構造裝飾器的裝飾器,以及裝飾器的最佳實踐。

構造裝飾器的裝飾器

在進階篇中我們已經知道了如何構造一個能接受任意參數的裝飾器函數,同時我們還能夠通過構造裝飾器工廠的方式來動態根據輸入參數生成裝飾器。接下來我們將應用這些知識來實現裝飾器的裝飾器。這一裝飾器能夠用來裝飾其他裝飾器函數,從而使得被裝飾的裝飾器函數能夠接收任意輸入參數(請認真讀一讀前面這句邏輯不太直觀的句子,確認你已經理解了下面代碼的目的)。

這一代碼的有用之處在於,我們能夠動態地將我們的任意一個裝飾器變爲一個能夠接收參數的裝飾器工廠。如進階篇中所述,由於裝飾器的函數簽名是固定的——def decorator_func(func_to_decorate),我們無法在使用@調用裝飾器函數的時候動態傳入參數,所以只能先定義一個裝飾器工廠,來替我們接收參數並返回包含了參數的閉環(也就是裝飾器)。而下面的裝飾器將這一功能抽象了出來,使得其可以複用。

代碼如下所示。

def decorator_for_decorator(decorator_to_enhance):
    """
    這一函數用來作爲一個裝飾器工廠來動態生成裝飾器。
    生成的裝飾器能夠被用來裝飾其他裝飾器函數,使得被裝飾的裝飾器函數變爲能夠任意接收參數的裝飾器。
    """
    # 爲了實現參數的傳遞,我們在這裏動態生成了一個裝飾器工廠,並作爲返回值
    # 這一裝飾器工廠將返回一個閉環作爲裝飾器,其中包裝了外界傳入的裝飾器參數
    def decorator_maker(*args, **kwargs):
        def decorator_wrapper(func):
            # 這裏使用了閉環來保證參數的傳遞
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper
    return decorator_maker

有了這一裝飾器之後,我們就可以動態改變其他裝飾器了。

# 注意爲了保證我們所包裝的裝飾器能夠正確接收參數,我們需要保證其函數簽名包含我們想要傳入的參數
# 但這樣的裝飾器函數是無法直接用來裝飾其他函數的
@decorator_for_decorator
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper

# 此時,我們就可以給我們的裝飾器傳入參數啦
@decorator_func(3, 5)
def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))

func('ice cream', 'pizza')
# output:
# Received arguments as (3, 5), {}
# $3 per ice cream, $5 per pizza, what would you like to have?
# Hello, I would like to have ice cream and pizza

上面的代碼可能邏輯上不是那麼直觀。我們接下來詳細分析。

首先我們定義了一個叫做decorator_maker的裝飾器函數。這一函數實質上是一個能夠返回工廠函數的函數。它返回一個能夠返回裝飾器的工廠函數。也就是說,被這一裝飾器裝飾過之後,原有的裝飾器函數將變爲一個裝飾器工廠函數。而這一工廠函數,正如我們在進階篇中所述,用來接收我們想要傳入的參數並通過返回一個動態生成的閉環作爲裝飾器的方式來實現將參數傳入裝飾器中的目的。緊接着我們就可以帶參數通過調用這個工廠函數的方式來實現裝飾一個函數的過程。

通過展開裝飾器的方式來理解裝飾的過程

進一步地,我們總是可以通過展開裝飾器的方式來理解裝飾的過程發生了什麼。這一方法可以用來分析所有的裝飾器裝飾過的函數。以上面的代碼爲例。

# 我們使用@decorator_for_decorator的方法裝飾了decorator_func函數。可以展開如下
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper
decorator_func = decorator_for_decorator(decorator_func)
# 經過上面的步驟,decorator_func引用所指向的其實已經是decorator_for_decorator所返回的decorator_maker函數了。這一函數是一個裝飾器工廠函數。

# 緊接着我們帶參數調用這一工廠函數(@decorator_func(3,5))並使用其返回的裝飾器函數來裝飾另一個函數(func)。這一過程展開如下。
true_decorator = decorator_func(3,5)
# 上面的true_decorator引用指向的是decorator_maker所返回的閉環decorator_wrapper。

def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func = true_decorator(func)
# 到上面這一步爲止,我們已經完成了裝飾func的任務,並且將參數通過閉環的方式傳入了我們所使用的裝飾器中。

裝飾器的最佳實踐

  • 裝飾器實在python2.4中被引入的。所以要使用裝飾器,需要確保我們使用的python版本>=2.4。
  • 使用裝飾器會減慢調用函數的速度。
  • 一旦一個函數被裝飾過之後,我們就無法在運行時再調用未裝飾過的原函數了。
  • 裝飾器事實上只是一個接受函數作爲輸入的函數,並返回一個對原函數的包裝函數。這一包裝過程可能會使得debug過程更加複雜和困難。但在2.5(含)之後的python版本中我們可以使用functools.wraps()來降低裝飾器對debug的影響。

python從2.5開始引入了functools模塊(module),其中包含的裝飾器函數functools.wraps()能夠保證被裝飾函數的函數名,模塊名,以及文檔字符串(docstring)被傳入裝飾器返回的包裝函數中,從而保證了拋出的錯誤信息中能夠包含正確的函數名,改善了debug體驗。如下面的代碼所示。

# python的stacktrace信息中包含函數的__name__屬性來幫助debug
def foo():
    pass
print(foo.__name__)
# output: foo

# 但是用了裝飾器之後,__name__屬性會發生變化
def bar(func):
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass
print(foo.__name__)
# output: wrapper

# 通過使用functools來改變這一狀況
import functools
def bar(func):
    @functools.wraps(func)
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass

print(foo.__name__)
# output: foo

那麼,裝飾器到底有什麼用呢?

未完待續...

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