點擊python編程從入門到實踐,置頂 公衆號重磅 python入門資料,第一時間送達javascript
讀完須要java
速讀僅需 6 分鐘python
/ python 生產實戰 從閉包到中間件 /web
注:這篇文章很長,但我保證你看完後能得到足夠多對閉包的瞭解,也會完全理解清楚中間件的實現原理。面試
閉包這個概念不管在你面試 python 開發工程師 的時候仍是在平常的 python 開發過程當中都有一些涉及,筆者以前在研究 Tornado 源碼的過程當中看到大量的使用閉包去實現特定功能的案例,上一篇中分享瞭如何經過中間件的方式解決生產環境中的實際問題從而拿到公司今年漲薪名額的案例。本篇咱們就從一個閉包的概念出發來一步步分析並完成一個可用的中間件功能。編程
1數組
什麼是閉包微信
閉包是一個普遍存在的概念,在數學,拓撲學以及計算機科學中都有這個它的身影,雖然都叫這個名字,可是在定義上仍是有所區別,此閉包非彼閉包。閉包
1.1app
計算機中的閉包
在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便已經離開了創造它的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。
1.2
數學領域中的閉包
集合 S 是閉集當且僅當 Cl(S)=S(這裏的 cl 即 closure,閉包)。特別的,空集的閉包是空集,X 的閉包是 X。集合的交集的閉包老是集合的閉包的交集的子集(不必定是真子集)。有限多個集合的並集的閉包和這些集合的閉包的並集相等;零個集合的並集爲空集,因此這個命題包含了前面的空集的閉包的特殊狀況。無限多個集合的並集的閉包不必定等於這些集合的閉包的並集,但前者必定是後者的父集。
關於各個領域中閉包的解釋,咱們就說這些。咱們的目的是讓你們清楚這個詞的使用範圍是很廣的,只需瞭解便可不是咱們本節的重點。
2
閉包使用場景
從本節日後的全文,若無特殊說明,提到閉包都指的是計算機領域的閉包。閉包的使用場景是很豐富的,我簡單的舉幾個例子:
由於閉包只有在被調用時才執行操做,因此它能夠被用來定義控制結構。例如:在 Smalltalk 語言中,全部的控制結構,包括分歧條件(if/then/else)和循環(while 和 for),都是經過閉包實現的。用戶也可使用閉包定義本身的控制結構。
多個函數可使用一個相同的環境,這使得它們能夠經過改變那個環境相互交流。
閉包能夠用來實現對象系統。
以上所講的都是脫離語言講的使用場景,固然結合各個語言其使用場景能夠演化出多種多樣,這些都不是本文討論的重點,在此忽略不提。
3
C/C++ 中的閉包思想
3.1
C 中相似閉包的結構
在 C 語言中,支持回調函數的庫有時在註冊時須要兩個參數:一個函數指針,一個獨立的 void*指針用以保存用戶數據。這樣的作法容許回調函數恢復其調用時的狀態。這樣的慣用法在功能上相似於閉包,但語法上有所不一樣。
3.2
C++ 中相似閉包的結構
C++容許經過重載operator()來定義函數對象。這種對象的行爲在某種程度上與函數式編程語言中的函數相似。它們能夠在運行時建立,保存狀態,可是不能如閉包通常隱式獲取局部變量。C++標準委員會正在考慮兩種在 C++中引入閉包的建議(它們都稱爲 lambda 函數)。這些建議間主要的區別在於一種默認在閉包中儲存所有局部變量的拷貝,而另外一種只存儲這些變量的引用。這兩種建議都提供了能夠覆蓋默認行爲的選項。若這兩種建議之一被接受,則能夠寫以下代碼:
void foo(string myname) { typedef vector<string> names; int y; names n; // ... names::iterator i = find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;}); // 'i' is now either 'n.end()' or points to the first string in 'n' // 'i' 如今是'n.end()'或指向'n'中第一個 // 不等於'myname'且長度大於'y'的字符串}
以上簡單的介紹了在 C/C++ 中存在的相似閉包的思想供你們參考。
4
python 預備知識之變量與做用域
本節開始以 python 語言爲例來一步步拆解閉包,本節核心是講講變量相關的預備知識,若你對 python 的變量做用域已經是很清楚了則可跳過直接看下一小結。
我定義一個函數 get_name,其 name 做爲函數 get_name 的局部變量,而後咱們在這個函數外部來獲取這個 name 的值,請結合代碼思考一下最終的執行後的結果是什麼?
def get_name(): name = "haishiniu"
print(name)
咱們先給出結論: 執行以後的結果是 NameError: name 'name' is not defined。如何,你答對了嗎?爲什麼會輸出這樣的結果呢?
爲了解釋這個點咱們先看一下在 python 中,一個變量被解釋器解釋的時候,其規則是怎麼樣的。當一個變數被使用時,會遵循 LEGB 的規則,也就是 Local、Enclosing、Global 與 Builtins。
1.Local 很好理解,即做用於同一做用域的局部名稱
2.Enclosing 即 Enclosing Scope,閉包中的核心,後續會詳細解釋
3.Global 全局名稱
4.Builtins 內建,好比一些內建的函數: str()、int()...
那什麼是 Enclosing Scope ?想要有 Enclosing Scope 首先都有 Scope 的存在,而函數就是建立 Scope 的方式。
上方會報錯的代碼中,函數 get_name 的建立就產生了一個 Scope,而 name 就在這個 Scope 中。那麼根據 LEGB 查詢原則,咱們能夠構造如下的代碼,來建立一種 Local 中沒有查詢到,須要到 Enclosing 中查詢的狀況。
def get_scope(): name = "haishiniu" def get_name(): print(name)
# Output: "haishiniu"get_scope()# NameError: name 'name' is not definedprint(name)
當在 get_name 函數內部使用 name 的時候,遵循 LEGB 原則,因爲 Local 中沒有找到名爲 name 的變量,因而到 Enclosing 中尋找,即函數 get_scope 所建立的 Scope 中去尋找,而後使用這個處於 get_name 函數外層的變量。然而如同上面的例子同樣,隨着 get_scope 函數的運行結束,name 也隨之消亡了,咱們在外層使用 name 一樣是行不通的。
那麼有沒有什麼方法可讓咱們脫離 get_scope 函數自己的做用範圍,即能不能在 get_scope 函數結束運行以後讓局部變量 name 還能夠被訪問獲得呢?答案就是閉包。
5
python 閉包
對上節中的代碼進行修改符合 python 對閉包的定義,可獲得以下代碼。
def get_scope(): name = "haishiniu" def get_name(): print(name) return get_name
test = get_scope()test() # Output: "haishiniu"
在通常情的況下,函數中的局部變量僅在函數的執行期間可用,一旦 get_scope() 執行事後,咱們會認爲 name 變量將再也不可用。然而真實狀況是咱們成功輸出了 name 的值,即使此時 name 函數早已經執行結束 -> 這種狀況下便造成了一個閉包。
因爲 get_scope() 返回了 get_name,且 get_name 中使用了處於 get_scope Scope 中的變量 name,因而 get_name 將 name 捕獲,造成了閉包,此時 name 即是一個自由變量。
再來回看 閉包的定義:閉包是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便已經離開了創造它的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。
一句話總結: 閉包是持有外部環境變量的函數。
5.1
閉包沒法修改自由變量
本小結是 python 開發面試中的常客,請務必理解掌握。
這裏的沒法修改是指不能改變自由變量的地址。咱們先看一段代碼:
def get_num_scope(): num = 1 def get_num (): num = 2 print(num) print(num) get_num() print(num) return get_num
test = get_num_scope()test()
咱們看一下輸出:
1 2 1 2
能夠看到 num = 2 只能在 get_num() 內部生效,而做爲閉包一部分的自由變量 num 的值不管如何始終爲 1,沒法改變。然而自由變量的值真的沒法改變嗎?事實上,因爲 int 類型在 Python 中爲不可變類型,在 x = 2 這個表達中,解釋器實質上只是把符號 num 從新分配給了內存中值爲 2 的一個 PyObject,參與閉包造成的自由變量的地址依然爲內存中值 1 的地址,因此在這個現象中沒法改變閉包的值實質上源自 Python 自己的特性,而非閉包之機制。
對於字典以及數組這類可變類型,是能夠對自由變量值作出改變的。咱們再來看一段代碼:
def get_list_data_scope(): list_data = [1] def get_list_data(): list_data.append(2) print(list_data) print(list_data) get_list_data() print(list_data) return get_list_data
test = get_list_data_scope()test()
咱們在看一下輸出結果:[1 ][1, 2][1, 2][1, 2, 2]
經過以上案例能夠看出 Python 在內部實現閉包時,與嵌套函數所綁定的實際上是自由變量的地址,咱們是能夠成功改變地址指向以內容的,而沒法改變造成閉包變量地址之自己。
5.2
循環與閉包配合
本小結是 python 開發面試中的常客,請務必理解掌握。不知道你出去面試的時候有沒有碰到過相似的一個題目:
func_list = []for i in range(3): def multi_f(): return i * 2 func_list.append(multi_f)
for f in func_list: print(f())
請問上述片斷輸出的結果是什麼呢?記得三年前出去面試的時候 我就傻傻分不清的說輸出的結果是:0,2,4。而後回去後我從新寫了這段代碼執行以後輸出的結果是:4,4,4。
這是爲何呢?在以前解釋閉包這個概念的時候有提到過,閉包中的自由變量來源必須是 Enclosing Scope 中的變量,而 Python 的中的循環並無 Scope 這個概念,咱們經過一個代碼片斷看一下:
for i in range(100): out_put = i + 1
print(out_put)
輸出的結果爲:100。
out_put 是在循環中定義的變量,但實際上 Python 中的循環並不構成一個 Scope,因此實際上循環結束後咱們依然能夠訪問 out_put,天然而然這個值就是最後一次循環獲得的結果。此時也就不難解釋以前的代碼爲什麼輸出了 4, 4, 4,因爲 i 並不知足成爲自由變量的資格(不存在 Scope),故在調用 f() 時咱們拿到的 i 值始終爲 2。若要實現循環中的閉包,咱們只須要再加一個函數,造成一個 Scope 就能夠實現這個需求了。咱們看一下代碼實現:
func_list = []
for i in range(3): def get_data_scope(x): def get_data(): return x * 2 return get_data func_list.append(get_data_scope(i))
for f in func_list: print(f())
輸出結果爲:0 2 4
6
從閉包實現中間件功能
在上篇處理中間件的問題時候查看了 python 主流框架的 中間件的實現源碼,本次想結合閉包來實現一個類中間件的功能,主要是分裝一個類 server 服務端對外提供服務,主要實現:
使用裝飾器 @server.add_middleware 添加自定義中間件
用裝飾器 @server.add_func('core_func_name') 添加自定義核心件
使用 Server.initilize() 進行封裝初始化後,能夠直接經過 Server.core_func_name() 來運行已經被全部自定義中間件包裹的自定義核心件。
在具體實現中,_load_middleware 這個方法經過循環和閉包把中間件一層一層包裹到核心件上去,最後返回最外層的入口。
咱們看一下實現代碼:
# 在會話中保存上下文class Context(): def __init__(self): self._next = []
@property def next(self): return self._next
class Server(): def __init__(self): self._middlewares = [] # 中間件隊列 self._funcs = {} # 自定義核心件映射容器
def add_middleware(self, middleware_func): """ 添加中間件 """ self._middlewares.append(middleware_func) return middleware_func
def add_func(self, name): """ 註冊自定義核心件 """ def decorate(func): self._funcs.setdefault(name, func) return func return decorate
def _load_middleware(self, ctx, func): """ 加載中間件 """ def next(*args, **kwargs): return func(ctx, *args, **kwargs)
for middleware in reversed(self._middlewares): # 使用閉包來封裝中間件 def f(middleware=middleware, next=next): def new_next(*args, **kwargs): ctx._next = next return middleware(ctx, *args, **kwargs)
return new_next next = f() return next
def _wrap(self, func): def f(*args, **kwargs): ctx = Context() return self._load_middleware(ctx, func)(*args, **kwargs) return f
def initilize(self): """ 初始化服務 """ for name, func in self._funcs.items(): self.__setattr__(name, self._wrap(func))
@server.add_middlewaredef the_first_middleware(ctx, *args, **kwargs): print("The first middleware ") return ctx.next(*args, **kwargs)
@server.add_middlewaredef the_second_middleware(ctx, *args, **kwargs): print("The second middleware") return ctx.next(*args, **kwargs)
@server.add_middlewaredef the_last_middleware(ctx, *args, **kwargs): print("The last middleware") return ctx.next(*args, **kwargs)
@server.add_func('core_func')def core_func(ctx, *args, **kwargs): return "The core function "
server = Server()
server.initilize()print(server.core_func())
輸出結果:
原創不易,只願能幫助那些須要這些內容的同行或剛入行的小夥伴,你的每次 點贊、分享 都是我繼續創做下去的動力,我但願能在推廣 python 技術的道路上盡我一份力量,感謝你們。
加入python學習交流微信羣,請後臺回覆「入羣」
推薦閱讀:
本文分享自微信公衆號 - python編程軍火庫(PythonCoder1024)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。