標籤:

Python進階課程筆記(二)

這一部分是關於Python的Callable。在Stackoverflow上有一個專門的問題叫做「What is a "callable" in Python」,高票回答中說:

A callable is anything that can be called.

這個回答很抽象,大雄從更具體的角度來闡述Callable這個概念——在Python中哪些是callable的?

  • function
  • closure
  • bound method
  • unbound method
  • class method
  • static method
  • functor
  • operator
  • class

先說答案,很明顯,列出的這些都是callable的。這些概念中的大部分我在工作中都有使用,包括比如closure的坑也幫助新同學調試bug的時候看到新入職的同學自己踩到過,但是對於bound methodunbound method這些概念還不是很清晰。我們也一個個來看。

3. Closure

Closure,閉包,在Python中本質上是一個函數,或者更具體來說它和Function的區別是它包含了Code和Environment,而Python中Environment又可以分為globals、locals和cells三部分。

globals和locals比較容易理解,其實就是兩個dict,分別保存了全局變數和局部變數,那這個cells是什麼?我們先來看一個非常經典的例子:

def foo():n logout_lst = []nn for i in xrange(5):n def logout():n print in logout_lst.append(logout)nn for l in logout_lst:n l()nnfoo()n

思考:這段代碼的輸出是什麼?

分析一下這段代碼,雖然這裡為了方便演示,構造了一個只有print的邏輯,你可能會質疑它的作用,但是在我們開發的過程中,就有同學在循環內部定義了類似的閉包用於引擎回調的調用,引用了外部了一個類似i的變數。例子中,在foo的函數內部,代碼def logout()定義了一個閉包(寫到這裡讓我想起了遙遠的過去寫JAVA代碼時使用的Inner Class),然後我們想使用外部變數i的值,這裡只是把它輸出出來,通常我們想要輸出的結果是列印0、1、2、3、4這幾個數字,當然中間有換行,但是最終的輸出結果是什麼呢?

5個4!

為什麼呢?我們來添加一些輸出日誌來查看一下,為了方便看輸出,我們只循環兩次來看,修改後的代碼如下:

def foo():n logout_lst = []nn for i in xrange(2):n def logout():n print "i:", i, id(i)n print "globals:", globals()n print "locals:", locals()n logout_lst.append(logout)nn for l in logout_lst:n l()n print "Cells:", l.__closure__, id(l.__closure__[0].cell_contents)n print nnfoo()n

輸出的結果如下:

i: 1 35882616nglobals: {__builtins__: <module __builtin__ (built-in)>, __file__: F:Davidnarrator.py, __package__: None, __name__: __main__, foo: <function foo at 0x022C72B0>, __doc__: None}nlocals: {i: 1}nCells: (<cell at 0x02354570: int object at 0x02238678>,) 35882616nni: 1 35882616nglobals: {__builtins__: <module __builtin__ (built-in)>, __file__: F:Davidnarrator.py, __package__: None, __name__: __main__, foo: <function foo at 0x022C72B0>, __doc__: None}nlocals: {i: 1}nCells: (<cell at 0x02354570: int object at 0x02238678>,) 35882616n

首先列印一下i的值與i這個變數的id,你可以認為這是i在Python虛擬機中的唯一編號,兩次輸出它的值都是1,id也都是一個35882616,然後輸出一下globals和locals看一下,這兩個很簡單,不做分析了。最後通過__closure屬性來看下閉包的內容:

Cells: (<cell at 0x02354570: int object at 0x02238678>,)n

這就是前面說的cells,它是一個cell對象,裡面的內容有一個int對象,通過cell_contents屬性可以查看到它的id是35882616,和i是一樣的。

可以看出,cells就是對於up-values的引用(references)注意引用

那之前的輸出就很容易理解了,引用,當後面調用閉包執行的時候,i變數值已經變成了4,那輸出i自然每次都是4。

最後,如何修改可以讓你的代碼可以按照之前的計劃正常執行呢?很簡單,不要直接使用cells中的值,而是用一個參數來讓它變成參數,就是定義這個閉包的時刻的值了。

def foo():n logout_lst = []nn for i in xrange(2):n def logout(x = i):n print "x:", x, id(x)n print "globals:", globals()n print "locals:", locals()n logout_lst.append(logout)nn for l in logout_lst:n l()n print "Cells:", l.__closure__n print nnfoo()n

輸出結果:

x: 0 37062276nglobals: {__builtins__: <module __builtin__ (built-in)>, __file__: F:Davidnarrator.py, __package__: None, __name__: __main__, foo: <function foo at 0x023E72B0>, __doc__: None}nlocals: {x: 0}nCells: Nonennx: 1 37062264nglobals: {__builtins__: <module __builtin__ (built-in)>, __file__: F:Davidnarrator.py, __package__: None, __name__: __main__, foo: <function foo at 0x023E72B0>, __doc__: None}nlocals: {x: 1}nCells: Nonen

此處,cells的內容變為了None,輸出的結果也是0和1,它們的id自然也不同。其實參數也可以寫成def logout(i = i):,內部可以使用i,但是這會造成一些困擾,個人不推薦這麼寫。

思考:那麼你以為這個坑就踩完了嗎?有沒有哪裡還可能存在問題?

def logout(x = i):這種定義雖然用在閉包里,但是其實是函數的默認參數,那麼默認參數如果使用list、dict或者python object等這樣mutable的值會怎樣?這自然是另外一個入門級的坑:

背景: 不建議在函數默認參數中使用mutable value,而保證只使用immutable value。

但有時候為了解決一個坑,可能不小心踩入另外一個坑。如果這裡使用了,比如一個list對象作為參數,那麼創建出來的這幾個閉包中的x都引用的會是同一個對象,而且,在任何一個閉包多次調用的時候,x的值都是同一個對象的引用。如果像例子中是只讀的邏輯的話,可能沒有問題,如果後面有人添加了修改的邏輯,那就呵呵呵呵了。可能會亂成一鍋粥,出現各種神奇的現象,寫這樣邏輯的人自求多福吧。

總結:理解閉包的概念,理解引用的概念,編寫代碼保持思路清晰,明確自己使用的變數存在在哪裡,是一件非常非常重要的事情,對團隊開發中避免匪夷所思令人抓狂的Bug很有幫助!

這一部分只講閉包這一個點,其實關於閉包還有很多知識點,有興趣的可以自己查閱相關資料。第三部分講解bound method和unbound method,這是我這次課程最喜歡的部分。

PS: 很多坑,你看過文章介紹,或者聽同事講過,但是寫代碼的時候有時還是會由於當時思路的混亂而饒進去,重新踩一遍,這往往難以避免,不親身經歷的坑思維上很難那麼敏感。經驗學習和知識積累的作用,是讓你從坑中往外爬的時候更快一些,回頭看那些坑印象更深刻一些。

2016年7月2日於杭州網易大廈

推薦閱讀:

TAG:Python |