預設作用域
前段時間學了下Lua,發現Lua的預設作用域和Python是相反的。 Lua定義變數時預設變數的作用域是全域(global,這樣說不是很準確,Lua在執行x = 1這樣的語句時會從當前環境開始一層層往上找x,只有在找不到x的情況下才定義全域變數)的,而Python定義變數時預設變數的作用域是局部(local)的(目前區塊)。另外,Lua可以再定義變數時在變數前面加上local關鍵字來定義局部變量,而Python沒有類似的關鍵字,Python的變數只能定義在目前區塊中。
我們知道,全域變數是不好的,而局部變數是好的,寫程式應該盡量使用局部變數。所以一開始時我覺得Python的這種約定比較好,它的優點就是可以少打些字。寫Lua程式時不斷在心底默念“勿忘local,勿忘local”,然而還是有時會出現幾個漏網之魚並引發了一些神奇的bug。
閉包
第一次意識到Python預設作用域的問題是在使用閉包時碰到的。關於閉包,Lua教學上有一段程式碼:
function new_counter() local n = 0 local function counter() n = n + 1 return n end return counter end c1 = new_counter() c2 = new_counter() print(c1()) -- 打印1 print(c2()) -- 打印1 print(c1()) -- 打印2 print(c2()) -- 打印2
閉包的本質可以參考SICP第三章的環境模型。這裡可以簡單的想像為函數counter有一個私有成員n。
現在問題來了:我想用Python實現同樣功能的閉包?
首先直接從Lua程式碼依葫蘆畫瓢改寫成Python程式碼:
def new_counter(): n = 0 def counter(): n = n + 1 return n return counter
然後傻眼:這個程式不能運行,第4行存取了未賦值的變數n。出錯的原因並非是Python不支援閉包,而是Python的賦值操作存取不了上一層的變數n(實際上,Python認為這是定義局部變量,而非賦值。在Python中定義局部變數與賦值運算在語法上是衝突的,Python乾脆只支援可重定義的定義語句)。由於Python預設作用域是局部的,所以當程式運行到n = n + 1時,Python認為這是一個變數定義操作,於是創建了一個(未初始化的)局部變數n——並且順利地覆蓋了new_counter這一層的n-然後試著把n + 1賦值給n,但n未初始化,n + 1沒辦法計算,所以程式報錯。
可以用個小技巧來實現閉包賦值的功能:
def new_counter(): n = [0] def counter(): n[0] = n[0] + 1 return n[0] return counter
這裡n[0] = n[0] + 1不會出錯
這裡n[0] = n[0] + 1不會出錯的原因是這裡的等號和前面n = n + 1的等號意義不一樣。 n[0] = n[0] + 1中的等號意思是修改n的某個屬性。事實上這個等號最後呼叫了list的__setitem__方法。而n = n + 1中的等號意思是在當前環境將n + 1這個值綁定到符號n中(如果當前環境已存在符號n,就覆蓋它)。 另外題外話:打死我不用這種寫法,多難看吶。反正Python是物件導向語言,要實作計數器,大不了寫個類別。 定義與賦值的分離先總結Python與Lua的預設作用域的特點:1、 Lua預設作用域是全域的,寫程式時要牢記local關鍵字(除非確實要定義全域變數) ,不小心忘了local也不會提示,就等著糾bug吧。 2、 Python預設作用域是局部的,雖然寫程式的思維負擔少些,但是喪失了對上層變數賦值的能力(可以改,但會讓語言更混亂)。 看來兩種預設作用域都有問題?個人認為,出現以上問題的原因是:Python和Lua沒有實現定義和賦值的分離。在Python和Lua中,像x = 1這樣的語句既可以表示定義,也可以表示賦值。其實不只是這兩種語言,其實很多高階語言都沒有實現定義和賦值的分離。定義和賦值兩者功能上很像,但是它們本質上是有差異的。 下面以x = 1為例解釋定義與賦值:定義的意思是:在當前環境中註冊符號x,並初始化為1。如果x已經存在,則報錯(不允許重定義)或覆蓋(允許重定義)。 賦值的意思是:從目前環境開始,一層層往上找直到第一次找到符號x,把它的值修改成1。如果找不到就報錯(變數不存在)。 現在我們稍微修改一下Python來實現定義和賦值的分離:用「:=」表示定義,用「=」表示賦值。然後重寫那個不能運行的new_counter例子(Python中賦值操作和定義局部變數衝突,換句話說,Python其實沒有賦值操作,所以我們只需簡單的把「=」全換成「:=」就行了) ,看看它錯在哪裡:def new_counter(): n := 0 def counter(): n := n + 1 return n return counter
def new_counter(): n := 0 def counter(): n = n + 1 return n return counter
最後說一些Lua的情況。 Lua感覺上就把定義和賦值的分離實現了一半。帶有local關鍵字的等號語句肯定是定義了。問題是不帶local的等號語句。對於這種語句Lua是這樣做的:先試圖做賦值,如果賦值失敗(變數不存在),就在最外層環境(全域環境)定義變數。也就是說,不帶local的等號語句把定義和賦值混在一起了。另外,如果實現了定義和賦值的分離,就不需要考慮預設作用域的問題了──定義全部是在目前環境下定義,都是定義局部變數。我實在想不出在一個函數體或什麼區塊中定義全域變數的好處。