python3的變量做用域規則和nonlocal關鍵字

也許你已經以爲本身能夠熟練使用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

本文索引

LEGB原則

變量的做用域,這是一個老生常談的問題了。bash

在python中做用域規則能夠簡單的概括爲LEGB原則,也就是說,對於一個變量name,首先會從當前的做用域開始查找,若是它不在函數裏那就從global開始,沒找到就查找builtin做用域,若是它位於函數中,就先從local做用域查找,接着若是當前的函數是一個閉包,那麼就查找外層閉包的做用域,也就是規則中的E,接着是global和builtin,若是都沒找到name這個變量,則拋出NameError閉包

那麼咱們來看一段代碼:函數

i = 100
def f():
  print(i)

在這段代碼中,print位於builtin做用域,i位於global,那麼:測試

  1. 在函數f中找不到這兩個名字,因此從local向上查找,
  2. 首先f不是閉包,所以跳過閉包做用域的查找,
  3. 而後查找global,找到了i,但print還未找到,
  4. 而後查找builtin,找到了print的builtin模塊裏的一個函數。

至此名字查找結束,調用找到的函數,輸出結果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_FASTSTORE_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_GLOBALSTORE_GLOBAL接手了,沒問題。

固然global也有它的侷限性:

  • 一旦聲明global,那麼這個名字始終是global做用域的一個變量,不能夠再是局部變量
  • 名字必須存在於global裏,由於python在運行時進行名字查找,因此你的變量在global裏找不到的話對它的引用將會出錯
  • 接上一條,由於global限定了名字查找的範圍,因此像閉包做用域的變量就找不到了

事實上須要引用非global名字的需求是極其常見的,所以爲了解決global的不足,python3引入了nonlocal

使用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

如今能夠正常使用和修改閉包做用域的變量了。

總結

固然,在函數裏修改外部變量每每會致使潛在的缺陷,但有時這樣作又是對的,因此但願你在好好了解做用域規則的前提下合理地利用它們。

做用域規則能夠總結爲下:

  1. 名字查找按照LEGB規則進行,若是當前代碼在global中則從global做用域開始查找,不然從local開始
  2. builtin做用域中是內置類型和函數,因此它們老是能被找到,前提是不要在局部做用域中對它們賦值
  3. global中存放着全部定義在當前模塊和導入的名字
  4. local是局部做用域,存放在造成局部做用於的代碼中有賦值行爲的名字
  5. 閉包做用域是閉包函數的外層做用域,裏面能夠存放一些自定義的狀態
  6. global聲明一個名字在global做用域中
  7. nonlocal聲明一個名字在閉包做用域中
  8. 最重要的一條,當你在能產生局部做用域的代碼中對一個名字進行賦值,那麼這個名字就會被認爲是一個local做用域的變量從而屏蔽其餘做用域中的同名對象

只要記住這些規則你就能夠和因做用域引發的各類問題說再見了。並且理解了這些規則還會爲你探索更深層次的python打下堅實的基礎,因此請將它牢記於心。

相關文章
相關標籤/搜索