在看閉包問題之前先來看看關於python中作用域的問題
變量作用域
對於上述代碼中出現錯誤,肯定沒什么疑問了,畢竟b並沒有定義和賦值,當我們把代碼更改如下后:
再看一個例子:
首先這個錯誤已經非常明顯:說在賦值之前引用了局部變量b
可能很多人覺得會打印10然后打印6,其實這里就是涉及到變量作用域的問題
當Python編譯函數的的定義體的時候,它判斷b是局部變量,畢竟在函數中有b = 9表示給b賦值了,所以python會從本地環境獲取b,當我們調用方法執行的時候,定義體會獲取並打印變量a的值,但是當嘗試獲取b的值的時候發現b沒有綁定值,所以要想讓上述代碼運行還可以把b設置為全局變量,或者把b賦值放到調用之前
函數對象的作用域
python中一切皆對象,同其他對象一樣,函數對象也有其使用的范圍即函數對象的作用域。
在python中我們通過def定義函數,函數對象的作用域與def所在的層級相同,
通過下面代碼進行理解:
def func1(): def func2(x): return 2*x print(func2(5)) func1() print(func2(5))
這個例子中我們在def func1函數內可以調用fun2,但是我們在外面是無法調用到func2的,所以結果為看到如下:
閉包
關於閉包主要有下面兩種說法:
- 閉包是符合一定條件的函數,定義為:閉包是在其詞法上下文中引用了自由變量的函數
- 閉包是由函數與其相關的引用環境組合而成的實體。定義為:在實現綁定時,需要創建一個能顯示表示引用環境的東西,並將它與相關的子程序捆綁在一起,這樣捆綁起來的整體稱為閉包
個人覺得第二種說法更准確,閉包只是在形式上表現像函數,實際不是函數。
我們對函數的定義是:一些可執行的代碼,這些代碼在函數定義后就確定了,不會在執行時發生變化,所以一個函數只有一個實例。
閉包在運行的時候可以有多個實例,不同的引用環境和相同的環境組合可以產生不同的實例。
這里有一個詞:引用環境,其實引用環境就是在執行運行的某個時間點,所有處於活躍狀態的變量所組成的集合,這里的變量是指變量的名字和其所代表的對象之間的聯系。
可以使用閉包語言的特點:
- 函數可以作為另外一個函數的返回值或者參數,還可以作為一個變量的值。
- 函數可以嵌套使用
而認為閉包是函數的有一句話是:
閉包是指延伸了作用域的函數,其中包含函數定義體中引用。但是不在定義體中定義的非全局變量。
上面這種說法個人覺得也是一種理解方式
相信看了這些概念也還是不好理解,還是通過下面例子更好理解:
先實現一種計算平均值的方法:
從結果我們可以看出這里保存了每次的歷史值
換一種方法實現:
實現了第一種相同的效果,對這種方法分析:
通常我們會認為我們調用avg(10)的時候make_averager函數已經返回了,而它的本地作用域也一去不復返,但這里其實series是自由變量,是指未在本地作用域綁定的變量
我們可以通過print(dir(avg)),看到如下結果:
其實這里面保存着均布變量和自由變量的名稱,我們可以通過下面方法查看:
eries的綁定在返回的avg函數的__closure__屬性中這或許就是有的人會認為閉包一種函數。閉包會保留定義函數時存在的自由變量的綁定,這樣調用函數時雖然定義作用域不能用了,但是仍能使用那些綁定
關於nonlocal
剛開始了解閉包之后,如果嘗試使用這種編程方式容易出現以下錯誤使用例子:
def make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager
先來看一下錯誤提示:
這個例子中和我們上面使用的不同之處是:這里的count和total是數字,是不可變類型,而之前的例子中series是一個列表是可變類型
所以這里重新回到了最開始說的作用域問題了,當我們在averager中使用
count += 1的時候其實就是count = count + 1,這樣就是在averager函數定義體中對count進行賦值,count就變成了局部變量。
問題小結:當時數字,字符串,元組等不可變類型時,只能讀取不能更新,如果使用類似count += 1就會隱式的把count變成局部變量,所以開始例子中使用series,我們后面的操作是append並且列表還是可變對象
不過python3引入了一個新的關鍵詞nonlocal,通過它把變量標記為自由變量,這樣我們把上面這個錯誤的例子簡單更改:
def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count,total count += 1 total += new_value return total / count return averager
到這里裝飾器的前奏就說完了,下面就是裝飾器,我個人覺得裝飾器只是閉包的一種應用,閉包在很多情況下都是一種非常好的變成技巧