1. 程式人生 > >細說裝飾器(精心整理,務必收藏)

細說裝飾器(精心整理,務必收藏)

什麼是裝飾器?
簡單來說,可以把裝飾器理解為一個包裝函式的函式,它一般將傳入的函式或者是類做一定的處理,返回修改之後的物件。
所以我們能夠在不修改原函式的基礎上,在執行原函式前後執行別的程式碼,比較常用的場景有日誌插入,事物處理等
我們知道,在python中韓式也是被視為物件的,可以作為引數傳遞,那麼假如把計算機耗時的獨立為一個單獨的函式,然後把需要
計算耗時的函式都不用修改自己的程式碼了

def calc_spend_time(func,*args,**kargs):
  start_time = datetime.datetime.now()
  result = fun(*args,**kargs)
  end_time = datetime.datetime.now()
  print "result:",result,"used:",(end_time - start_time).microseconds
def calc_add(a,b):
  return a + b
calc_spend_time(calc_add,1,1)
#calc_spend_time(calc_add,a=1,b=2)

看起來也不錯,負責計算的函式不用更改,只需呼叫的時候作為引數傳給計算時間查的函式。但就是這,呼叫的時候形式變了,不再是
clac(1,2),而是calc_spend_time(clac_add,1,2),萬一calc_add大規模被呼叫,那麼還得一處一處找,然後修改過來,還是很麻煩
那麼可以在calc_spend_time()裡把傳入的calc包裝一下,然後返回包裝後的新的函式,再把返回的包裝好的函式服給clac,那麼calc()
的效果就和calc_spend_time(calc())的效果一樣

import datetime
def calc_spend_time(func):
 def new_func(a,b):
   start_time = datetime.datetime.now()
   result = fumc(a,b)
   end_time = datetime.datetime.now()
   print "result:",result,"used:",(end_time - start_time).microseconds
 return new_func 
def calc_add(a,b):
 return a + b
calc_add = calc_spend_time(calc_add)
calc_add(1,2 ) 

語法糖:
上面的列子就是裝飾器的概念,包含函式的函式,事實上上面的例子還可以更簡潔

import datetime
def calc_spend_time(func):
 def new_func(a,b):
   start_time = datetime.datetime.now()
   result = func(a,b)
   end_time = datetime.datetime.now()
   print "result",result,"used:",(end_time - start_time).microseconds,"us"
 return new_func
 def calc_add(a,b):
  return a+b
 calc_add(1,2)
@calc_spend_time就是語法糖,它的本質就是:calc_add = calc_spend_time(calc_add)

無引數的函式裝飾器

import datetime
def calc_spend_time(func):
 def new_func(*args,**kargs):
  start_time = datetime.datetime.now()
  result = func(*args,**kargs)
  end_time = datetime.datetime.now()
  print "result:",result,"used:",(end_time - start_time).microseconds,"us"
 result new_func
 @calc_spend_time
 def calc_add(a,b):
  return a+b
 @calc_spend_time
 def calc_diff(a,b):
  return a - b
 calc_add(a=1,b=2)
 calc_diff(1,2)

*args:把所有的引數按出現的順序打包成list
**kargs:把所有的key=value形式的引數打包成一個dict
帶引數的函式裝飾器
假如我們需要知道函式的一些額外的資訊,假如函式作者,可以通過給裝飾器函式增加引數來實現

import datetime
def calc_spend_time(author):
 def first_deco(func):
  def new_func(*args,**kargs):
   start_time=datetime.datetime.now()
   result = func(*args,**kargs):
   end_time = datetime.datetime.now()
   print author,"result:",result,"used:",(end_time - start_time).microseconds,"us"
  return new_func
 return first_deco
@calc_spend_time('author_1')
def calc_add(a,b):
 return a+b
@calc_spend_time('author_2')
def calc_diff(a,b):
 return a-b
calc_add(a=1,b=2)
calc_diff(1,2)

python內建裝飾器
python內建的裝飾器有三個:staticmethod,classmethod和property
staticmethod:把類中的方法定義為靜態方法,使用staticmethod裝飾的方法可以使用類或者類的例項物件來呼叫,不需要傳入self

class Human(object):
 "docstring for Human"
 def __int__(self):
  super(Human,self).__int_()
 @staticmethod
 def say(message):
  if not message:
    message = 'hello'
  print 'I say %s'% message
 def speak(self,message):
  self.say(message)
Human.say(None)
human = Human()
human.speak('hi')
輸出:
I say hello
I say hi

classmmethod:把類中的方法定義為類的方法,使用classmethod裝飾的方法可以使用類的或者類的例項物件來呼叫,
並將該class物件隱式的作為第一個引數傳入
class Human(object):
“”“docstring for Human”""
def int(self):
super(Human,self).init()
self.message = ‘111’
def say(message):
if not message:
message = “hlleo”
print “I say %s” %message
@classmethod
def speak(cls,message):
if not message:
message = ‘hello’
cls.say(message)
human = Human()
human.speak(‘hi’)
輸出:
I say hello
I say hi

property:把方法變成屬性
class Human(object):
def init(self,value):
super(Human,self).init()
self.arg = value
@property
def age(self):
return self._age
human = Human(20)
print human.age

很多人對裝飾器難以理解,原因是由於以下三點內容沒有搞清楚:

關於函式“變數”(或“變數”函式)的理解
關於高階函式的理解
關於巢狀函式的理解
那麼如果能對以上的問題一一攻破,同時遵循裝飾器的基本原則,相信會對裝飾器有個很好的理解的。那麼我們先來看以下裝飾器的目的及其原則。

1、裝飾器
裝飾器實際上就是為了給某程式增添功能,但該程式已經上線或已經被使用,那麼就不能大批量的修改原始碼,這樣是不科學的也是不現實的,因為就產生了裝飾器,使得其滿足:

不能修改被裝飾的函式的原始碼
不能修改被裝飾的函式的呼叫方式
滿足1、2的情況下給程式增添功能
那麼根據需求,同時滿足了這三點原則,這才是我們的目的。因為,下面我們從解決這三點原則入手來理解裝飾器。

等等,我要在需求之前先說裝飾器的原則組成:

< 函式+實參高階函式+返回值高階函式+巢狀函式+語法糖 = 裝飾器 >
這個式子是貫穿裝飾器的靈魂所在!

2、需求的實現
假設有程式碼:

improt time
def test():
time.sleep(2)
print(“test is running!”)
test()
很顯然,這段程式碼執行的結果一定是:等待約2秒後,輸出

test is running

那麼要求在滿足三原則的基礎上,給程式新增統計執行時間(2 second)功能
在行動之前,我們先來看一下文章開頭提到的原因1(關於函式“變數”(或“變數”函式)的理解)

2.1、函式“變數”(或“變數”函式)
假設有程式碼:

x = 1
y = x
def test1():
print(“Do something”)
test2 = lambda x:x*2
那麼在記憶體中,應該是這樣的:

很顯然,函式和變數是一樣的,都是“一個名字對應記憶體地址中的一些內容”
那麼根據這樣的原則,我們就可以理解兩個事情:

test1表示的是函式的記憶體地址
test1()就是呼叫對在test1這個地址的內容,即函式
如果這兩個問題可以理解,那麼我們就可以進入到下一個原因(關於高階函式的理解)

2.2高階函式
那麼對於高階函式的形式可以有兩種:

把一個函式名當作實參傳給另外一個函式(“實參高階函式”)
返回值中包含函式名(“返回值高階函式”)
那麼這裡面所說的函式名,實際上就是函式的地址,也可以認為是函式的一個標籤而已,並不是呼叫,是個名詞。如果可以把函式名當做實參,那麼也就是說可以把函式傳遞到另一個函式,然後在另一個函式裡面做一些操作,根據這些分析來看,這豈不是滿足了裝飾器三原則中的第一條,即不修改原始碼而增加功能。那我們看來一下具體的做法:

還是針對上面那段程式碼:

improt time

def test():
time.sleep(2)
print(“test is running!”)

def deco(func):
start = time.time()
func() #2
stop = time.time()
print(stop-start)

deco(test) #1
我們來看一下這段程式碼,在#1處,我們把test當作實參傳遞給形參func,即func=test。注意,這裡傳遞的是地址,也就是此時func也指向了之前test所定義的那個函式體,可以說在deco()內部,func就是test。在#2處,把函式名後面加上括號,就是對函式的呼叫(執行它)。因此,這段程式碼執行結果是:

test is running!
the run time is 3.0009405612945557

我們看到似乎是達到了需求,即執行了源程式,同時也附加了計時功能,但是這隻滿足了原則1(不能修改被裝飾的函式的原始碼),但這修改了呼叫方式。假設不修改呼叫方式,那麼在這樣的程式中,被裝飾函式就無法傳遞到另一個裝飾函式中去。

那麼再思考,如果不修改呼叫方式,就是一定要有test()這條語句,那麼就用到了第二種高階函式,即返回值中包含函式名

如下程式碼:

improt time

def test():
time.sleep(2)
print(“test is running!”)

def deco(func):
print(func)
return func
t = deco(test) #3
#t()#4

test()
我們看這段程式碼,在#3處,將test傳入deco(),在deco()裡面操作之後,最後返回了func,並賦值給t。因此這裡test => func => t,都是一樣的函式體。最後在#4處保留了原來的函式呼叫方式。
看到這裡顯然會有些困惑,我們的需求不是要計算函式的執行時間麼,怎麼改成輸出函式地址了。是因為,單獨採用第二張高階函式(返回值中包含函式名)的方式,並且保留原函式呼叫方式,是無法計時的。如果在deco()裡計時,顯然會執行一次,而外面已經呼叫了test(),會重複執行。這裡只是為了說明第二種高階函式的思想,下面才真的進入重頭戲。

2.3 巢狀函式
巢狀函式指的是在函式內部定義一個函式,而不是呼叫,如:

def func1():
def func2():
pass
而不是
def func1():
func2()
另外還有一個題外話,函式只能呼叫和它同級別以及上級的變數或函式。也就是說:裡面的能呼叫和它縮排一樣的和他外部的,而內部的是無法呼叫的。

那麼我們再回到我們之前的那個需求,想要統計程式執行時間,並且滿足三原則。

程式碼:

improt time

def timer(func) #5
def deco():
start = time.time()
func()
stop = time.time()
print(stop-start)
return deco

test = timer(test) #6

def test():
time.sleep(2)
print(“test is running!”)
test() #7
這段程式碼可能會有些困惑,怎麼忽然多了這麼多,暫且先接受它,分析一下再來說為什麼是這樣。

首先,在#6處,把test作為引數傳遞給了timer(),此時,在timer()內部,func = test,接下來,定義了一個deco()函式,當並未呼叫,只是在記憶體中儲存了,並且標籤為deco。在timer()函式的最後返回deco()的地址deco。

然後再把deco賦值給了test,那麼此時test已經不是原來的test了,也就是test原來的那些函式體的標籤換掉了,換成了deco。那麼在#7處呼叫的實際上是deco()。

那麼這段程式碼在本質上是修改了呼叫函式,但在表面上並未修改呼叫方式,而且實現了附加功能。

那麼通俗一點的理解就是:
把函式看成是盒子,test是小盒子,deco是中盒子,timer是大盒子。程式中,把小盒子test傳遞到大盒子temer中的中盒子deco,然後再把中盒子deco拿出來,開啟看看(呼叫)

這樣做的原因是:

我們要保留test(),還要統計時間,而test()只能呼叫一次(呼叫兩次執行結果會改變,不滿足),再根據函式即“變數”,那麼就可以通過函式的方式來回閉包。於是乎,就想到了,把test傳遞到某個函式,而這個函式內恰巧內嵌了一個內函式,再根據內嵌函式的作用域(可以訪問同級及以上,內嵌函式可以訪問外部引數),把test包在這個內函式當中,一起返回,最後呼叫這個返回的函式。而test傳遞進入之後,再被包裹出來,顯然test函式沒有弄丟(在包裹裡),那麼外面剩下的這個test標籤正好可以替代這個包裹(內含test())。

至此,一切皆合,大功告成,單隻差一步。

3、 真正的裝飾器
根據以上分析,裝飾器在裝飾時,需要在每個函式前面加上:

test = timer(test)

顯然有些麻煩,Python提供了一種語法糖,即:

@timer

這兩句是等價的,只要在函式前加上這句,就可以實現裝飾作用。

以上為無參形式

4、裝飾有參函式
improt time

def timer(func)
def deco():
start = time.time()
func()
stop = time.time()
print(stop-start)
return deco

@timer
def test(parameter): #8
time.sleep(2)
print(“test is running!”)
test()
對於一個實際問題,往往是有引數的,如果要在#8處,給被修飾函式加上引數,顯然這段程式會報錯的。錯誤原因是test()在呼叫的時候缺少了一個位置引數的。而我們知道test = func = deco,因此test()=func()=deco()
,那麼當test(parameter)有引數時,就必須給func()和deco()也加上引數,為了使程式更加有擴充套件性,因此在裝飾器中的deco()和func(),加如了可變引數*agrs和 **kwargs。

完整程式碼如下:

improt time

def timer(func)
def deco(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print(stop-start)
return deco

@timer
def test(parameter): #8
time.sleep(2)
print(“test is running!”)
test()
那麼我們再考慮個問題,如果原函式test()的結果有返回值呢?比如:

def test(parameter):
time.sleep(2)
print(“test is running!”)
return “Returned value”
那麼面對這樣的函式,如果用上面的程式碼來裝飾,最後一行的test()實際上呼叫的是deco()。有人可能會問,func()不就是test()麼,怎麼沒返回值呢?

其實是有返回值的,但是返回值返回到deco()的內部,而不是test()即deco()的返回值,那麼就需要再返回func()的值,因此就是:

def timer(func)
def deco(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)#9
stop = time.time()
print(stop-start)
return res#10
return deco
其中,#9的值在#10處返回。

完整程式為:

improt time

def timer(func)
def deco(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
stop = time.time()
print(stop-start)
return res
return deco

@timer
def test(parameter): #8
time.sleep(2)
print(“test is running!”)
return “Returned value”
test()
5、帶引數的裝飾器
又增加了一個需求,一個裝飾器,對不同的函式有不同的裝飾。那麼就需要知道對哪個函式採取哪種裝飾。因此,就需要裝飾器帶一個引數來標記一下。例如:

@decorator(parameter = value)

比如有兩個函式:

def task1():
time.sleep(2)
print(“in the task1”)

def task2():
time.sleep(2)
print(“in the task2”)

task1()
task2()
要對這兩個函式分別統計執行時間,但是要求統計之後輸出:

the task1/task2 run time is : 2.00……

於是就要構造一個裝飾器timer,並且需要告訴裝飾器哪個是task1,哪個是task2,也就是要這樣:

@timer(parameter=‘task1’) #
def task1():
time.sleep(2)
print(“in the task1”)

@timer(parameter=‘task2’) #
def task2():
time.sleep(2)
print(“in the task2”)

task1()
task2()
那麼方法有了,但是我們需要考慮如何把這個parameter引數傳遞到裝飾器中,我們以往的裝飾器,都是傳遞函式名字進去,而這次,多了一個引數,要怎麼做呢?
於是,就想到再加一層函式來接受引數,根據巢狀函式的概念,要想執行內函式,就要先執行外函式,才能呼叫到內函式,那麼就有:

def timer(parameter): #
print(“in the auth :”, parameter)

def outer_deco(func): #
    print("in the outer_wrapper:", parameter)

    def deco(*args, **kwargs):

    return deco

return outer_deco

首先timer(parameter),接收引數parameter=’task1/2’,而@timer(parameter)也恰巧帶了括號,那麼就會執行這個函式, 那麼就是相當於:

timer = timer(parameter)
task1 = timer(task1)
後面的執行就和一般的裝飾器一樣了:

import time

def timer(parameter):

def outer_wrapper(func):

    def wrapper(*args, **kwargs):
        if parameter == 'task1':
            start = time.time()
            func(*args, **kwargs)
            stop = time.time()
            print("the task1 run time is :", stop - start)
        elif parameter == 'task2':
            start = time.time()
            func(*args, **kwargs)
            stop = time.time()
            print("the task2 run time is :", stop - start)

    return wrapper

return outer_wrapper

@timer(parameter=‘task1’)
def task1():
time.sleep(2)
print(“in the task1”)

@timer(parameter=‘task2’)
def task2():
time.sleep(2)
print(“in the task2”)

task1()
task2()
至此,裝飾器的全部內容結束。