1. 程式人生 > >【Python】【裝飾器】

【Python】【裝飾器】

Python中的裝飾器是你進入Python大門的一道坎,不管你跨不跨過去它都在那裡。

為什麼需要裝飾器
我們假設你的程式實現了say_hello()和say_goodbye()兩個函式。

def say_hello():
print "hello!"

def say_goodbye():
print "hello!" # bug here

if name == 'main':
say_hello()
say_goodbye()
但是在實際呼叫中,我們發現程式出錯了,上面的程式碼列印了兩個hello。經過除錯你發現是say_goodbye()出錯了。老闆要求呼叫每個方法前都要記錄進入函式的名稱,比如這樣:

Hello!
DEBUG: Enter say_goodbye()
Goodbye!
好,小A是個畢業生,他是這樣實現的。

def say_hello():
print "DEBUG: enter say_hello()"
print "hello!"

def say_goodbye():
print "DEBUG: enter say_goodbye()"
print "hello!"

if name == 'main':
say_hello()
say_goodbye()
很low吧? 嗯是的。小B工作有一段時間了,他告訴小A可以這樣寫。

def debug():
import inspect
caller_name = inspect.stack()[1][3]
print "DEBUG

: enter {}()".format(caller_name)

def say_hello():
debug()
print "hello!"

def say_goodbye():
debug()
print "goodbye!"

if name == 'main':
say_hello()
say_goodbye()
是不是好一點?那當然,但是每個業務函式裡都要呼叫一下debug()函式,是不是很難受?萬一老闆說say相關的函式不用debug,do相關的才需要呢?

那麼裝飾器這時候應該登場了。

裝飾器本質上是一個Python函式,它可以讓其他函式在不需要做任何程式碼變動的前提下增加額外功能,裝飾器的返回值也是一個函式物件。它經常用於有切面需求的場景,比如:插入日誌、效能測試、事務處理、快取、許可權校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同程式碼並繼續重用。

概括的講,裝飾器的作用就是為已經存在的函式或物件新增額外的功能。

怎麼寫一個裝飾器
在早些時候 (Python Version < 2.4,2004年以前),為一個函式新增額外功能的寫法是這樣的。

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

def say_hello():
print "hello!"

say_hello = debug(say_hello) # 新增功能並保持原函式名不變
上面的debug函式其實已經是一個裝飾器了,它對原函式做了包裝並返回了另外一個函式,額外添加了一些功能。因為這樣寫實在不太優雅,在後面版本的Python中支援了@語法糖,下面程式碼等同於早期的寫法。

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

@debug
def say_hello():
print "hello!"
這是最簡單的裝飾器,但是有一個問題,如果被裝飾的函式需要傳入引數,那麼這個裝飾器就壞了。因為返回的函式並不能接受引數,你可以指定裝飾器函式wrapper接受和原函式一樣的引數,比如:

def debug(func):
def wrapper(something): # 指定一毛一樣的引數
print "DEBUG: enter {}()".format(func.__name__)
return func(something)
return wrapper # 返回包裝過函式

@debug
def say(something):
print "hello {}!".format(something)
這樣你就解決了一個問題,但又多了N個問題。因為函式有千千萬,你只管你自己的函式,別人的函式引數是什麼樣子,鬼知道?還好Python提供了可變引數*args和關鍵字引數**kwargs,有了這兩個引數,裝飾器就可以用於任意目標函數了。

def debug(func):
def wrapper(*args, **kwargs): # 指定宇宙無敵引數
print "DEBUG: enter {}()".format(func.__name__)
print 'Prepare and say...',
return func(*args, **kwargs)
return wrapper # 返回

@debug
def say(something):
print "hello {}!".format(something)
至此,你已完全掌握初級的裝飾器寫法。

高階一點的裝飾器
帶引數的裝飾器和類裝飾器屬於進階的內容。在理解這些裝飾器之前,最好對函式的閉包和裝飾器的介面約定有一定了解。(參見http://betacat.online/posts/python-closure/)

帶引數的裝飾器

假設我們前文的裝飾器需要完成的功能不僅僅是能在進入某個函式後打出log資訊,而且還需指定log的級別,那麼裝飾器就會是這樣的。

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

@logging(level='INFO')
def say(something):
print "say {}!".format(something)

如果沒有使用@語法,等同於

say = logging(level='INFO')(say)

@logging(level='DEBUG')
def do(something):
print "do {}...".format(something)

if name == 'main':
say('hello')
do("my work")
是不是有一些暈?你可以這麼理解,當帶引數的裝飾器被打在某個函式上時,比如@logging(level='DEBUG'),它其實是一個函式,會馬上被執行,只要這個它返回的結果是一個裝飾器時,那就沒問題。細細再體會一下。

'''
基於類實現的裝飾器

裝飾器函式其實是這樣一個介面約束,它必須接受一個callable物件作為引數,然後返回一個callable物件。在Python中一般callable物件都是函式,但也有例外。只要某個物件過載了__call__()方法,那麼這個物件就是callable的。

class Test():
def call(self):
print 'call me!'

t = Test()
t() # call me
像__call__這樣前後都帶下劃線的方法在Python中被稱為內建方法,有時候也被稱為魔法方法。過載這些魔法方法一般會改變物件的內部行為。上面這個例子就讓一個類物件擁有了被呼叫的行為。

回到裝飾器上的概念上來,裝飾器要求接受一個callable物件,並返回一個callable物件(不太嚴謹,詳見後文)。那麼用類來實現也是也可以的。我們可以讓類的建構函式__init__()接受一個函式,然後過載__call__()並返回一個函式,也可以達到裝飾器函式的效果。
【不帶引數的裝飾器】
'''
class logging(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)

@logging
def say(something):
print ('say %s' % something)

say('abc')
'''
【結果】
DEBUG:enter function say
say abc
'''

'''
【帶引數的裝飾器】
如果需要通過類形式實現帶引數的裝飾器,那麼會比前面的例子稍微複雜一點。那麼在建構函式裡接受的就不是一個函式,而是傳入的引數。通過類把這些引數儲存起來。然後在過載__call__方法是就需要接受一個函式並返回一個函式。
'''
class logging(object):
def init(self,level='INFO'):
self.level = level

def __call__(self, func): #接收函式
    def wrapper(*args,**kwargs):
        print ('[%s]: enter function %s' % (self.level,func.__name__))
        func(*args,**kwargs)
    return wrapper     #返回函式

@logging(level='DEBUG')
def say(something):
print ('say %s' % something)
say('hello python')
'''
【結果】
DEBUG: enter function say
say hello python
'''

【內建的裝飾器】

內建的裝飾器和普通裝飾器原理是一樣的,只不過返回的不是函式,而是類物件

@property

在瞭解這個裝飾器前,你需要知道在不使用裝飾器怎麼寫一個屬性

class X(object):
def init(self,x):
self._x = x

def getx(self):
    return self._x

def setx(self, x):
    self._x = x

def delx(self):
    del self._x

p = property(getx,setx,delx,'I am doc for property')

xx = X('abc')
print (xx.p)
'''
【結果】
abc
'''

def getx(self):
return self._x

def setx(self, x):
self._x = x

def delx(self):
del self._x
p = property(getx,setx,delx,'I am doc for property')
print (p)
print (property())
'''
【結果】
<property object at 0x102253098>
<property object at 0x1022530e8>
'''

'''

以上就是一個Python屬性的標準寫法,其實和Java挺像的,但是太羅嗦。有了@語法糖,能達到一樣的效果但看起來更簡單。
'''

@property
def getx(self):
return self._x
print (property())
'''
【結果】
<property object at 0x102253138>
'''

'''
【備註】屬性有三個裝飾器:setter, getter, deleter ,都是在property()的基礎上做了一些封裝,因為setter和deleter是property()的第二和第三個引數,不能直接套用@語法。getter裝飾器和不帶getter的屬性裝飾器效果是一樣的,估計只是為了湊數,本身沒有任何存在的意義。經過@property裝飾過的函式返回的不再是一個函式,而是一個property物件。
'''

'''
【classmethod & staticmethod】
'''

'''
儘管classmethod和staticmethod非常的相似,但是兩者在具體的使用上還是有著細微的差別:classmethod必須使用類物件作為第一個引數,而staticmethod則可以不傳遞任何引數。

讓我們通過實際的例子來看看。

樣板

讓我們假設有處理日期資訊的類:

class Date(object):
day = 0
month = 0
year = 0

def __init__(self, day=0, month=0, year=0):
    self.day = day
    self.month = month
    self.year = year

這個類很顯然可以被用來儲存某些日期資訊(不考慮時區資訊;讓我們假設所有的日期都用UTC表示)

這裡定義了__init__,典型的類例項初始化方法,它作為典型的instancemethod接受引數,其中第一個傳遞的必要引數是新建的例項本身。

類方法

有一些可以通過使用classmethod很好解決的任務。

假設我們有很多('dd-mm-yyyy')格式字串的日期資訊,想要把它們建立成Date類例項。我們不得不在專案的不同地方做這些事情。

所以我們必須要做到:

分析得到的年月日字串,把它們轉化成三個整形變數或者擁有三個元素的元組的變數。
通過傳遞這些值例項化Date。
得到:

day, month, year = map(int, string_date.split('-'))
date1 = Date(day, month, year)
C++擁有過載的特性可以達到這種目的,但是Python缺乏此類特性。所以,python使用classmethod的方式。讓我們嘗試一種另類的建構函式。
'''
class Date(object):
day = 0
month = 0
year = 0

def __init__(self,day=0,month=0,year=0):
    self.day = day
    self.month = month
    self.year = year

@classmethod
def from_str(cls,date_as_str):
    day,month,year = date_as_str.split('-')
    date1 = cls(year,month,day)
    return date1

date2 = Date.from_str('2018-12-15')
print (date2)
print (date2.year)
date4 = Date(1,1,2020)
print (date4.year)
'''
【結果】
<main.Date object at 0x102a50f28>
2018
2020
'''

class subDate(Date):
pass

date3 = subDate.from_str('2019-01-01')
print (date3)
print (date3.year)
date5 = subDate(1,1,2021)
print (date5.year)
'''
【結果】
<main.subDate object at 0x102a560b8>
2019
2021
'''
'''
進一步分析一下以上程式碼的執行,以及它的優勢:

1.在一個地方解析日期字串並且重複使用它。
2.做到很好的封裝(相對於把執行字串解析作為一個單獨的函式在任何地方執行,這裡使用的方法更符合OOP的正規化)
3.cls表示類物件,而不是類例項。這樣很酷,因為如果我們繼承Date類,那麼所有的子類也都將擁有from_string這個方法。
'''

'''
靜態方法

那麼staticmethod又是什麼呢?它和classmethod非常的相似,但是不強制要求傳遞引數(但是做的事與類方法或例項方法一樣)。

讓我們來看一個使用的例子。

我們有一個日期字串需要以某種方式驗證。這個任務與之前一樣要定義在Date類內部,但是不要求例項化它。

靜態方法在這種情況下就非常有用。看一下下面這個程式碼片段:
'''
class Date2(object):
@staticmethod
def is_date_valid(date_as_str):
year,month,day = map(int,date_as_str.split('-'))
return year <= 3999 and month <=12 and day <= 31
is_date = Date2.is_date_valid('2019-1-1')
print (is_date)
'''
【結果】
True
'''
'''
現在正如我們瞭解到的staticmethod的使用,我們不需要訪問它所屬的類,它本質上就是一個函式,呼叫方式和呼叫函式一樣,不同的是它不關注物件和物件內部屬性。
'''

'''
@staticmethod,@classmethod

有了@property裝飾器的瞭解,這兩個裝飾器的原理是差不多的。@staticmethod返回的是一個staticmethod類物件,而@classmethod返回的是一個classmethod類物件。他們都是呼叫的是各自的__init__()建構函式。

class classmethod(object):
"""
classmethod(function) -> method
"""
def init(self, function): # for @classmethod decorator
pass
# ...
class staticmethod(object):
"""
staticmethod(function) -> method
"""
def init(self, function): # for @staticmethod decorator
pass
# ...
裝飾器的@語法就等同呼叫了這兩個類的建構函式。

class Foo(object):

@staticmethod
def bar():
    pass

# 等同於 bar = staticmethod(bar)

至此,我們上文提到的裝飾器介面定義可以更加明確一些,裝飾器必須接受一個callable物件,其實它並不關心你返回什麼,可以是另外一個callable物件(大部分情況),也可以是其他類物件,比如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 b_hello(name='b_hello'):
return 'Hello {}!'.format(name)

@html_tags('div')
def div_hello(name='div_hello'):
return 'Hello %s' % name

b_hello()
b_hello('wxue')
b_hello('wqi')
div_hello()
div_hello('wxue')
div_hello('wqi')

'''
在裝飾器中我在各個可能的位置都加上了print語句,用於記錄被呼叫的情況。你知道他們最後打印出來的順序嗎?如果你心裡沒底,那麼最好不要在裝飾器函式之外新增邏輯功能,否則這個裝飾器就不受你控制了。以下是輸出結果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
Hello b_hello!
Hello wxue!
Hello wqi!

Hello div_hello
Hello wxue
Hello wqi
'''

'''
錯誤的函式簽名和文件
裝飾器裝飾過的函式,看上去名字沒變,其實已經變了
'''
def logging(func):
def wrapper(*args,**kwargs):
print ('DEBUG: enter %s' % func.__name__)
return func(*args,**kwargs)
return wrapper
@logging
def say(something):
print ('say %s' % something)
print (say.__name__) #wrapper

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

say = logging(say)
logging其實返回的函式名字剛好是wrapper,那麼上面的這個語句剛好就是把這個結果賦值給say,say的__name__自然也就是wrapper了,不僅僅是name,其他屬性也都是來自wrapper,比如doc,source等等。

使用標準庫裡的functools.wraps,可以基本解決這個問題。
'''

from functools import wraps
def logging(func):
@wraps(func)
def wrapper(*args,**kwargs):
print ('DEBUG: enter %s' % func.__name__)
return func(*args,**kwargs)
return wrapper

@logging
def say(something):
print ('say %s' % something)

print (say.__name__) #say

'''
看上去不錯!主要問題解決了,但其實還不太完美。因為函式的簽名和原始碼還是拿不到的。

import inspect
print inspect.getargspec(say) # failed
print inspect.getsource(say) # failed
如果要徹底解決這個問題可以借用第三方包,比如wrapt。後文有介紹。
'''

'''
不能裝飾@classmethod 和@staticmethod
'''
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(*args,**kwargs):
print ('DEBUG : enter %s' % func.__name__)
return func(*args,**kwargs)
return wrapper
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))

Car.check_model_for('abc') #AttributeError: 'staticmethod' object has no attribute 'name'

'''
前面已經解釋了@staticmethod這個裝飾器,其實它返回的並不是一個callable物件,而是一個staticmethod物件,
那麼它是不符合裝飾器要求的(比如傳入一個callable物件),你自然不能在它之上再加別的裝飾器。
要解決這個問題很簡單,只要把你的裝飾器放在@staticmethod之前就好了,
因為你的裝飾器返回的還是一個正常的函式,然後再加上一個@staticmethod是不會出問題的。
'''
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(*args,**kwargs):
print ('DEBUG: enter %s' % func.__name__)
return func(*args,**kwargs)
return wrapper
class Car(object):
def init(self,model):
self.model = model
@logging
def run(self):
print ('%s is running..' % self.model)

@staticmethod
@logging
def check_model_for(obj):
    if isinstance(obj,Car):
        print ('The model of your car is %s' % obj.model)
    else:
        print ('%s is not a car!' % obj)

Car.check_model_for('abc')
'''
【結果】
DEBUG: enter check_model_for
abc is not a car!
'''

'''
[如何優化你的裝飾器]
巢狀的裝飾函式不太直觀,我們可以使用第三方包類改進這樣的情況,讓裝飾器函式可讀性更好。
decorator.py
是一個非常簡單的裝飾器加強包,你可以很直觀的先定義包裝函式wrapper(),再使用decorator(func,wrapper)方法就可以完成一個裝飾器
記得要先pip install decorator哦
'''

from decorator import decorate

def wrapper(func,*args,**kwargs):
print ('DEBUG : enter %s' % func.__name__)
return func(*args,**kwargs)

def logging(func):
return decorate(func,wrapper)

@logging
def hello(something):
print ('hello %s' % something)

hello('decorator')
'''
DEBUG : enter hello
hello decorator
'''

'''
你也可以使用她自帶的@decorator裝飾器來完成你的裝飾器
'''
from decorator import decorator

@decorator
def logging(func,*args,**kwargs):
print ('DEBUG: enter %s' % func.__name__)
return func(*args,**kwargs)

@logging
def hello(something):
print ('hello %s' % something)
hello('wqi')
print (hello.__name__)
print (hello.__doc__)
import inspect
print (inspect.getsource(hello))
print (inspect.getsource(hello.__wrapped__))
'''
hello wqi
hello
None
@logging
def hello(something):
print ('hello %s' % something)

@logging
def hello(something):
print ('hello %s' % something)

【解析】
原文的解析是:
decorator.py實現的裝飾器能完整保留原函式的name,doc和args,唯一有問題的就是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的值你能夠更加靈活的調整你的裝飾器。另外,args和kwargs也是固定的,注意前面沒有星號。在裝飾器內部呼叫原函式時才帶星號。

如果你需要使用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
'''

'''
【小結】
Python的裝飾器和Java的註解(Annotation)並不是同一回事,和C#中的特性(Attribute)也不一樣,完全是兩個概念。

裝飾器的理念是對原函式、物件的加強,相當於重新封裝,所以一般裝飾器函式都被命名為wrapper(),意義在於包裝。函式只有在被呼叫時才會發揮其作用。比如@logging裝飾器可以在函式執行時額外輸出日誌,@cache裝飾過的函式可以快取計算結果等等。

而註解和特性則是對目標函式或物件新增一些屬性,相當於將其分類。這些屬性可以通過反射拿到,在程式執行時對不同的特性函式或物件加以干預。比如帶有Setup的函式就當成準備步驟執行,或者找到所有帶有TestMethod的函式依次執行等等。

至此我所瞭解的裝飾器已經講完,但是還有一些內容沒有提到,比如裝飾類的裝飾器。有機會再補充。謝謝觀看。
'''

參考原文地址:http://www.cnblogs.com/cicaday/p/python-decorator.html 謝謝作者 很詳細