python中常見的函式陷阱
本地變數是靜態檢測的
正如我們所知道的一樣,Python定義的在一個函式中進行分配的變數名是預設為本地變數的,它們存在於函式的作用域並只在函式執行時存在。Python是靜態檢測Python的本地變數的,當編譯def程式碼時,不是通過發現賦值語句在執行時進行檢測的。這導致了在Python中入門者最為常見的陷阱之一。
一般來說,沒有在函式中賦值的變數名會在整個模組檔案中查詢。
X = 99
def selector():
print(X)
>>>selector()
99
這裡,函式中的X被解析為模組中的X。但是如果在引用之後增加了一個賦值語句,看看會發生什麼。
你得到了一個未定義變數名的錯誤,但其原因是微妙的。在互動模式下輸入或從一個模組檔案匯入時,Python讀入並編譯這段程式碼。在編譯時,Python看到了對X的賦值語句,並且決定了X將會在函式中的任意地方都將是本地變數名。但是,當函式實際執行時,因為在print執行時賦值語句並沒有發生,Python告訴你正在使用一個未定義的變數名。根據其變數名規則,本地變數X是在其被賦值前就被使用了。實際上,任何在函式體內的賦值將會使其成為一個本地變數名。Import、=、巢狀def、巢狀類等,都會受這種行為的影響。def selector(): print(X) X = 88 >>>selector() UnboundLocalError: local variable 'X' referenced before assignment
產生這種問題的原因在於被賦值的變數名在函式內部室當作本地變數來對待的,而不是僅僅在賦值以後的語句中才被當做是本地變數。實際上,前一個例子是最含糊不清的:是希望列印一個全域性變數X之後建立一個本地變數X,還是這真的是一個程式錯誤?因為Python會在函式中將X作為本地變數,它就是一個錯誤。如果你真的想要列印全域性變數X,需要在一個global語句中宣告這一點。
def selector():
global X
print(X)
X = 88
>>>selector()
99
記住,儘管這樣,這一位置的賦值語句同樣會改變全域性變數X,而不是一個本地變數。在函式中,不可能同時使用同一個簡單變數名的本地變數和全域性變數。如果真的希望列印全域性變數,並在之後設定一個有相同變數名的本地變數,匯入上層的模組,並使用模組的屬性標記來獲得其全域性變數。
點號運算(.X這部分)從名稱空間物件中獲取了變數的值。互動模式下的名稱空間是一個名為__main__的名稱空間,所以__main__.X得到了全域性變數版本的X。X = 99 def selector(): import __main__ print(__main__.X) X = 88 print(X) selector() 99 88
在Python最近的版本中,已經針對這種情況釋出了更為專用的“unbound local”錯誤訊息來改進這一問題;然而這個陷阱仍然普遍存在。
預設和可變物件
預設引數是在def語句執行時評估並儲存的,而不是在這個函式呼叫時。從內部來講,Python會將每一個預設引數儲存成一個物件,附加在這個函式本身。
這也就是通常我們想要的:因為預設引數是在def時被評估的,如果必要的話,它能夠從整個作用域內儲存值,但是因為預設引數在呼叫之間都儲存了一個物件,必須對修改可變的預設引數十分小心。例如,下面的函式使用了一個空列表作為預設引數,並在函式每次呼叫時都對它進行了改變。
def saver(x=[]):
x.append(1)
print(x)
>>>saver([2])
[2,1]
>>>saver()
[1]
>>>saver()
[1,1]
>>>saver()
[1,1,1]
有些人把這種行為當作一種特性。因為可變型別的預設引數在函式呼叫之前儲存了它們的狀態,從某種意義上講它們能夠充當C語言中的靜態本地函式變數的角色。在一定程度上,它們工作起來就像全域性變數,但是它們的變數名對於函式來說是本地變數,而且不會與程式中的其他變數名發生衝突。
儘管這樣,對於大多數人來說,這看起來就像一個陷阱,特別是第一次遇到這樣的情況的時候。在Python中有更好的辦法在呼叫之間儲存狀態(例如,使用類)。
此外,可變型別預設引數記憶起來比較困難(理解起來也不容易)。它們的值取決於預設物件構建的時間。在上一個例子中,其中只有一個列表物件作為預設值,這個列表物件是在def語句執行時被建立的。不會每次函式呼叫時都得到一個新的列表,所以每次新的元素加入後,列表會變大,對於每次呼叫,它都沒有重置為空列表。
如果這不是你想要的行為的話,在函式主體的開始對預設引數進行簡單的拷貝,或者將預設引數值的表示式移至函式體內部。只要值是存在於程式碼中,而這部分程式碼在函式每次執行時都會執行的話,你就會每次都得到一個新的物件。
def saver(x=None):
if x is None:
x = []
x.append(1)
print(x)
>>>saver([2])
[2,1]
>>>saver()
[1]
>>>saver()
[1]
使用函式屬性:
def saver():
saver.x.append(1)
print(saver.x)
>>>saver.x = []
>>>saver()
[1]
>>>saver()
[1,1]
>>>saver()
[1,1,1]
該函式的名稱對於函式自身來講是全域性的,但是,它不需要宣告,因為它在函式內部是不會直接修改的。這並不是總以完全的方式使用,但是,當這樣編寫程式碼的時候,一個物件到函式的附加總是更加明確(並且肯定更容易理解)。 沒有return語句的函式 在Python函式中,return(以及yield)語句是可選的。當一個函式沒有精確的返回值的時候,函式在控制權從函式主體脫離時,函式將會退出。從技術上講,所有的函式都返回了一個值,如果沒有提供return語句,函式將自動返回None物件:
def proc(x):
print(x)
>>>x = proc('testing 123...')
testing 123...
>>>print(x)
None
沒有return語句的函式與Python對應於一些其他語言中所謂的“過程”是等效的。它們常被當做語句,並且None這個結果被忽略了,就像它們只是執行任務而不需要計算有用的結果一樣。
瞭解這些內容是值得的,因為如果你想要嘗試使用一個沒有返回值的函式的結果時,Python不會告訴你。例如,將一個列表新增方法的結果賦值不會導致錯誤,但是得到的會是None,而不是改變後的列表。
>>>list = [1,2,3]
>>>list = list.append(4)
>>>print(list)
None
這樣的函式執行任務也會有副作用,就是它們往往設計成語句來執行,而不是表示式。
以上內容來自Python學習手冊第四部分第20章,內容有刪減。