裝飾器與函數式Python(譯)

原文:Decorators and Functional Python

譯者:youngsterxyf

裝飾器是Python的一大特色。除了在語言中的原本用處,還幫助我們以一種有趣的方式(函數式)進行思考。

我打算自底向上解釋裝飾器如何工作。首先解釋幾個話題以幫助理解裝飾器。然後,深入一點探索幾個簡單的裝飾器以及它們如何工作。最後,討論一些更高級的使用裝飾器的方式,比如:傳遞可選參數給裝飾器或者串接幾個裝飾器。

首先以我能想到的最簡單的方式來定義Python函數是什麼。基於該定義,我們可以類似的簡單方式來定義裝飾器。

函數是一個完成特定任務的可複用代碼塊。

好的,那麼裝飾器又是什麼呢?

裝飾器是一個修改其他函數的函數。

現在在裝飾器的定義上進行詳述,先解釋一些先決條件。

函數是一等對象

Python中,所有東西都是對象。這意味着可以通過名字引用函數,以及像其他對象那樣傳遞。例如:

def traveling_function():
    print "Here I am!"

function_dict = {
    "func": traveling_function
}

trav_func = function_dict['func']
trav_func()
# >> Here I am!

traveling_function 被賦值給 function_dict 字典中鍵 func 的值,仍舊可以正常調用。

一等函數允許高階函數

我們可以像其他對象那樣傳遞函數。可以將函數作爲值傳遞給字典,放在列表中,或者作爲對象的屬性進行賦值。那爲什麼不能作爲參數傳給另一個函數呢?當然可以!如果一個函數接受另一個函數作爲其參數或者返回另一個函數,則稱之爲高階函數。

def self_absorbed_function():
    return "I'm an amazing function!"

def printer(func):
    print "The function passed to me says: " + func()

# Call `printer` and give it `self_absorbed_function` as an argument
printer(self_absorbed_function)
# >>> The function passed to me says: I'm an amazing function!

現在你也看到函數可以作爲參數傳給另一個函數,而且傳給函數的函數還可以調用。這允許我們創建一些有意思的函數,例如裝飾器。

裝飾器基礎

本質上,裝飾器就是一個以另一個函數爲參數的函數。大多數情況下,它們會返回所包裝函數的一個修改版本。來看個我們能想到的最簡單的裝飾器---同一性(identity)裝飾器,或許對我們理解裝飾器的工作原理有所幫助。

def identity_decorator(func):
    def wrapper():
        func()
    return wrapper

def a_function():
    print "I'm a normal function."

# `decorated_function` 是 `identity_function` 返回的函數,也就是嵌套函數 `wrapper`
decorated_function = identity_function(a_function)

# 如下調用 `identity_function` 返回的函數
decorated_function()
# >>> I'm a normal function

這裏, identity_decorator 根本沒有修改它包裝的函數,只是簡單地返回一個函數(wrapper),這個函數在被調用之時,會去調用原來作爲identity_decorator 參數的函數。這是個沒有用處的裝飾器!

關於 identity_decorator 的有趣之處是 wrapper 能夠訪問變量 func ,即使 func 並非是它的參數。這歸因於閉包。

閉包

閉包是一個花哨的術語,意爲聲明一個函數時,該函數會維持一個指向聲明所處詞法環境的引用。

上例中定義的函數 wrapper 能夠在其局部作用域(local scope)中訪問 func。這意味着在 wrapper (返回並賦值給變量 decorated_function )的整個生命週期內,它都可以訪問 func 變量。一旦 identity_decorator返回,那麼訪問 func 的唯一方式就是通過 decorated_function 。 func 只作爲一個變量存在於decorated_function 作用域環境的內部。

一個簡單的裝飾器

現在我們來創建一個確實有點用的裝飾器。這個裝飾器所做的就是記錄它所修改的函數被調用了多少次。

def logging_decorator(func):
    def wrapper():
        wrapper.count += 1
        print "The function I modify has been called {0} time(s)".format(wrapper.count)
        func()
    wrapper.count = 0
    return wrapper

def a_function():
    print "I'm a normal function."

modified_function = logging_decorator(a_function)

modified_function()
# >>> The function I modify has been called 1 time(s).
# >>> I'm a normal function.

modified_function()
# >>> The function I modify has been called 2 time(s).
# >>> I'm a normal function.

我們說裝飾器會修改函數,這樣來想對理解也是有幫助的。但如例子所見, logging_decorator 返回的是一個類似於 a_function 的新函數,只是多了一個日誌特性。

上例中, logging_decorator 不僅接受一個函數作爲參數,並且返回一個函數, wrapper 。每次 logging_decorator 返回的函數得到調用,它就對 wrapper.count的值加1,打印出來,然後調用 logging_decorator 包裝的函數。

你也許正疑惑爲什麼我們的計數器是 wrapper 的一個屬性而不是一個普通的變量。難道 wrapper 的閉包環境不是讓我們訪問在其局部作用域中聲明的任意變量麼?是的,但有個問題。Python中,閉包允許對其函數作用域鏈中任一變量的進行任意讀操作,但只允許對可變對象(列表、字典、等等)進行寫操作。整數在Python中是非可變對象,因此我們不能修改 wrapper 內部整型變量的值。相反,我們將計數器作爲 wrapper 的一個屬性---一個可變對象,因此可以隨我們自己增大它的值。

裝飾器語法

在前一個例子中,我們看到可以將一個函數作爲參數傳給裝飾器,從而使用裝飾器函數對該函數進行包裝。然而,Python還有一個語法模式使得這一切更加直觀,更容易閱讀,一旦你熟悉了裝飾器。

# In the previous example, we used our decorator function by passing the
# function we wanted to modify to it, and assigning the result to a variable
def some_function():
    print "I'm happiest when decorated."

# Here we will make the assigned variable the same name as the wrapped function
some_function = logging_decorator(some_function)

# We can achieve the exact same thing with this syntax:

@logging_decorator
def some_function():
    print "I'm happiest when decorated"

使用裝飾器語法,鳥瞰其中發生的事情:

  1. 解釋器到達被裝飾的函數,編譯 some_function,並將其命名爲 'some_function'。
  2. 然後將該函數傳遞給裝飾行中指定的裝飾器函數( logging_function )。
  3. 裝飾器函數(通常是用來包裝原函數的另一個函數)的返回值取代原來的函數(some_function ),綁定到變量名 some_function 。

將這些步驟記住,讓我們來更清晰地解釋 identity_decorator 。

def identity_decorator(func):
    # Everything here happens when the decorator LOADS and is passed
    # the function as decribed in step 2 above
    def wrapper():
        # Things here happen each time the final wrapped function gets CALLED
        func()
    return wrapper

希望那些註釋有助於理解。每次調用被包裝的函數,僅執行裝飾器返回的函數中的指令。返回函數之外的指令僅執行一次---上述步驟2中描述的:裝飾器首次接收到傳遞給它的待包裝函數之時。

在觀察更多的有意思的裝飾器之前,我想再解釋一樣東西。

*args與**kwargs

以前你也許有時會把這兩者相混淆了。讓我們一次性地討論它們。

  • 通過在形參列表中使用 *args 語法,python函數能夠接收可變數量的位置參數(positional arguments)。 *args 會將所有沒有關鍵字的參數放入一個參數元組中,在函數裏可以訪問元組中的參數。相反,將 *args 用於函數調用時的實參列表之時,它會將參數元組展開成一系列的位置參數。

def function_with_many_arguments(*args):
    print args

# `args` within the function will be a tuple of any arguments we pass
# which can be used within the function like any other tuple
function_with_many_arguments('hello', 123, True)
# >>> ('hello', 123, True)

def function_with_3_parameters(num, boolean, string):
    print "num is " + str(num)
    print "boolean is " + str(boolean)
    print "string is " + string

arg_list = [1, False, 'decorators']

# arg_list will be expanded into 3 positional arguments by the `*` symbol
function_with_3_parameters(*arg_list)
# >>> num is 1
# >>> boolean is False
# >>> string is decorators

重述一遍:在形參列表中, *args會將一系列的參數壓縮進一個名爲'args'的元組,而在實參列表中, *args 會將一個可迭代的參數數據結構展開爲一系列的位置實參應用於函數。

如你所見在實參展開的例子中, * 符號可與'args'之外的名字一起使用。當壓縮/展開一般的參數列表,使用 *args 的形式僅僅是一種慣例。

  • **kwargs 與 *args 的行爲類似,但用於關鍵字參數而非位置參數。如果在函數的形參列表中使用 **kwargs ,它會收集函數收到的所有額外關鍵字參數,放入一個字典中。如果用於函數的實參列表,它會將一個字典展開爲一系列的關鍵字參數。

def funtion_with_many_keyword_args(**kwargs):
    print kwargs

function_with_many_keyword_args(a='apples', b='bananas', c='cantalopes')
# >> {'a':'apples', 'b':'bananas', 'c':'cantalopes'}

def multiply_name(count=0, name=''):
    print name * count

arg_dict = {'count': 3, 'name': 'Brian'}

multiply_name(**arg_dict)
# >> BrianBrianBrian

既然你理解了 *args 與 **kwargs 的工作原理,那麼我們就繼續研究一個你會發現很有用的裝飾器。

緩存製表(Memoization)

緩存製表是避免潛在的昂貴的重複計算的一種方法,通過緩存函數每次執行的結果來實現。這樣,下一次函數以相同的參數執行,就可以從緩存中獲取返回結果,不需要再次計算結果。

from functools import wraps

def memoize(func):
    cache = {}

    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def an_expensive_function(arg1, arg2, arg3):
    ...

你可能注意到了示例代碼中一個奇怪的 @wraps 裝飾器。在完整地討論 memoize 之前我將簡要地解釋這個裝飾器。

  • 使用裝飾器的一個副作用是被包裝的函數失去了本來有的 __name__ , __doc__ , 以及 __module__ 屬性。 wraps 函數是一個包裝另一個裝飾器返回的函數的裝飾器,將那三個屬性的值恢復爲函數未裝飾之時的值。例如: 如果不使用 wraps 裝飾器, an_expensive_function 的名字(通過an_expensive_function.__name__ 可以看到)將是 'wrapper' 。

我認爲 memoize 是一個很好的裝飾器用例。它服務於一個很多函數都需要的目的,通過將它創建爲一個通用裝飾器,我們可以將它的功能應用於任一能夠從其中獲益的函數。這就避免了在多種不同的場合重複實現這個功能。因爲不需要重複自己,所以我們的代碼更容易維護,並且更容易閱讀和理解。只要讀一個單詞你就能立刻理解函數使用了緩存製表。

需要提醒的是:緩存製表僅適用於純函數。也就是說給定一個特定的參數設置,函數確定總會產生相同的結果。如果函數依賴於不作爲參數傳遞的全局變量、I/O、或者其它任意可能影響返回值的東西,緩存製表會產生令人迷惑的結果!並且,一個純函數不會有任何副作用。因此,如果你的函數會增大一個計數器,或者調用另一個對象的方法,或者其它任意不在函數的返回結果中表示的東西,當結果是從緩存中返回時,副作用操作並不會得到執行。

類的裝飾器

最初,我們說裝飾器是一個修改另一個函數的函數,但其實它們可以用於修改類或者方法。對類進行裝飾並不常見,但某些情況下作爲元類(metaclass)的一個替代,類的裝飾器是一個有用的工具。

foo = ['important', 'foo', 'stuff']

def add_foo(klass):
    klass.foo = foo
    return klass


@add_foo
class Person(object):
    pass

brian = Person()

print brian.foo
# >> ['important', 'foo', 'stuff']

現在,類 Person 的任一對象都有一個超級重要的 foo 屬性!注意,因爲我們裝飾的是一個類,所以裝飾器返回的不是一個函數,而是一個類。更新一下裝飾器的定義:

裝飾器是一個修改函數、或方法、或類的函數。

裝飾器類

事實證明我早先對你隱瞞了一些其它事情。不僅裝飾器可以裝飾一個類,並且裝飾器也可以是一個類!對於裝飾器的唯一要求就是它的返回值必須可調用(callable)。這意味着裝飾器必須實現 __call__ 魔術方法,當你調用一個對象時,會隱式調用這個方法。函數當然是隱式設置這個方法的。我們重新將identity_decorator 創建爲一個類來看看它是如何工作的。

class IdentityDecorator(object):
    def __init__(self, func):
        self.func = func

    def __call__(self):
        self.func()


@IdentityDecorator
def a_function():
    print "I'm a normal function."

a_function()
# >> I'm a normal function.

如下是上例中發生的事情:

  • 當 IdentityDecorator 裝飾 a_function 時,它的行爲就和裝飾器函數一樣。這個代碼片段等價於上例中的裝飾器語法:a_function = IdentityDecorator(a_function) 。調用(實例化)該裝飾器類時,需將其裝飾的函數作爲一個實參傳遞給它。

  • 實例化 IdentityDecorator 之時,會以被裝飾的函數作爲實參調用初始化函數 __init__ 。本例中,初始化函數所做的事情就是將被裝飾函數賦值給一個屬性,這樣之後就可以通過其它方法進行調用。

  • 最後,調用 a_function (實際上是返回的包裝了 a_function 的 IdentityDecorator 對象)之時,會調用對象的 __call__ 方法。這僅是一個同一性裝飾器,所以方法只是簡單地調用了該類所裝飾的函數。

再次更新一下我們對裝飾器的定義!

裝飾器是一個修改函數、方法或者類的可調用對象。

帶參數的裝飾器

有時,需要根據不同的情況改變裝飾器的行爲。你可以通過傳參來做到這一點。

from functools import wraps

def argumentative_decorator(gift):
    def func_wrapper(func):
        @wraps(func)
        def returned_wrapper(*args, **kwargs):
             print "I don't like this " + gift + "you gave me!"
             return func(gift, *args, **kwargs)
        return returned_wrapper
    return func_wrapper

@argumentative_decorator("sweater")
def grateful_function(gift):
    print "I love the " + gift + "!Thank you!"

grateful_function()
# >> I don't like this sweater you gave me!
# >> I love the sweater! Thank you!

我們來看看如果不使用裝飾器語法這個裝飾器函數是如何工作的:

# If we tried to invoke without an argument:
grateful_function = argumentative_function(grateful_function)

# But when given an argument, the pattern changes to:
grateful_function = argumentative_decorator("sweater")(grateful_function)

需要注意的地方是:當給定參數,首先僅以那些參數調用裝飾器---被包裝的函數並不在參數中。裝飾器調用返回後,裝飾器要包裝的函數被傳遞給裝飾器初始調用返回的函數(本例中,爲 argumentative_decorator("sweater") 的返回值)。

逐步地:

  1. 解釋器到達被裝飾函數之處,編譯 grateful_function ,並將其綁定到名字'grateful_function'。
  2. 傳遞參數"sweater"調用 argumentative_decorator ,返回 func_wrapper 。
  3. 以 grateful_function 爲參調用 func_wrapper ,返回 returned_wrapper 。
  4. 最後, returned_wrapper 取代原來的函數 grateful_function ,並綁定到名字'grateful_function' 。

我想這一過程相比沒有裝飾器參數理解起來更難一點,但是如果你花些時間將其理解通透,我希望是有意義的。

帶可選參數的裝飾器

有多種方式讓裝飾器接受可選參數。根據你是想使用位置參數、關鍵字參數還是兩者皆是,需要使用稍微不同的模式。如下我將展示一種接受一個可選關鍵字參數的方式:

from functools import wraps

GLOBAL_NAME = "Brian"

def print_name(function=None, name=GLOBAL_NAME):
    def actual_decorator(function):
        @wraps(function)
        def returned_func(*args, **kwargs):
            print "My name is " + name
            return function(*args, **kwargs)
        return returned_func

    if not function:    # User passed in a name argument
        def waiting_for_func(function):
            return actual_decorator(function)
        return waiting_for_func

    else:
        return actual_decorator(function)

@print_name
def a_function():
    print "I like the name!"

@print_name(name='Matt')
def another_function():
    print "Hey, that's new!"

a_function()
# >> My name is Brian
# >> I like that name!

another_function()
# >> My name is Matt
# >> Hey, that's new!

如果我們傳遞關鍵字參數 name 給 print_name ,那麼它的行爲就與前一個例子中的 argumentative_decorator 相似。即,首先以 name 爲參調用 print_name 。然後,將待包裝的函數傳遞給首次調用返回的函數。

如果我們沒有提供 name 實參, print_name 的行爲就與前面我們看到的不帶參數的裝飾器一樣。裝飾器僅以待包裝的函數作爲唯一的參數進行調用。

print_name 支持兩種可能性。它會檢查是否收到作爲參數的被包裝函數。如果沒有,則返回函數 waiting_for_func ,該函數可以被包裝函數作爲參數進行調用。如果收到被包裝函數作爲參數,則跳過中間步驟,直接調用 actual_decorator 。

串接裝飾器

現在來探索一下今天要講的最後一個裝飾器的特性:串接。你可以在任意給定的函數之上堆疊使用多個裝飾器, 這種構建函數的方式與使用多重繼承構建類相類似。不過最好不要瘋狂使用這種特性。

@print_name('Sam')
@logging_decorator
def some_function():
    print "I'm the wrapped function!"

some_function()
# >> My name is Sam
# >> The function I modify has been called 1 time(s).
# >> I'm the wrapped function!

當你串接使用裝飾器時,它們堆疊的順序是自底向上的。將被包裝的函數 some_function 經編譯後傳遞給它之上的第一個裝飾器( logging_decorator )。然後第一個裝飾器的返回值被傳遞給第二個裝飾器。依此逐個應用鏈上每個裝飾器。

因爲我們使用的兩個裝飾器都是 print 一個值,然後執行傳遞給它們的函數,這意味着當調用被包裝函數時,鏈中的最後一個裝飾器 print_name 打印輸出中的第一行。

總結

我認爲裝飾器最大的好處之一在於讓你能夠從更高的抽象層次進行思考。假如你開始閱讀一個函數定義,看到有一個 memoize 裝飾器,你立刻就能明白你正在看的是一個使用緩存製表的函數。如果緩存製表的代碼包含在函數體內,就會需要額外的腦力進行解析,並且會有引入誤解的可能。使用裝飾器也允許代碼複用,從而節省時間、簡化調試,並且使得重構更加容易。

玩玩裝飾器也是一種很好的學習函數式概念(如高階函數與閉包)的方式。

我希望本文閱讀起來很愉快,並且內容翔實。

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