也許你已經以爲本身能夠熟練使用python並能勝任許多開發任務,因此這篇文章是在浪費你的時間。不過彆着急,咱們先從一個例子開始:html
i = 0 def f(): print(i) i += 1 print(i) f() print(i)
猜猜看輸出是什麼?你會說不就是0,1,1麼,真的是這樣嗎?python
> python test.py Traceback (most recent call last): File "a.py", line 7, in <module> f() File "a.py", line 3, in f print(i) UnboundLocalError: local variable 'i' referenced before assignment
這是爲何?若是你還不清楚產生錯誤的緣由,那就請繼續往下閱讀吧!es6
本文索引
變量的做用域,這是一個老生常談的問題了。bash
在python中做用域規則能夠簡單的概括爲LEGB原則
,也就是說,對於一個變量name
,首先會從當前的做用域開始查找,若是它不在函數裏那就從global開始,沒找到就查找builtin做用域,若是它位於函數中,就先從local做用域查找,接着若是當前的函數是一個閉包,那麼就查找外層閉包的做用域,也就是規則中的E
,接着是global和builtin,若是都沒找到name
這個變量,則拋出NameError
。閉包
那麼咱們來看一段代碼:函數
i = 100 def f(): print(i)
在這段代碼中,print位於builtin做用域,i位於global,那麼:測試
至此名字查找結束,調用找到的函數,輸出結果100。ui
如今你可能更加疑惑了,既然查找規則按照LEGB
的方向進行,那麼test.py中的f不就應該找到i爲global中的變量嗎,爲何會報錯呢?code
在揭曉答案以前,咱們先複習一下名字隱藏。htm
它是指一個聲明在局部做用中的名字會隱藏外層做用域中的同名的對象。許多語言都遵照這一特性,python也不例外。
那麼暫時性死區是什麼呢?這是es6的一個概念,當你在局部做用域中定義了一個非全局的名字時,這個名字會綁定在當前做用域中,並將外部做用域的同名對象隱藏:
var i = 'hello' function f() { i = 'world' let i }
這段代碼中函數中的i被綁定在局部做用域(也就是函數體內)中,在綁定的做用域中可見,並將外部的名字隱藏,而對一個未聲明的局部變量賦值會致使錯誤,因此上面的代碼會引起ReferenceError: i is not defined
。
對於python來講也是同樣的問題,python代碼在執行前首先會被編譯成字節碼,這就會致使某些時候實際執行的程序會和咱們看到的產生出入。不過咱們有dis
模塊幫忙,它能夠輸出python對象的字節碼,下面咱們就來看下通過編譯後的f
:
> dis(f) 2 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (i) 4 CALL_FUNCTION 1 6 POP_TOP 3 8 LOAD_CONST 1 ('a') 10 STORE_FAST 0 (i) 4 12 LOAD_GLOBAL 0 (print) 14 LOAD_FAST 0 (i) 16 CALL_FUNCTION 1 18 POP_TOP 20 LOAD_CONST 0 (None) 22 RETURN_VALUE
字節碼的解釋在這裏。
其中LOAD_FAST
和STORE_FAST
是讀取和存儲local做用域的變量,咱們能夠看到,i變成了局部做用域的變量!而對i的賦值早於i的定義,因此報錯了。
產生這種現象的緣由也很簡單,python對函數的代碼是獨立編譯的,若是未加說明而在函數內對一個變量賦值,那麼就認爲你定義了一個局部變量,從而把外部的同名對象屏蔽了。這麼作無可厚非,畢竟python沒有獨立的聲明一個局部變量的語法,但結果就會形成咱們看到的相似暫時性死區的現象。因此請容許我把es6的概念套用在python身上。
既然知道問題的癥結在於python沒法區分局部變量的聲明和定義,那麼咱們就來解決它。
對於一個能夠區分聲明和定義的語言來講是沒有這種煩惱的,好比c:
int i = 0; void f(void) { i++; printf("%d\n", i); // 1 const char *i = "hello"; printf("%s\n", i); // "hello" }
python中不能這麼作,可是咱們能夠換一個思路,聲明一個變量是全局做用域的,這樣不就解決了嗎?
global
運算符就是爲了這個目的而存在的,它聲明一個變量始終是全局做用域的變量,所以只要存在global聲明,那麼當前做用域裏的這個名字就是一個對同名全局變量的引用。改進後的函數以下:
def f(): global i print(i) i += 1 print(i)
如今運行程序就會是你想要的結果了:
> python test.py 0 1 1
若是你仍是不放心,那麼咱們再來看看字節碼:
> dis(f) 3 0 LOAD_GLOBAL 0 (print) 2 LOAD_GLOBAL 1 (i) 4 CALL_FUNCTION 1 6 POP_TOP 4 8 LOAD_CONST 1 ('a') 10 STORE_GLOBAL 1 (i) 5 12 LOAD_GLOBAL 0 (print) 14 LOAD_GLOBAL 1 (i) 16 CALL_FUNCTION 1 18 POP_TOP 20 LOAD_CONST 0 (None) 22 RETURN_VALUE
對於i的存取已經由LOAD_GLOBAL
和STORE_GLOBAL
接手了,沒問題。
固然global
也有它的侷限性:
事實上須要引用非global名字的需求是極其常見的,所以爲了解決global的不足,python3引入了nonlocal
假設咱們有一個需求,一個函數須要知道本身被調用了多少次,最簡單的實現就是使用閉包:
def closure(): count = 0 def func(): # other code count += 1 print(f'I have be called {count} times') return func
仍是老問題,這樣寫對嗎?
答案是不對,你又製造暫時性死區啦!
>>> f=closure() >>> f() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in func UnboundLocalError: local variable 'count' referenced before assignment
這時候就要nonlocal
出場了,它聲明一個名字位於閉包做用域中,若是閉包做用域中未找到就報錯。
因此修正後的函數以下:
def closure(): count = 0 def func(): # other code nonlocal count count += 1 print(f'I have be called {count} times') return func
測試一下:
>>> f=closure() >>> f() I have be called 1 times >>> f() I have be called 2 times >>> f() I have be called 3 times >>> f2=closure() >>> f2() I have be called 1 times
如今能夠正常使用和修改閉包做用域的變量了。
固然,在函數裏修改外部變量每每會致使潛在的缺陷,但有時這樣作又是對的,因此但願你在好好了解做用域規則的前提下合理地利用它們。
做用域規則能夠總結爲下:
只要記住這些規則你就能夠和因做用域引發的各類問題說再見了。並且理解了這些規則還會爲你探索更深層次的python打下堅實的基礎,因此請將它牢記於心。