Closure的應用和替代方案比較

發表於2017-05-04

建議從這裡下載這篇文章對應的.ipynb檔案和相關資源。這樣你就能在Jupyter中邊閱讀,邊測試文中的程式碼。

Closure(閉包) 和相關實現方案

python中, function本身也是object, 可以直接被作為變數傳入函式或者被作為結果返回(Java等語言就不能這麼幹)。 這種靈活性帶來很多有趣的應用,其中一個就是closure。 上一個最簡單的程式碼的例子。

closure的三個基本要素

  • 存在一個外層函式(outer)。在outer函式的內部,定義了一個(inner)函式
  • inner函式內部使用outer函式中存在的資料, 這個例子中是a
  • outer函式返回inner函式

下面例舉幾個可以用到closure的場景

場景一

需要很多功能類似, 但是具體功能又有一定變化的函式。

這時, 可以把outer函式看做是另外一個函式的“製造工廠”, inner函式看做是“工廠”的產出。 通過給outer函式傳入引數, 控制製造出的inner函式的具體行為。

例如有一個自動決策系統,可以接受使用者自己編寫的決策器函式。 這個例子裡面

  • 假設決策器只是簡單的判斷傳入資料知否大於某個閾值。
  • 假設框架約定, 傳入的決策器函式, 只能接受一個變數。 這個變數是待判斷的數值。

懶方案

預先定義一大堆決策器, 每個決策器中的閾值都不同

顯然, 這種方法是不現實的。 我們顯然不可能窮盡所有可能的閾值並且會造成大量的重複程式碼。

closure方案

利用closure

這樣如果要得到不用閾值的決策器, 只要把閾值傳給“工廠”, 讓它“生產”我們需要的決策器函式即可。 我們通過傳入不同的閾值, 可以得到幾百, 幾千個不同的決策器函式, 但是”工廠”函式只需要寫一個即可。

其它方案

實現一個特定的目的,不一定只有一種方案。 為了實現之前的需求,除了用closure, 當然也可以用其它的方式實現。可以先寫一個接受雙引數的函式

這個函式不符合只接受“待判斷”數值的約定,不過可以用python的partial函式實現。

上面這段程式碼把threshold引數固定成1, 並且返回只需要接受x的新函式,起到的功能和closure是一樣的。

如果不想引入functools這個庫, 其實寫個lambda函式也是可以起到一樣的作用的

場景二

closure的應用

outer函式中的資料, 在每次inner函式被執行的時候, 並不會被清除而是會記憶之前的狀態。 因此可以利用這點,創造一個能記錄狀態的函式。典型的場景是一個計數器。

不錯要注意的是,inner函式只能修改outer函式中mutable型別的資料。 如果嘗試修改mutable型別的資料, 會導致出錯。

object方案

建立一個object, 讓object內部的屬性去記錄自己的狀態

通過新增一個__call__特殊method, 可以讓上面的counter像函式那樣被使用, 這樣和closure的例子就更像了。(能夠像函式那樣被呼叫的object稱為functor)

各種實現方式對效能的影響

有多種實現方案都能實現“記錄狀態”或是”固定部分行為”, 不同的實現方式對效能的影響如何?以下是一個實驗。

code region
0 110000 北京市
1 110100 市轄區
2 110101 東城區
3 110102 西城區
4 110105 朝陽區

為了比較不同版本的程式碼消耗的時間, 先寫一個函式。功能是重複執行函式n次後顯示消耗的時間。

接下來準備好五個版本的程式碼用於比較

雙引數版

partial函式版

lambda函式版

closure版

functor版

然後比較一下它們的速度, 各執行1000次

似乎效果一樣

lambda lazy binding

lambda是不能配合for迴圈批量的”製造”函式的

原因是, lambda函式的lazy binding機制, 導致只有lambda函式被呼叫的時候,才會去查詢b的值。這時迴圈已經結束,b的數值固定為20

但是closure函式並不會出現這個問題

如果一定要用lambda函式實現類似效果的話,可以利用預設值來傳入b的數值

partial版的測試

可以看到, partial並沒有lazy binding的問題

相關文章