天天看點

Python裝飾器

裝飾器的優點是能夠抽離出大量函數中與函數功能本身無關的雷同代碼并繼續重用。即,可以将函數“修飾”為完全不同的行為,可以有效的将業務邏輯正交分解。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。例如記錄日志,需要對某些函數進行記錄。笨的辦法,每個函數加入代碼,如果代碼變了,就悲催了。裝飾器的辦法,定義一個專門日志記錄的裝飾器,對需要的函數進行裝飾。

Python 的 Decorator在使用上和Java/C#的Annotation很相似,都是在方法名前面加一個@XXX注解來為這個方法裝飾一些東西。但是,Java/C#的Annotation也很讓人望而卻步,在使用它之前你需要了解一堆Annotation的類庫文檔,讓人感覺就是在學另外一門語言。而Python使用了一種相對于Decorator Pattern和Annotation來說非常優雅的方法,這種方法不需要你去掌握什麼複雜的OO模型或是Annotation的各種類庫規定,完全就是語言層面的玩法:一種函數式程式設計的技巧。

裝飾器背後的原理

在Python中,裝飾器實作是十分友善。原因是:函數可以被扔來扔去。

Python的函數就是對象

要了解裝飾器,就必須先知道,在Python裡,函數也是對象(functions are objects)。明白這一點非常重要,讓我們通過一個例子來看看為什麼。

def shout(word="yes"):
    **return** word.capitalize() + "!"
print(shout())
\# outputs : 'Yes!'
\# 作為一個對象,你可以像其他對象一樣把函數指派給其他變量
scream = shout
\# 注意我們沒有用括号:我們不是在調用函數,
\# 而是把函數'shout'的值綁定到'scream'這個變量上
\# 這也意味着你可以通過'scream'這個變量來調用'shout'函數
print(scream())
\# outputs : 'Yes!'
\# 不僅如此,這也還意味着你可以把原來的名字'shout'删掉,
\# 而這個函數仍然可以通過'scream'來通路
del shout
**try**:
    print(shout())
**except** NameError as e:
    print(e)
    \# outputs: "name 'shout' is not defined"
print(scream())
\# outputs: 'Yes!'      

Python 函數的另一個有趣的特性是,它們可以在另一個函數體内定義。

def talk():
    \# 你可以在 'talk' 裡動态的(on the fly)定義一個函數...
    **def** whisper(word="yes"):
        **return** word.lower() + "..."
    \# ... 然後馬上調用它!
    print(whisper())
\# 每當調用'talk',都會定義一次'whisper',然後'whisper'在'talk'裡被調用
talk()
\# outputs:
\# "yes..."
\# 但是"whisper" 在 "talk"外并不存在:
**try**:
    print(whisper())
**except** NameError as e:
    print(e)
    \# outputs : "name 'whisper' is not defined"      

函數引用(Functions references)

你剛剛已經知道了,Python的函數也是對象,是以:

  • 可以被指派給變量
  • 可以在另一個函數體内定義

那麼,這樣就意味着一個函數可以傳回另一個函數:

def get_talk(type="shout"):
    \# 我們先動态定義一些函數
    **def** shout(word="yes"):
        **return** word.capitalize() + "!"
    **def** whisper(word="yes"):
        **return** word.lower() + "..."
    \# 然後傳回其中一個
    **if** type == "shout":
        \# 注意:我們是在傳回函數對象,而不是調用函數,是以不要用到括号 "()"
        **return** shout
    **else**:
        **return** whisper
\# 那你改如何使用d呢?
\# 先把函數指派給一個變量
talk = get_talk()
\# 你可以發現 "talk" 其實是一個函數對象:
print(talk)
\# outputs : <function shout at 0xb7ea817c>
\# 這個對象就是 get_talk 函數傳回的:
print(talk())
\# outputs : Yes!
\# 你甚至還可以直接這樣使用:
print(get_talk("whisper")())
\# outputs : yes...      

既然可以傳回一個函數,那麼也就可以像參數一樣傳遞:

def shout(word="yes"):
    **return** word.capitalize() + "!"
scream = shout
**def** do_something_before(func):
    print("I do something before then I call the function you gave me")
    print(func())
do_something_before(scream)
\# outputs:
\# I do something before then I call the function you gave me
\# Yes!      

裝飾器實戰

現在已經具備了了解裝飾器的所有基礎知識了。裝飾器也就是一種包裝材料,它們可以讓你在執行被裝飾的函數之前或之後執行其他代碼,而且不需要修改函數本身。

手工制作的裝飾器

\# 一個裝飾器是一個需要另一個函數作為參數的函數
**def** my_shiny_new_decorator(a_function_to_decorate):
    \# 在裝飾器内部動态定義一個函數:wrapper(原意:包裝紙).
    \# 這個函數将被包裝在原始函數的四周
    \# 是以就可以在原始函數之前和之後執行一些代碼.
    **def** the_wrapper_around_the_original_function():
        \# 把想要在調用原始函數前運作的代碼放這裡
        print("Before the function runs")
        \# 調用原始函數(需要帶括号)
        a_function_to_decorate()
        \# 把想要在調用原始函數後運作的代碼放這裡
        print("After the function runs")
    \# 直到現在,"a_function_to_decorate"還沒有執行過 (HAS NEVER BEEN EXECUTED).
    \# 我們把剛剛建立的 wrapper 函數傳回.
    \# wrapper 函數包含了這個函數,還有一些需要提前後之後執行的代碼,
    \# 可以直接使用了(It's ready to use!)
    **return** the_wrapper_around_the_original_function
\# Now imagine you create a function you don't want to ever touch again.
**def** a_stand_alone_function():
    print("I am a stand alone function, don't you dare modify me")
a_stand_alone_function()
\# outputs: I am a stand alone function, don't you dare modify me
\# 現在,你可以裝飾一下來修改它的行為.
\# 隻要簡單的把它傳遞給裝飾器,後者能用任何你想要的代碼動态的包裝
\# 而且傳回一個可以直接使用的新函數:
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
\# outputs:
\# Before the function runs
\# I am a stand alone function, don't you dare modify me
\# After the function runs      

裝飾器的文法糖

我們用裝飾器的文法來重寫一下前面的例子:

\# 一個裝飾器是一個需要另一個函數作為參數的函數
**def** my_shiny_new_decorator(a_function_to_decorate):
    \# 在裝飾器内部動态定義一個函數:wrapper(原意:包裝紙).
    \# 這個函數将被包裝在原始函數的四周
    \# 是以就可以在原始函數之前和之後執行一些代碼.
    **def** the_wrapper_around_the_original_function():
        \# 把想要在調用原始函數前運作的代碼放這裡
        print("Before the function runs")
        \# 調用原始函數(需要帶括号)
        a_function_to_decorate()
        \# 把想要在調用原始函數後運作的代碼放這裡
        print("After the function runs")
    \# 直到現在,"a_function_to_decorate"還沒有執行過 (HAS NEVER BEEN EXECUTED).
    \# 我們把剛剛建立的 wrapper 函數傳回.
    \# wrapper 函數包含了這個函數,還有一些需要提前後之後執行的代碼,
    \# 可以直接使用了(It's ready to use!)
    **return** the_wrapper_around_the_original_function
@my_shiny_new_decorator
**def** another_stand_alone_function():
    print("Leave me alone")
another_stand_alone_function()
\# outputs:
\# Before the function runs
\# Leave me alone
\# After the function runs      

是的,這就完了,就這麼簡單。@decorator 隻是下面這條語句的簡寫(shortcut):

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)      

裝飾器文法糖其實就是裝飾器模式的一個Python化的變體。為了友善開發,Python已經内置了好幾種經典的設計模式,比如疊代器(iterators)。當然,你還可以堆積使用裝飾器:

def bread(func):
    **def** wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    **return** wrapper
**def** ingredients(func):
    **def** wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    **return** wrapper
**def** sandwich(food="--ham--"):
    print(food)
sandwich()
\# outputs: --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
\# outputs:
\# </''''''\>
\# #tomatoes#
\# --ham--
\# ~salad~
\# <\______/>      

用Python的裝飾器文法表示:

def bread(func):
    **def** wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    **return** wrapper
**def** ingredients(func):
    **def** wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    **return** wrapper
@bread
@ingredients
**def** sandwich(food="--ham--"):
    print(food)
sandwich()
\# outputs:
\# </''''''\>
\# #tomatoes#
\# --ham--
\# ~salad~
\# <\______/>      

裝飾器放置的順序也很重要:

def bread(func):
    **def** wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    **return** wrapper
**def** ingredients(func):
    **def** wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    **return** wrapper
@ingredients
@bread
**def** strange_sandwich(food="--ham--"):
    print(food)
strange_sandwich()
\# outputs:
\##tomatoes#
\# </''''''\>
\# --ham--
\# <\______/>
\# ~salad~      

給裝飾器函數傳參

\# 這不是什麼黑色魔法(black magic),你隻是必須讓wrapper傳遞參數:
**def** a_decorator_passing_arguments(function_to_decorate):
    **def** a_wrapper_accepting_arguments(arg1, arg2):
        print("I got args! Look:", arg1, arg2)
        function_to_decorate(arg1, arg2)
    **return** a_wrapper_accepting_arguments
\# 當你調用裝飾器傳回的函數式,你就在調用wrapper,而給wrapper的
\# 參數傳遞将會讓它把參數傳遞給要裝飾的函數
@a_decorator_passing_arguments
**def** print_full_name(first_name, last_name):
    print("My name is", first_name, last_name)
print_full_name("Peter", "Venkman")
\# outputs:
\# I got args! Look: Peter Venkman
\# My name is Peter Venkman      

含參數的裝飾器

在上面的裝飾器調用中,比如@decorator,該裝飾器預設它後面的函數是唯一的參數。裝飾器的文法允許我們調用decorator時,提供其它參數,比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。

\# a new wrapper layer
**def** pre_str(pre=''):
    \# old decorator
    **def** decorator(F):
        **def** new_F(a, b):
            print(pre + " input", a, b)
            **return** F(a, b)
        **return** new_F
    **return** decorator
\# get square sum
@pre_str('^_^')
**def** square_sum(a, b):
    **return** a ** 2 + b ** 2
\# get square diff
@pre_str('T_T')
**def** square_diff(a, b):
    **return** a ** 2 - b ** 2
print(square_sum(3, 4))
print(square_diff(3, 4))
\# outputs:
\# ('^_^ input', 3, 4)
\# 25
\# ('T_T input', 3, 4)
\# -7      

上面的pre_str是允許參數的裝飾器。它實際上是對原有裝飾器的一個函數封裝,并傳回一個裝飾器。我們可以将它了解為一個含有環境參量的閉包。當我們使用@pre_str(‘^_^’)調用的時候,Python能夠發現這一層的封裝,并把參數傳遞到裝飾器的環境中。該調用相當于:

square_sum = pre_str('^_^') (square_sum)      

裝飾“類中的方法”

Python的一個偉大之處在于:方法和函數幾乎是一樣的(methods and functions are really the same),除了方法的第一個參數應該是目前對象的引用(也就是 self)。這也就意味着隻要記住把 self 考慮在内,你就可以用同樣的方法給方法建立裝飾器:

def method_friendly_decorator(method_to_decorate):
    **def** wrapper(self, lie):
        lie = lie - 3  # very friendly, decrease age even more :-)
        **return** method_to_decorate(self, lie)
    **return** wrapper
**class** Lucy(object):
    **def** __init__(self):
        self.age = 32
    @method_friendly_decorator
    **def** say_your_age(self, lie):
        print("I am %s, what did you think?" % (self.age + lie))
l = Lucy()
l.say_your_age(-3)
\# outputs: I am 26, what did you think?      

當然,如果你想編寫一個非常通用的裝飾器,可以用來裝飾任意函數和方法,你就可以無視具體參數了,直接使用 *args, **kwargs 就行:

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    \# The wrapper accepts any arguments
    **def** a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print("Do I have args?:")
        print(args)
        print(kwargs)
        \# Then you unpack the arguments, here *args, **kwargs
        \# If you are not familiar with unpacking, check:
        \# http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
        function_to_decorate(*args, **kwargs)
    **return** a_wrapper_accepting_arbitrary_arguments
@a_decorator_passing_arbitrary_arguments
**def** function_with_no_argument():
    print("Python is cool, no argument here.")
function_with_no_argument()
\# outputs
\# Do I have args?:
\# ()
\# {}
\# Python is cool, no argument here.
@a_decorator_passing_arbitrary_arguments
**def** function_with_arguments(a, b, c):
    print(a, b, c)
function_with_arguments(1, 2, 3)
\# outputs
\# Do I have args?:
\# (1, 2, 3)
\# {}
\# 1 2 3
@a_decorator_passing_arbitrary_arguments
**def** function_with_named_arguments(a, b, c, platypus="Why not ?"):
    print("Do %s, %s and %s like platypus? %s" % (a, b, c, platypus))
function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
\# outputs
\# Do I have args ? :
\# ('Bill', 'Linus', 'Steve')
\# {'platypus': 'Indeed!'}
\# Do Bill, Linus and Steve like platypus? Indeed!
**class** Mary(object):
    **def** __init__(self):
        self.age = 31
    @a_decorator_passing_arbitrary_arguments
    **def** say_your_age(self, lie=-3):  # You can now add a default value
        print("I am %s, what did you think ?" % (self.age + lie))
m = Mary()
m.say_your_age()
\# outputs
\# Do I have args?:
\# (<__main__.Mary object at 0xb7d303ac>,)
\# {}
\# I am 28, what did you think?      

裝飾類

在上面的例子中,裝飾器接收一個函數,并傳回一個函數,進而起到加工函數的效果。在Python 2.6以後,裝飾器被拓展到類。一個裝飾器可以接收一個類,并傳回一個類,進而起到加工類的效果。

def decorator(aClass):
    **class** newClass:
        **def** __init__(self, age):
            self.total_display = 0
            self.wrapped = aClass(age)
        **def** display(self):
            self.total_display += 1
            print("total display", self.total_display)
            self.wrapped.display()
    **return** newClass
@decorator
**class** Bird:
    **def** __init__(self, age):
        self.age = age
    **def** display(self):
        print("My age is", self.age)
eagleLord = Bird(5)
**for** i **in** range(3):
    eagleLord.display()      

在decorator中,我們傳回了一個新類newClass。在新類中,我們記錄了原來類生成的對象(self.wrapped),并附加了新的屬性total_display,用于記錄調用display的次數。我們也同時更改了display方法。通過修改,我們的Bird類可以顯示調用display的次數了。

内置裝飾器

Python中有三種我們經常會用到的裝飾器, property、 staticmethod、 classmethod,他們有個共同點,都是作用于類方法之上。

property 裝飾器

property 裝飾器用于類中的函數,使得我們可以像通路屬性一樣來擷取一個函數的傳回值。

class XiaoMing:
    first_name = '明'
    last_name = '小'
    @property
    **def** full_name(self):
        **return** self.last_name + self.first_name
xiaoming = XiaoMing()
print(xiaoming.full_name)      

例子中我們像擷取屬性一樣擷取 full_name 方法的傳回值,這就是用 property 裝飾器的意義,既能像屬性一樣擷取值,又可以在擷取值的時候做一些操作。

staticmethod 裝飾器

staticmethod 裝飾器同樣是用于類中的方法,這表示這個方法将會是一個靜态方法,意味着該方法可以直接被調用無需執行個體化,但同樣意味着它沒有 self 參數,也無法通路執行個體化後的對象。

class XiaoMing:
    @staticmethod
    **def** say_hello():
        print('同學你好')
XiaoMing.say_hello()
\# 執行個體化調用也是同樣的效果
\# 有點多此一舉
xiaoming = XiaoMing()
xiaoming.say_hello()      

classmethod 裝飾器

classmethod 依舊是用于類中的方法,這表示這個方法将會是一個類方法,意味着該方法可以直接被調用無需執行個體化,但同樣意味着它沒有 self 參數,也無法通路執行個體化後的對象。相對于 staticmethod 的差別在于它會接收一個指向類本身的 cls 參數。

class XiaoMing:
    name = '小明'
    @classmethod
    **def** say_hello(cls):
        print('同學你好, 我是' + cls.name)
        print(cls)
XiaoMing.say_hello()      

wraps 裝飾器

一個函數不止有他的執行語句,還有着 name(函數名),doc (說明文檔)等屬性,我們之前的例子會導緻這些屬性改變。

def decorator(func):
    **def** wrapper(*args, **kwargs):
        """doc of wrapper"""
        print('123')
        **return** func(*args, **kwargs)
    **return** wrapper
@decorator
**def** say_hello():
    """doc of say hello"""
    print('同學你好')
print(say_hello.__name__)
print(say_hello.__doc__)      

由于裝飾器傳回了 wrapper 函數替換掉了之前的 say_hello 函數,導緻函數名,幫助文檔變成了 wrapper 函數的了。解決這一問題的辦法是通過 functools 子產品下的 wraps 裝飾器。

from functools import wraps
**def** decorator(func):
    @wraps(func)
    **def** wrapper(*args, **kwargs):
        """doc of wrapper"""
        print('123')
        **return** func(*args, **kwargs)
    **return** wrapper
@decorator
**def** say_hello():
    """doc of say hello"""
    print('同學你好')
print(say_hello.__name__)
print(say_hello.__doc__)      

裝飾器總結

裝飾器的核心作用是name binding。這種文法是Python多程式設計範式的又一個展現。大部分Python使用者都不怎麼需要定義裝飾器,但有可能會使用裝飾器。鑒于裝飾器在Python項目中的廣泛使用,了解這一文法是非常有益的。

常見錯誤:“裝飾器”=“裝飾器模式”

設計模式是一個在計算機世界裡鼎鼎大名的詞。假如你是一名 Java 程式員,而你一點設計模式都不懂,那麼我打賭你找工作的面試過程一定會度過的相當艱難。

但寫 Python 時,我們極少談起“設計模式”。雖然 Python 也是一門支援面向對象的程式設計語言,但它的鴨子類型設計以及出色的動态特性決定了,大部分設計模式對我們來說并不是必需品。是以,很多 Python 程式員在工作很長一段時間後,可能并沒有真正應用過幾種設計模式。

不過裝飾器模式(Decorator Pattern)是個例外。因為 Python 的“裝飾器”和“裝飾器模式”有着一模一樣的名字,我不止一次聽到有人把它們倆當成一回事,認為使用“裝飾器”就是在實踐“裝飾器模式”。但事實上,它們是兩個完全不同的東西。

“裝飾器模式”是一個完全基于“面向對象”衍生出的程式設計手法。它擁有幾個關鍵組成:一個統一的接口定義、若幹個遵循該接口的類、類與類之間一層一層的包裝。最終由它們共同形成一種“裝飾”的效果。

而 Python 裡的“裝飾器”和“面向對象”沒有任何直接聯系,**它完全可以隻是發生在函數和函數間的把戲。事實上,“裝飾器”并沒有提供某種無法替代的功能,它僅僅就是一顆“文法糖”而已。下面這段使用了裝飾器的代碼:

@log_time
@cache_result
**def** foo(): pass      

基本完全等同于:

def foo(): pass
foo = log_time(cache_result(foo))      

裝飾器最大的功勞,在于讓我們在某些特定場景時,可以寫出更符合直覺、易于閱讀的代碼。它隻是一顆“糖”,并不是某個面向對象領域的複雜程式設計模式。

參考連結:

  • Primer on Python Decorators
  • [Decorator Basics:Python’s functions are objects]