之前 分析了裝飾器的語法,由此可以直接推導出其基本框架。但為了寫出壹個功能完整的裝飾器,還需要了解壹個概念——閉包。
閉包(closure) ,是引用了自由變量的函數。 這個被引用的自由變量將和這個函數壹同存在,即使已經離開了創造它的環境也不例外。
看下面的例子
對 f 內部的函數 g 來說,參數 a 既不是它的參數,也不是它的局部變量,而是它的自由變量。該自由變量可以
閉包和嵌套函數的概念有所區別。閉包當然是嵌套函數,但沒有引用自由變量的嵌套函數卻不是閉包。
Python 的函數有壹個只讀屬性 __closure__ ,存儲的就是函數所引用的自由變量,
如果僅僅是嵌套函數,它的 __closure__ 應該是 None 。
閉包有個重要的特性:內部函數只能引用而不能修改外部函數中定義的自由變量。試圖直接修改只有兩種結果,要麽運行出錯,要麽妳以為妳修改了,實際並沒有。
不能修改不是因為 Python 設計者故意限制,不給它權限,而是外部的自由變量被內部的局部變量覆蓋了;被覆蓋了也不是閉包獨有的特性,從普通函數內部同樣也不能直接修改全局變量。Python 命名空間的查找規則簡寫為 LEGB,四個字母分別代表 local、enclosed、global 和 build-in,閉包外層函數的命名空間就是 enclosed。Python 在檢索變量時,按照 L -> E -> G -> B 的順序依次查找,如果在 L 中找到了變量,就不會繼續向後查找。
在示例 1 中,妳的本意是修改自由變量 number ,然而並不能:由於存在對 number 的賦值語句( number += 1 ),Python 會認為 number 是 printer 的局部變量,可是在局部變量字典中又查找不到它的定義,只好拋出異常。拋出的異常不是因為不能修改自由變量,而是局部變量還沒賦值就被引用了。
在示例 2 中,Python 成功地在 printer 內定義了局部變量 number ,並覆蓋了同名自由變量,妳可能以為自己成功修改了 print_msg 中的 number ,然而並沒有。
怎麽才能修改呢?
壹種做法是利用可變類型(mutable)的特性,把變量存放在列表(List)之中。對可變的列表的修改並不需要對列表本身賦值, number[0] = 3 只是修改了列表元素。雖然列表發生了變化,但引用列表的變量卻並沒有改變,巧妙地“瞞”過了 Python。見示例3。
Python 3 引入了 nonlocal 關鍵字,明確告訴解釋器:這不是局部變量,要找上外頭找去。在示例 4 中, nonlocal 幫助我們實現了所期望的對自由變量的修改。
其實,在 Python 2 中,用 global 代替 nonlocal ,也能達到類似的效果,但由於全局變量的不易控制,這種做法不被提倡。
下面的例子很好地展示了自由變量的特點:與引用它的函數壹同存在,而想要修改它,得小心謹慎。
裝飾器 rate_limit 的作用,是限制被裝飾的函數每秒內最多被訪問 max_per_sec 次。為此,需要維護壹個變量用以記錄上次被調用的時刻,它獨立於函數之外,和被修飾的函數壹同存在,還能在每次被調用的時候更新。 last_time_called 就是這樣的變量。為了正確地更新, last_time_called 以列表的形式存在。如果在 Python 3 中,它也可以直接存為 float ,只要在內部函數中聲明為 nonlocal ,也可以達到同樣的目的。