-- Illustrations by Charlie Davis --
上一篇文章主要以一步一步演進的方式介紹了裝飾器的工作原理以及使用(沒看的小夥伴可以關注一下 一文讀懂Python裝飾器由來(一)),其實只要認真學習上一篇文章,已經能夠滿足日常對裝飾器的使用了。但是,若想真正理解裝飾器,並進行更高階的使用還要了解其他一些知識:
python中,函數是一等對象;
區分導入時執行和運行時執行;
閉包和 nonlocal 聲明;
下面我們逐個介紹:
第一點,在 Python 中,函數是一等對象,這在上一篇其實已經提到了。「一等對象」滿足下述條件:
a.在運行時創建;
b.能賦值給變量或數據結構中的元素;
c.能作為參數傳給函數;
d.能作為函數的返回結果;
Python 中的整數、字符串和字典等都是一等對象,大家對比著理解一下,在此不再過多介紹。 第二點,函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。看下面的例子:
al = []
def deco(func):
print('running deco and parm is{}'.format(func))
al.append(func)
return func
@deco
def f1():
print('running f1()')
@deco
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('al ->', al)
f1()
f2()
f3()
if __name__=='__main__':
main()
輸出:
running deco and parm is<function f1 at 0x00000000006C2AE8>
running deco and parm is<function f2 at 0x00000000011E6510>
running main()
al -> [<function f1 at 0x00000000006C2AE8>, <function f2 at 0x00000000011E6510>]
running f1()
running f2()
running f3()
我們簡單定義了一個裝飾器,把傳進來的參數(函數名)添加到列表,然後再返回該函數名。觀察輸出結果,在運行main函數之前,deco就已經運行了(輸出了2次,因為f1和f2都用deco進行了裝飾),之後對列表的輸出也印證了這一點,而不管是被裝飾的f1、f2還是未被裝飾的f3都是在明確的調用之後才執行的。這就是Python 程式設計師所說的導入時和運行時之間的區別。 第三點,閉包可以說是行為良好的裝飾器賴以生存的關鍵。閉包其實並不難以理解,因為它只存在於嵌套函數中。還是看例子:
def get_averager():
nums = []
def averager(new_value):
nums.append(new_value)
total = sum(nums)
return total/len(nums)
return averager
avg = get_averager()
print(avg)
print(avg(10))
print(avg(11))
print(avg(12))
輸出:
<function get_averager.<locals>.averager at 0x0000000000672AE8>
10.0
10.5
11.0
定義一個嵌套函數,作用是計算累計傳入參數的平均值。通過輸出結果我們可以看到avg是getaverager()返回的averager,通過不斷的調用avg(),返回當前的平均值。這裡面有個問題是我們之前沒有探討的:nums是外層函數中的變量,那麼在getaverager()返回完畢之後,它的本地作用域應該一併消失,那為什麼avg中還可以使用呢?這就是閉包的作用了。其實,閉包就是指函數作用域延伸了(從外層函數延伸到內層函數)。延伸的值保存在內層函數的code屬性中:
>>> def get_averager():
nums = []
def averager(new_value):
nums.append(new_value)
total = sum(nums)
return total/len(nums)
return averager
>>> avg = get_averager()
>>> avg.__code__.co_freevars
('nums',)
我們注意到上面這個例子把所有值存儲在歷史列表中,然後在每次調用 averager 時使用 sum 求和。更好的實現方式是,只存儲目前的總值和元素個數,然後使用這兩個數計算均值。依照這個思路我們可以對代碼進行優化,但是在此之前我們需要看一個簡單的例子:
>>> b = 99
>>> def f(t):
print(t)
print(b)
b = 2
>>> f(10)
各位可以想像一下,這個輸出會是什麼?
10
99
是不是這個?其實不然,真實的結果是這樣:
>>> f(10)
10
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
f(10)
File "<pyshell#41>", line 3, in f
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
>>>
這個結果可能讓你驚訝,但事實就是如此。因為Python 編譯函數的定義體時,由於b在函數中給它賦值了,因此它判斷 b 是局部變量。後面調用 f(10) 時, f 的定義體會獲取並列印局部變量 b的值,但是嘗試獲取局部變量 b的值時,發現 b 沒有綁定值。這不是缺陷,而是設計選擇:Python 不要求聲明變量,但是假定在函數定義體中賦值的變量是局部變量。了解了這一點,我們來優化一下之前計算平均值的例子:
def get_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
邏輯上看沒啥問題,但是有了之前的鋪墊,你可能會發現一些問題:內層函數對外層函數中的變量進行了重新賦值。我們來運行一下代碼,就會發現報錯:
UnboundLocalError: local variable 'count' referenced before assignment
而優化前的例子沒遇到這個問題,因為nums是列表,我們只是調用 nums.append,也就是說,我們利用了列表是可變的對象這一事實。但是對數字、字符串、元組等不可變類型來說,只能讀取,不能更新。如果嘗試重新綁定,例如 count = count + 1,其實會隱式創建局部變量 count。 為了解決這個問題,Python 3 引入了 nonlocal 聲明,如果為 nonlocal 聲明的變量賦予新值,閉包中保存的綁定會更新。
>>> def get_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
>>> avg = get_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
以上三點就是對裝飾器基礎知識的補充,希望對大家有所幫助。
讚賞作者
最近熱門文章
用Python更加了解微信好友
如何用Python做一個騷氣的程式設計師
用Python爬取陳奕迅新歌《我們》10萬條評論的新發現
用Python分析蘋果公司股價數據
Python自然語言處理分析倚天屠龍記
▼ 點擊下方閱讀原文,免費成為社區會員