Python裝飾器學習筆記

裝飾器(Decorators)

裝飾器是 Python 的一個重要部分。它是修改其餘函數的功能的函數,有助於讓咱們的代碼更簡短html

 

裝飾器本質上是一個Python函數,它可讓其餘函數在不須要作任何代碼變更的前提下增長額外功能,裝飾器的返回值也是一個函數對象。它常常用於有切面需求的場景,好比:插入日誌、性能測試、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,咱們就能夠抽離出大量與函數功能自己無關的雷同代碼並繼續重用。python

 

歸納的講,裝飾器的做用就是爲已經存在的函數或對象添加額外的功能程序員

爲何須要裝飾器

咱們假設你的程序實現了func_enter()func_quit()兩個函數。緩存

def func_enter():
print "enter!"


def func_quit():
print "enter!" # bug here


if __name__ == '__main__':
func_enter()
func_quit()

運行結果:bash

enter!
enter!
(wda_python) bash-3.2$ 

可是在實際調用中, 咱們發現程序出錯了, 上面打印了2個enter。通過調試咱們發現是func_quit()出錯了app

如今假如要求調用每一個方法前都要記錄進入函數的名稱, 好比這樣:函數

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!

一種最直白簡單的方式是這樣寫:性能

def func_enter():
    print "[DEBUG]: enter func_enter()"
    print "enter!"


def func_quit():
    print "[DEBUG]: enter func_quit()"
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

可是很low對吧, 咱們能夠試着這樣寫:測試

def debug():
    import inspect
    caller_name = inspect.stack()[1][3]
    print '[BEBUG]: enter {}()'.format(caller_name)

def func_enter():
    debug()
    print "enter!"


def func_quit():
    debug()
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

看起來會好一點, 可是每一個函數都要調用一次debug()函數,仍是不太夠, 萬一若是又改需求進出不打印調用者了, 其餘地方或者函數在打印, 又要大改優化

怎麼辦呢? 這個時候裝飾器就能夠派上用場了

 

怎麼寫一個裝飾器

咱們來看一個例子

def debug(func):
    def wrapper():
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func()
    return wrapper

@debug
def func_enter():
    print "enter!"

@debug
def func_quit():
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

運行結果:

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!
(wda_python) bash-3.2$ 

這是一個最簡單的裝飾器, 可是有個問題, 若是被裝飾的函數須要傳入參數, 那麼這個裝飾器就壞了,由於返回的函數並不能接受參數

這裏能夠指定裝飾器函數wrapper接受和原函數同樣的參數, 好比:

#coding: utf-8

def debug(func):
    def wrapper(something):     # 這裏指定同樣的參數
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(something)
    return wrapper # 返回包裝過的函數

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

運行結果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!

這樣解決了傳參數的問題, 可是這裏有個很大的問題是這裏只適配了咱們的func_enter和func_quit函數的參數, 若是要用來去裝飾其餘帶參數的函數呢?

還好python提供可變參數*args和關鍵字參數**kwargs, 有這兩個參數裝飾器就能夠用於任意目標函數了

#coding: utf-8

def debug(func):
    def wrapper(*args, **kwargs):     # 這裏指定同樣的參數
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(*args, **kwargs)
    return wrapper # 返回包裝過的函數

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

運行結果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

帶參數的裝飾器

若是前面咱們的裝飾器須要完成的功能不只僅是能在進入某個函數後打印出調用信息,還要指定log級別, 那麼裝飾器能夠是這樣:

#coding: utf-8

def debug(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print '[{level}]: enter {func}()'.format(level=level,func=func.__name__)
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@debug(level='Debug')
def func_enter(something):
    print "enter {}!".format(something)

@debug(level='Debug')
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

運行結果:

[Debug]: enter func_enter()
enter enter_func!
[Debug]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

基於類實現的裝飾器

裝飾器函數實際上是這樣一個接口約束,它必須接受一個callable對象做爲參數,而後返回一個callable對象。在Python中通常callable對象都是函數,但也有例外。只要某個對象重載了__call__()方法,那麼這個對象就是callable的。

class Test():
    def __call__(self, *args, **kwargs):
        print 'call me!'

t = Test()
t()

運行結果:

call me!
(wda_python) bash-3.2$ 

__call__這樣先後都帶下劃線的方法在Python中被稱爲內置方法,有時候也被稱爲魔法方法。重載這些魔法方法通常會改變對象的內部行爲。上面這個例子就讓一個類對象擁有了被調用的行爲。

 

回到裝飾器上的概念上來,裝飾器要求接受一個callable對象,並返回一個callable對象(不太嚴謹,詳見後文)。那麼用類來實現也是也能夠的。咱們可讓類的構造函數__init__()接受一個函數,而後重載__call__()並返回一個函數,也能夠達到裝飾器函數的效果。

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

    def __call__(self, *args, **kwargs):
        print "[DEBUG]: enter function {func}()".format(func=self.func.__name__)
        return self.func(*args, **kwargs)

@Debug_info
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

運行結果:

[DEBUG]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

帶參數的類裝飾器

若是須要經過類形式實現帶參數的裝飾器,那麼會比前面的例子稍微複雜一點。那麼在構造函數裏接受的就不是一個函數,而是傳入的參數。經過類把這些參數保存起來。而後在重載__call__方法是就須要接受一個函數並返回一個函數。

#coding: utf-8

class Debug_info(object):
    def __init__(self, level='INFO'):
        self.level= level

    def __call__(self, func):  # 接受函數
        def wrapper(*args, **kwargs):
            print "[{level}]: enter function {func}()".format(level=self.level,func=func.__name__)
            func(*args, **kwargs)
        return wrapper

@Debug_info(level='INFO')
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

運行結果:

[INFO]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

內置的裝飾器

在綁定屬性時,若是咱們直接把屬性暴露出去,雖然寫起來很簡單,可是,沒辦法檢查參數,致使能夠把成績隨便改:

s = Student()
s.score = 9999

這顯然不合邏輯。爲了限制score的範圍,能夠經過一個set_score()方法來設置成績,再經過一個get_score()來獲取成績,這樣,在set_score()方法裏,就能夠檢查參數:

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

如今,對任意的Student實例進行操做,就不能爲所欲爲地設置score了:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

可是,上面的調用方法又略顯複雜,沒有直接用屬性這麼直接簡單。

有沒有既能檢查參數,又能夠用相似屬性這樣簡單的方式來訪問類的變量呢?對於追求完美的Python程序員來講,這是必需要作到的!

還記得裝飾器(decorator)能夠給函數動態加上功能嗎?對於類的方法,裝飾器同樣起做用。Python內置的@property裝飾器就是負責把一個方法變成屬性調用的:

 

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property的實現比較複雜,咱們先考察如何使用。把一個getter方法變成屬性,只須要加上@property就能夠了,此時,@property自己又建立了另外一個裝飾器@score.setter,負責把一個setter方法變成屬性賦值,因而,咱們就擁有一個可控的屬性操做:

>>> s = Student()
>>> s.score = 60 # OK,實際轉化爲s.set_score(60)
>>> s.score # OK,實際轉化爲s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

注意到這個神奇的@property,咱們在對實例屬性操做的時候,就知道該屬性極可能不是直接暴露的,而是經過getter和setter方法來實現的。

還能夠定義只讀屬性,只定義getter方法,不定義setter方法就是一個只讀屬性:

 

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的birth是可讀寫屬性,而age就是一個只讀屬性,由於age能夠根據birth和當前時間計算出來。

@property普遍應用在類的定義中,可讓調用者寫出簡短的代碼,同時保證對參數進行必要的檢查,這樣,程序運行時就減小了出錯的可能性。

 

裝飾器裏的那些坑

裝飾器可讓你代碼更加優雅,減小重複,但也不全是優勢,也會帶來一些問題。

位置錯誤的代碼

讓咱們直接看示例代碼:

def html_tags(tag_name):
    print 'begin outer function.'
    def wrapper_(func):
        print "begin of inner wrapper function."
        def wrapper(*args, **kwargs):
            content = func(*args, **kwargs)
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content)
        print 'end of inner wrapper function.'
        return wrapper
    print 'end of outer function'
    return wrapper_

@html_tags('b')
def hello(name='Toby'):
    return 'Hello {}!'.format(name)

hello()
hello()

在裝飾器中我在各個可能的位置都加上了print語句,用於記錄被調用的狀況。你知道他們最後打印出來的順序嗎?若是你內心沒底,那麼最好不要在裝飾器函數以外添加邏輯功能,不然這個裝飾器就不受你控制了。如下是輸出結果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Toby!</b>
<b>Hello Toby!</b>
(wda_python) bash-3.2$ 

 

錯誤的函數簽名和文檔

裝飾器裝飾過的函數看上去名字沒變,其實已經變了。

import datetime

def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
return wrapper

@logging
def say(something):
"""say something"""
print "say {}!".format(something)

print say.__name__
print say.__doc__

運行結果:

wrapper
print log before a function.
(wda_python) bash-3.2$ 

 

爲何會這樣呢?只要你想一想裝飾器的語法糖@代替的東西就明白了。@等同於這樣的寫法。

say = logging(say)

logging其實返回的函數名字恰好是wrapper,那麼上面的這個語句恰好就是把這個結果賦值給saysay__name__天然也就是wrapper了,不只僅是name,其餘屬性也都是來自wrapper,好比docsource等等。

使用標準庫裏的functools.wraps,能夠基本解決這個問題。

import datetime
from functools import wraps

def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

運行結果:

say
say something
(wda_python) bash-3.2$ 

可是其實還不太完美, 由於函數的簽名和源碼仍是拿不到

import datetime
from functools import wraps
def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

import inspect
print inspect.getargspec(say)
print inspect.getsource(say)

運行結果:

say
say something
ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)

(wda_python) bash-3.2$ 

若是要完全解決這個問題能夠借用第三方包,好比wrapt, 後文有介紹

 

不能裝飾@staticmethod 或者 @classmethod

當你想把裝飾器用在一個靜態方法或者類方法時,很差意思,報錯了。

class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 裝飾實例方法,OK
    def run(self):
        print "{} is running!".format(self.model)

    @logging  # 裝飾靜態方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print "The model of your car is {}".format(obj.model)
        else:
            print "{} is not a car!".format(obj)

"""
Traceback (most recent call last):
...
  File "example_4.py", line 10, in logging
    @wraps(func)
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'staticmethod' object has no attribute '__module__'
"""

前面已經解釋了@staticmethod這個裝飾器,其實它返回的並非一個callable對象,而是一個staticmethod對象,那麼它是不符合裝飾器要求的(好比傳入一個callable對象),你天然不能在它之上再加別的裝飾器。要解決這個問題很簡單,只要把你的裝飾器放在@staticmethod以前就行了,由於你的裝飾器返回的仍是一個正常的函數,而後再加上一個@staticmethod是不會出問題的。

class Car(object):
    def __init__(self, model):
        self.model = model

    @staticmethod
    @logging  # 在@staticmethod以前裝飾,OK
    def check_model_for(obj):
        pass

 

如何優化你的裝飾器

嵌套的裝飾函數不太直觀,咱們可使用第三方包類改進這樣的狀況,讓裝飾器函數可讀性更好。

decorator.py

decorator.py是一個很是簡單的裝飾器增強包。你能夠很直觀的先定義包裝函數wrapper(),再使用decorate(func, wrapper)方法就能夠完成一個裝飾器。

from decorator import decorate

def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

def logging(func):
    return decorate(func, wrapper)  # 用wrapper裝飾func

你也可使用它自帶的@decorator裝飾器來完成你的裝飾器。

from decorator import decorator

@decorator
def logging(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

decorator.py實現的裝飾器能完整保留原函數的namedocargs,惟一有問題的就是inspect.getsource(func)返回的仍是裝飾器的源代碼,你須要改爲inspect.getsource(func.__wrapped__)

 

wrapt

wrapt是一個功能很是完善的包,用於實現各類你想到或者你沒想到的裝飾器。使用wrapt實現的裝飾器你不須要擔憂以前inspect中遇到的全部問題,由於它都幫你處理了,甚至inspect.getsource(func)也準確無誤。

import wrapt

# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print "[DEBUG]: enter {}()".format(wrapped.__name__)
    return wrapped(*args, **kwargs)

@logging
def say(something): pass

使用wrapt你只須要定義一個裝飾器函數,可是函數簽名是固定的,必須是(wrapped, instance, args, kwargs),注意第二個參數instance是必須的,就算你不用它。當裝飾器裝飾在不一樣位置時它將獲得不一樣的值,好比裝飾在類實例方法時你能夠拿到這個類實例。根據instance的值你可以更加靈活的調整你的裝飾器。另外,argskwargs也是固定的,注意前面沒有星號。在裝飾器內部調用原函數時才帶星號。

若是你須要使用wrapt寫一個帶參數的裝飾器,能夠這樣寫:

def logging(level):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print "[{}]: enter {}()".format(level, wrapped.__name__)
        return wrapped(*args, **kwargs)
    return wrapper

@logging(level="INFO")
def do(work): pass

關於wrapt的使用,建議查閱官方文檔,在此不在贅述。

  • http://wrapt.readthedocs.io/en/latest/quick-start.html
相關文章
相關標籤/搜索