原文發表在我的博客主頁,轉載請注明出處
如果把python當作腳本語言,每次就是寫個幾十行上百行來處理數據的話,裝飾器也許不是很必要,但是如果要開發一個大型系統,裝飾器是躲不開的,最開始體會ryu的裝飾器之美是在閱讀ryu源碼的時候,用python官網的一句話來說,learning about descriptors creates a deeper understanding of how python works and an appreciation for the elegance of its design。本篇文章從閉包講起,也從閉包結束,因為如果理解了閉包,也就理解了裝飾器。
不論在什麼語言中,變量都是必不可少的,在python中,一般存在以下幾個變量類型,local:函數內部作用域;enclosing:函數內部與內嵌函數之間;global:全局作用域;build-in:內置作用域。解釋器在遇到變量的時候,會按照如下的順序進行查找:L > E > G > B,簡稱LEGB。下面以一個簡單的函數來說明上述過程:
#coding:utf-8
#global var
passline = 60
def func(val):
# for func, it's local var
# for in_func, it's enclosing var
passline = 90
if val >= passline:
print "pass"
else:
print "fail"
def in_func():
print val
in_func()
def Max(val1, val2):
# max is a built-in fun
return max(val1, val2)
f = func(89)
f()
print Max(90, 100)
local,global,build-in這三個類型比較容易理解,enclosing變量呢?下一節詳細講解。
首先理解下python中的函數,在python中,函數是一個對象(可以通過type函數查看),在內存中占用空間;函數執行完成之後內部的變量會被解釋器回收,但是如果某變量被返回,則不會回收,因為引用計數器的值不為0;既然函數也是一個對象,他也擁有自己的屬性;對於python函數來說,返回的不一定是變量,也可以是函數。
上一節提到了enclosing變量,在func函數中又定義了一個函數in_func,它輸出了變量val,在輸出過程中查找變量的時候發現本地沒有,所以他就會去func函數裡面找並且找到了,這個變量就是enclosing變量。在上面一段代碼中,敏感的人可能已經發現了問題,分析代碼執行的過程,func函數返回了in_func函數給了f,但是沒有返回變量,所以在調用func完成之後val變量應該已經被解釋器回收,但是在執行了f函數之後卻仍然輸出了val的值89,為什麼呢?其原因就是:如果引用了enclosing作用域變量的話,會將變量添加到函數屬性中,當再次查找變量時,不是去代碼中查找,而是去函數屬性中查找。可以通過如下代碼進行驗證:
#coding:utf-8
passline = 60
def func(val):
print "%x" %id(val)
if val >= passline:
print "pass"
else:
print "fail"
def in_func():
print val
in_func()
return in_func
f = func(89)
f() #in_func
print f.__closure__
執行上面的代碼,可以發現f函數的__closure__屬性擁有一個變量,這個變量的ID和func函數中val變量的ID一樣。
上面的代碼是用來判斷學生的成績是否及格,即在百分制中60分及格,如果現在需要添加新的功能,即150分制中90分作為及格線,如何完成代碼呢?最簡單的就是我們創建兩個函數,分別為func_100和func_150來完成判斷,判斷邏輯完全一樣,代碼如下:
#coding:utf-8
def func_150(val):
passline = 90
if val >= passline:
print "pass"
else:
print "fail"
def func_100(val):
passline = 60
if val >= passline:
print "pass"
else:
print "fail"
func_100(89)
func_150(89)
如果再增加應用場景呢?這樣重復而沒有任何技術含量的代碼的增添十分繁瑣,我們可以使用新的方法——閉包來解決這個問題。什麼是閉包呢?閉包就是內部函數中對enclosing作用域的變量進行引用。代碼如下:
#coding:utf-8
def set_passline(passline):
def cmp(val):
if val >= passline:
print "pass"
else:
print "fail"
return cmp
f_100 = set_passline(60)
f_150 = set_passline(90)
print type(f_100)
print f_100.__closure__
f_100(89)
f_150(89)
在上述代碼中,我們定義了一個set_passline函數,這個函數返回cmp函數,在這個函數中定義了cmp函數,在cmp函數中是我們之前的邏輯。在之後不論要進行多少分制的及格判斷,只需要調用set_passline函數設置及格線就好,我們以百分制60分及格為例,分析上述代碼的執行過程,基本分為兩步,一是set_passline函數把返回值cmp函數給f_100,同時將60作為屬性給cmp函數,二是f_100函數的執行其實相當於執行存儲了passline的cmp函數。
接下來考慮一個問題,上面的passline是整數,能否換成函數?既然變量和函數都是對象,而且以python的靈活性,答案是肯定的。下面的代碼同時描述了閉包的另一個應用場景,同時展示了如何使用。
#coding:utf-8
def my_sum(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return sum(arg)
def my_average(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return sum(arg)/len(arg)
def dec(func):
def in_dec(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return func(*arg)
return in_dec
# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)
在上面的代碼中,首先定義了兩個函數,這兩個函數的功能大同小異,分別為求和函數和求平均值函數,由於求平均值元祖的長度不能為空,同時元祖中的數據都應該為整數,所以在每個函數中都先需要參數檢查,本著以人為本的方針,這部門代碼應該復用。所以在下面定義了另外一個函數dec,這個函數的參數為一個函數,返回值為一個內嵌函數,這個內嵌函數的邏輯和上面求和求平均值的邏輯一樣,先判斷,再返回結果。這個函數如何使用呢?比如現在我們要求和,代碼如下:
def my_sum(*arg):
return sum(arg)
def dec(func):
def in_dec(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return func(*arg)
return in_dec
# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)
my_sum = dec(my_sum)函數分兩步,首先my_sum得到dec返回的in_dec函數,其次調用in_dec函數。
所以,遵上來看,閉包可以在很大程度上實現封裝和代碼復用。
最開始就說,這篇博客始於閉包,終於閉包,所以裝飾器不多說,只說四句話:
1.裝飾器就是對閉包的使用;
2.裝飾器用來裝飾函數;
3.返回一個函數對象,被裝飾的函數接收;
4.被裝飾函數標識符指向返回的函數對象。
接觸python裝飾器很久了,殊不知從閉包更容易理解裝飾器。