python裝飾器入門與提高

本文轉自http://mingxinglai.com/cn/2015/08/python-decorator/


[TOC]

1. 介紹

Python 2.2 開始提供了裝飾器(decorator),裝飾器作為修改函數的一種便捷方式,為程序員編寫程序提供了便利性和靈活性,適當使用裝飾器,能夠有效的提高代碼的可讀性和可維護性,然而,裝飾器并沒有被廣泛的使用,主要還是因為大多數人并不理解裝飾器的工作機制。
本文首先介紹了裝飾器的概念和用法(第2節),然后介紹了裝飾器使用過程中的注意事項(第3節),之后討論了裝飾器的使用場景和注意是想(第4節),最后提供了一些裝飾器的學習素材(第5節)。

2. 裝飾器

裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換。概念比較抽象,一起來看兩個裝飾器的例子。

1. 裝飾器的概念

考慮這樣一組函數,它們在被調用時需要對某些參數進行檢查,在本例中,需要對用戶名進行檢查,以判斷用戶是否有相應的權限進行某些操作。
程序清單1

class Store(object):
    def get_food(self, username, food):
        if username != 'admin':
            raise Exception("This user is not allowed to get food")
        return self.storage.get(food)

    def put_food(self, username, food):
        if username != 'admin':
            raise Exception("This user is not allowed to put food")
        self.storage.put(food)

顯然,代碼有重復,作為一個有追求的工程師,我們嚴格遵守DRY(Don’t repeat yourself)原則,于是,代碼被改寫成了程序清單2這樣:
程序清單2

def check_is_admin(username):
    if username != 'admin':
        raise Exception("This user is not allowed to get food")

class Store(object):
    def get_food(self, username, food):
        check_is_admin(username)
        return self.storage.get(food)

    def put_food(self, username, food):
        check_is_admin(username)
        return self.storage.put(food)

現在代碼整潔一點了,但是,有裝飾器能夠做的更好:
程序清單3

def check_is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*arg, **kargs)
    return wrapper

class Storage(object):
    @check_is_admin
    def get_food(self, username, food):
        return self.storage.get(food)

    @check_is_admin
    def put_food(self, username, food):
        return storage.put(food)

上面這段代碼就是使用裝飾器的典型例子:函數里面定義了一個函數,并將定義的這個函數作為返回值。這個例子足夠簡單,所以它的好處也不夠明顯,但是,卻可以很好的演示裝飾器的語法。
即使這樣,我們也可以說,程序清單3比程序清單2更好,因為程序清單3能夠將條件檢查與具體邏輯分隔開來。在本例中,check_is_admin只是預檢查,它的重要性不如具體的業務邏輯。我們將業務邏輯看做是這段程序的”重點”的話,那么,程序清單3一眼看過去就能看到”重點”,而程序清單2則不能,需要簡單的思考(轉個彎)才能區分條件檢查和業務邏輯。當然,你可能覺得這沒什么,但是,作為一名有追求的工程師,我們希望我們寫出的代碼能和散文一樣優美。

2. 裝飾器的本質

前面說過,裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換。下面這個例子能夠更好地理解這句話。
程序清單4

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "</______\>"
    return wrapper

sandwich_copy = bread(sandwich)
sandwich_copy()

輸出結果如下:

</''''''\>
--ham--
</______\>

分析如下: bread是一個函數,它接受一個函數作為參數,然后返回一個新的函數,新的函數對原來的函數進行了一些修改和擴展,且這個新函數可以當做普通函數進行調用。
下面這段代碼和程序清單4輸出結果一摸一樣,只是用了python提供的裝飾器語法,看起來更加簡單直接。
程序清單5

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "</______\>"
    return wrapper

@bread
def sandwich(food="--ham--"):
    print food

到這里,我們應該已經能夠理解裝飾器的作用和用法了,再強調一遍:裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換

3. 使用裝飾器需要注意的地方

我們在上一節中演示了裝飾器的用法,可以看到,裝飾器其實很好理解,也非常簡單。但是,要用好裝飾器,還有一些我們需要注意的地方,這一節就對這些需要注意的地方進行了討論,首先討論了在使用裝飾器的情況下,如何保留原有函數的屬性(見3.1節);然后討論了如何實現一個更加智能的裝飾器;之后討論了使用多個裝飾器時,各個裝飾器的調用順序(見3.3節);最后說明了如何給裝飾器傳遞參數(見3.4節)。

1. 函數的屬性變化

裝飾器動態創建的新函數替換原來的函數,但是,新函數缺少很多原函數的屬性,如docstring和名字。
程序清單6

def is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper

def foobar(username='someone'):
    """Do crazy stuff"""
    pass

@is_admin
def barfoo(username='someone'):
    """Do crazy stuff"""
    pass

def main():
    print foobar.func_doc
    print foobar.__name__

    print barfoo.func_doc
    print barfoo.__name__

if __name__ == '__main__':
    main()

程序清單6的輸出結果:

Do crazy stuff
foobar

None
wrapper

程序清單6中,我們定義了兩個函數foobar與barfoo,其中,barfoo使用裝飾器進行了封裝,我們獲取foobar與barfoo的docstring和函數名字,可以看到,使用了裝飾器的函數,不能夠正確獲取函數原有的docstring與名字,為了解決這個問題,可以使用python內置的 functools模塊。
程序清單7

import functools

def is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*arg, **kwargs)
    return wrapper

我們只需要增加一行代碼,就能夠正確地獲取函數的屬性。
此外,我們也可以向下面這樣:

def is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return functools.wraps(f)(wrapper) # important

當然,個人推薦第一種方法,因為第一種方法可讀性更強。

2. 使用inspect獲取函數參數

下面看一下程序清單8,它是否會正確輸出結果呢?
程序清單8

import functools

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        print kwargs
        if kwargs.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper


@check_is_admin
def get_food(username, food='chocolate'):
    return "{0} get food: {1}".format(username, food)


def main():
    print get_food('admin')

if __name__ == '__main__':
    main()

程序清單8會拋出一個異常,因為我們傳入的’admin’是一個位置參數,而我們卻去關鍵字參數(kwargs)獲取用戶名,因此,`kwargs.get(‘username’)返回None,那么,權限檢查發現,用戶沒有相應的權限,拋出異常。
為了提供一個更加智能的裝飾器,我們需要使用python的inspect模塊。inspect能夠取出函數的簽名,并對其進行操作,如下所示:
程序清單9

import functools
import inspect

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(f, *args, **kwargs)
        print func_args
        if func_args.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper


@check_is_admin
def get_food(username, food='chocolate'):
    return "{0} get food: {1}".format(username, food)


def main():
    print get_food('admin')

if __name__ == '__main__':
    main()

承擔主要工作的函數是inspect.getcallargs,它返回一個將參數名字和值作為鍵值對的字典,這程序清單7中,這個函數返回{'username':'admin','food':'chocolate'}。這意味著我們的裝飾器不必檢查參數username是基于位置的參數還是基于關鍵字的參數,而只需在字典中查找即可。

3. 多個裝飾器的調用順序

多個裝飾器的調用順序也很好理解,我們一stackoverflow上的這個問題為例進行說明。
問題

How can I make two decorators in Python that would do the following?
@makebold
@makeitalic
def say():
return "Hello"
which should return
<b><i>Hello</i></b>

答案

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

print hello() ## returns <b><i>hello world</i></b>

分析
我們在2.2節說過,裝飾器就是在外層進行了封裝:

@bread
sandwich()

sandwich_copy = bread(sandwich)

那么,封裝兩層可以像這樣理解:

@makebold
@makeitalic
hello()

hello-copy = makebold(makeitalic(helo))

因此,這樣理解以后,對于多個裝飾器的調用順序,就不再有疑問了。

4. 給裝飾器傳遞參數

下面通過一個官方的例子來看如何給裝飾器傳遞參數。官方介紹了一個非常有用的裝飾器,即設置超時器。如果函數調用超時,則拋出異常。
程序清單10

def timeout(seconds, error_message = 'Function call timed out'):
   def decorated(func):
       def _handle_timeout(signum, frame):
           raise TimeoutError(error_message)

       def wrapper(*args, **kwargs):
           signal.signal(signal.SIGALRM, _handle_timeout)
           signal.alarm(seconds)
           try:
               result = func(*args, **kwargs)
           finally:
               signal.alarm(0)
           return result

       return functools.wraps(func)(wrapper)

   return decorated

使用方法如下:

import time

@timeout(1, 'Function slow; aborted')
def slow_function():
    time.sleep(5)

對應于我們這篇博客一直討論的例子,傳遞參數的代碼如下所示:
程序清單11

def is_admin(admin='admin'):
    def decorated(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            if kwargs.get("username") != admin:
                raise Exception("This user is not allowed to get food")
            return f(*args, **kwargs)
        return wrapper
    return decorated


@is_admin(admin='root')
def barfoo(username='someone'):
    """Do crazy stuff"""
    print '{0} get food'.format(username)


if __name__ == '__main__':
    barfoo(username='root')

4. 裝飾器的使用場景與缺點

1. 裝飾器的使用場景

裝飾器雖然語法比較復雜,但是,在一些場景下,也確實比較有用。包括:
注入參數(提供默認參數,生成參數)
記錄函數行為(日志、緩存、計時什么的)
預處理/后處理(配置上下文什么的)
修改調用時的上下文(線程異步或者并行,類方法)
下面這個例子演示了上面提到的3中情況,如下所示:
程序清單12

def benchmark(func):
    """
    A decorator that prints the time a function takes
    to execute.
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print func.__name__, time.clock()-t
        return res
    return wrapper


def logging(func):
    """
    A decorator that logs the activity of the script.
    (it actually just prints it, but it could be logging!)
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print func.__name__, args, kwargs
        return res
    return wrapper


def counter(func):
    """
    A decorator that counts and prints the number of times a function has been executed
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

關于裝飾器的例子,官方列出了一個長長的列表,這里很多代碼可以直接拿來使用,如果需要詳細地了解裝飾器的使用場景,可以學習一下這份列表。

2. 裝飾器有哪些缺點

在我們目前的實際項目中,裝飾器使用還不夠多,所以沒有積累很多的經驗,下面是國外大神給出的裝飾器的缺點,以供參考:

Decorators were introduced in Python 2.4, so be sure your code will be run on >= 2.4.
Decorators slow down the function call. Keep that in mind.
You cannot un-decorate a function. (There are hacks to create decorators that can be removed, but nobody uses them.) So once a function is decorated, it’s decorated for all the code.
Decorators wrap functions, which can make them hard to debug.

5. 其他學習資料

本文較為全面地介紹了裝飾器的用法,也給出了裝飾器的使用場景和缺點。如果還需要進一步的學習裝飾器,可以了解一下下面這幾份資料:
python decorator library
source code of flask
Magic decorator syntax for asynchronous code in Python
A Python decorator that helps ensure that a Python Process is running only once

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • 本文為《爬著學Python》系列第四篇文章。從本篇開始,本專欄在順序更新的基礎上,會有不規則的更新。 在Pytho...
    SyPy閱讀 2,514評論 4 11
  • http://python.jobbole.com/85231/ 關于專業技能寫完項目接著寫寫一名3年工作經驗的J...
    燕京博士閱讀 7,606評論 1 118
  • 要點: 函數式編程:注意不是“函數編程”,多了一個“式” 模塊:如何使用模塊 面向對象編程:面向對象的概念、屬性、...
    victorsungo閱讀 1,552評論 0 6
  • Python進階框架 希望大家喜歡,點贊哦首先感謝廖雪峰老師對于該課程的講解 一、函數式編程 1.1 函數式編程簡...
    Gaolex閱讀 5,515評論 6 53
  • 鵬程萬里 鵬鯤展翅行萬里, 程門立雪苦讀書。 萬卷詩書終受益, 里內里外人稱許。 注1:鵬鯤即鯤鵬,古代傳說中的大...
    亮靚_27d5閱讀 234評論 20 38