Python中的LEGB規則


目標

  • 命名空間和作用域——Python從哪里查找變量名?
  • 我們能否同時定義或使用多個對象的變量名?
  • Python查找變量名時是按照什么順序搜索不同的命名空間?

命名空間與作用域的介紹

命名空間

大約來說,命名空間就是一個容器,其中包含的是映射到不同對象的名稱。你可能已經聽說過了,Python中的一切——常量,列表,字典,函數,類,等等——都是對象。

這樣一種“名稱-對象”間的映射,使得我們可以通過為對象指定的名稱來訪問它。舉例來說,如果指定一個簡單的字符串a_string = "Hello string",我們就創建了一個對象“Hello string”的引用,之后我們就可以通過它的名稱a_string來訪問它。

我們可以把命名空間描述為一個Python字典結構,其中關鍵詞代表名稱,而字典值是對象本身(這也是目前Python中命名空間的實現方式),如:

a_namespace = {'name_a':object_1, 'name_b':object_2, ...}

現在比較棘手的是,我們在Python中有多個獨立的命名空間,而且不同命名空間中的名稱可以重復使用(只要對象是獨一無二的),比如:

a_namespace = {'name_a':object_1, 'name_b':object_2, ...}
b_namespace = {'name_a':object_3, 'name_b':object_4, ...}

舉例來說,每次我們調用for循環或者定義一個函數的時候,就會創建它自己的命名空間。命名空間也有不同的層次(也就是所謂的“作用域”),我們會在下一節詳細討論。

作用域

在上一節中,我們已經學習到命名空間可以相互獨立地存在,而且它們被安排在某個特定層次,由此引出了“作用域”的概念。Python中的“作用域”定義了一個“層次”,我們從其中的命名空間中查找特定的“名稱-對象”映射對。

舉例來說,我們來考慮一下下面的代碼:

i = 1

def foo():
    i = 5
    print(i, 'in foo()')

print(i, 'global')

foo()
1 global
5 in foo()

我們剛剛兩次定義了變量名i,其中一次是在函數foo內。

  • foo_namespace = {'i':object_3, ...}
  • global_namespace = {'i':object_1, 'name_b':object_2, ...}

這樣的話,如果我們要打印變量i的值,Python如何知道應該搜索哪個命名空間呢?到此LEGB規則就開始起作用了,我們將在下一節進行討論。

提示:

如果我們想要打印出全局變量與局部變量的字典映射,我們可以使用函數globals()locals():

#print(globals()) # prints global namespace
#print(locals()) # prints local namespace

glob = 1

def foo():
    loc = 5
    print('loc in foo():', 'loc' in locals())

foo()
print('loc in global:', 'loc' in globals())    
print('glob in global:', 'foo' in globals())
loc in foo(): True
loc in global: False
glob in global: True

通過LEGB規則對變量名進行作用域解析

我們已經知道了多個命名空間可以獨立存在,而且可以在不同的層次上包含相同的變量名。“作用域”定義了Python在哪一個層次上查找某個“變量名”對應的對象。接下來的問題就是:“Python在查找‘名稱-對象’映射時,是按照什么順序對命名空間的不同層次進行查找的?”

答案就是:使用的是LEGB規則,表示的是Local -> Enclosed -> Global -> Built-in,其中的箭頭方向表示的是搜索順序。

  • Local 可能是在一個函數或者類方法內部。
  • Enclosed 可能是嵌套函數內,比如說 一個函數包裹在另一個函數內部。
  • Global 代表的是執行腳本自身的最高層次。
  • Built-in 是Python為自身保留的特殊名稱。

因此,如果某個name:object映射在局部(local)命名空間中沒有找到,接下來就會在閉包作用域(enclosed)進行搜索,如果閉包作用域也沒有找到,Python就會到全局(global)命名空間中進行查找,最后會在內建(built-in)命名空間搜索(注:如果一個名稱在所有命名空間中都沒有找到,就會產生一個NameError)。

注意:

命名空間也可以進一步嵌套,例如我們導入模塊時,或者我們定義新類時。在那些情形下,我們必須使用前綴來訪問那些嵌套的命名空間。我用下面的代碼來說明:

import numpy
import math
import scipy

print(math.pi, 'from the math module')
print(numpy.pi, 'from the numpy package')
print(scipy.pi, 'from the scipy package')
3.141592653589793 from the math module
3.141592653589793 from the numpy package
3.141592653589793 from the scipy package

(這也就是為什么當我們通過“from a_module import *”導入模塊時需要格外小心,因為這樣會把變量名加載到全局命名空間中,而且可能覆蓋已經存在的變量名)

1. LG - 局部與全局作用域

例 1.1

作為熱身練習,我們先忘記LEGB規則中的外圍函數(E)和內建(B)兩個作用域,只考慮LG——局部與全局作用域。

下面的代碼輸出是怎樣的?

a_var = 'global variable'

def a_func():
    print(a_var, '[ a_var inside a_func() ]')

a_func()
print(a_var, '[ a_var outside a_func() ]')

a) raise an error

b) global value [ a_var outside a_func() ]

c)

global value [ a_var inside a_func() ] 
global value [ a_var outside a_func() ]

解析:

我們首先調用了a_func(),其中要打印a_var的值。根據LEGB規則,函數會首先搜索它自身的局部作用域(L),查看是否定義了a_var。因為a_func()沒有定義自己的a_var,它將會在上一層的全局作用域(G)中進行搜索,其中已經定義了a_var。

例 1.2

現在,我們在全局作用域和局部作用域中都定義變量a_var。你是否知道下面代碼的結果?

a_var = 'global value'

def a_func():
    a_var = 'local value'
    print(a_var, '[ a_var inside a_func() ]')

a_func()
print(a_var, '[ a_var outside a_func() ]')

a)

raises an error

b)

local value [ a_var inside a_func() ]
global value [ a_var outside a_func() ]

c)

global value [ a_var inside a_func() ]  
global value [ a_var outside a_func() ]

解析:

當我們調用a_func()時,首先會在局部作用域中查找a_var,因為a_var已經在局部作用域進行了定義,所以它在局部作用域所賦的值就會被打印出來。注意這並不會影響全局變量,因為是在不同的作用域當中。

不過,如果使用global關鍵字,也可以修改全局變量。如下所示:

a_var = 'global value'

def a_func():
    global a_var
    a_var = 'local value'
    print(a_var, '[ a_var inside a_func() ]')

print(a_var, '[ a_var outside a_func() ]')
a_func()
print(a_var, '[ a_var outside a_func() ]')
global value [ a_var outside a_func() ]
local value [ a_var inside a_func() ]
local value [ a_var outside a_func() ]

但是我們必須注意順序:如果我們沒有明確地告訴Python我們要使用的是全局作用域,而是直接嘗試修改變量值的話,就很容易產生UnboundLocalError。(記住,賦值操作的右半部分是先執行的)

a_var = 1

def a_func():
    a_var = a_var + 1
    print(a_var, '[ a_var inside a_func() ]')

print(a_var, '[ a_var outside a_func() ]')
a_func()
a_var = 1

def a_func():
    a_var = a_var + 1
    print(a_var, '[ a_var inside a_func() ]')

print(a_var, '[ a_var outside a_func() ]')
a_func()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)

<ipython-input-4-a6cdd0ee9a55> in <module>()
      6
      7 print(a_var, '[ a_var outside a_func() ]')
----> 8 a_func()


<ipython-input-4-a6cdd0ee9a55> in a_func()
      2
      3 def a_func():
----> 4     a_var = a_var + 1
      5     print(a_var, '[ a_var inside a_func() ]')
      6


UnboundLocalError: local variable 'a_var' referenced before assignment


1 [ a_var outside a_func() ]

2. LEG - 局部,閉包與全局作用域

現在我們引入外圍函數(E)作用域的概念。根據順序“Local-> Enclosed -> Global”,你能否猜出下面代碼的輸出結果?

例 2.1

a_var = 'global value'

def outer():
    a_var = 'enclosed value'

    def inner():
        a_var = 'local value'
        print(a_var)

    inner()

outer()

a)

global value

b)

enclosed value

c)

local value

解析:

我們來快速總結一下剛才做了什么:我們調用了outer(),它定義了一個局部變量a_var(在全局作用域已經存在一個a_var)。接下來,outer()函數調用了inner(),該函數也定義了一個名稱為a_var的變量。在inner()內的print()函數首先在局部作用域內搜索(L->E),因此會打印出在局部作用域內所賦的值。

類似於上一節所說的global關鍵字,我們也可以在內部函數中使用nonlocal關鍵字來明確地訪問外部(外圍函數)作用域的變量,也可以修改它的值。

注意nonlocal關鍵字是在Python 3.x才新加的,而且在Python 2.x中沒有實現(目前還沒有)。

a_var = 'global value'

def outer():
       a_var = 'local value'
       print('outer before:', a_var)
       def inner():
           nonlocal a_var
           a_var = 'inner value'
           print('in inner():', a_var)
       inner()
       print("outer after:", a_var)
outer()
outer before: local value
in inner(): inner value
outer after: inner value

3. LEGB - 局部,外圍,全局,內建

為了完整理解LEGB規則,我們來學習內建作用域。在這里,我們定義“自己的”長度函數,碰巧跟內建的len()函數同名。如果我們執行下面的代碼,你認為輸出結果是什么?

例 3

a_var = 'global variable'

def len(in_var):
    print('called my len() function')
    l = 0
    for i in in_var:
        l += 1
    return l

def a_func(in_var):
    len_in_var = len(in_var)
    print('Input variable is of length', len_in_var)

a_func('Hello, World!')

a)

raises an error (conflict with in-built `len()` function)

b)

called my len() function
Input variable is of length 13

c)

Input variable is of length 13

解析:

因為完全相同的名稱也可以映射到不同的對象——只要名稱是在不同的命名空間中——因此重新使用名稱len來定義我們自己的長度函數是沒有問題的(這只是為了示范,不是必須的)。我們在Python中按照L -> E -> G -> B層次進行搜索,a_func()函數在嘗試搜索內建(B)命名空間之前,首先會在全局作用域(G)中發現len()。

自我評估練習

現在我們已經完成了一些練習,我們來快速地檢查一下效果。那么再問一次:下面的代碼會輸出什么?

a = 'global'

def outer():

    def len(in_var):
        print('called my len() function: ', end="")
        l = 0
        for i in in_var:
            l += 1
        return l

    a = 'local'

    def inner():
        global len
        nonlocal a
        a += ' variable'
    inner()
    print('a is', a)
    print(len(a))


outer()

print(len(a))
print('a is', a)
a is local variable
called my len() function: 14
6
a is global

總結

我希望這個簡短的教程能有助於理解Python中的一個基本概念,即使用LEGB規則的作用域解析順序。我鼓勵你明天重新查看一下這些代碼片段(作為一個小的自我評估),檢查一下你是否可以准確預測所有的輸出值。

經驗法則

在實際中,在函數作用域內修改全局變量通常是個壞主意,因為這經常造成混亂或者很難調試的奇怪錯誤。如果你想要通過一個函數來修改一個全局變量,建議把它作為一個變量傳入,然后重新指定返回值。

例如:

a_var = 2

def a_func(some_var):
    return 2**3

a_var = a_func(a_var)
print(a_var)
8

答案

為了防止你無意中看到,我把答案寫成了二進制形式。如果要顯示成字符形式,你只需要執行下面的代碼行:

print('Example 1.1:', chr(int('01100011',2)))

print('Example 1.2:', chr(int('01100010',2)))

print('Example 2.1:', chr(int('01100011',2)))

print('Example 3.1:', chr(int('01100010',2)))
# Execute to run the self-assessment solution

sol = "000010100110111101110101011101000110010101110010001010"\
"0000101001001110100000101000001010011000010010000001101001011100110"\
"0100000011011000110111101100011011000010110110000100000011101100110"\
"0001011100100110100101100001011000100110110001100101000010100110001"\
"1011000010110110001101100011001010110010000100000011011010111100100"\
"1000000110110001100101011011100010100000101001001000000110011001110"\
"1010110111001100011011101000110100101101111011011100011101000100000"\
"0011000100110100000010100000101001100111011011000110111101100010011"\
"0000101101100001110100000101000001010001101100000101001100001001000"\
"0001101001011100110010000001100111011011000110111101100010011000010"\
"1101100"

sol_str =''.join(chr(int(sol[i:i+8], 2)) for i in range(0, len(sol), 8))
for line in sol_str.split('\n'):
    print(line)

警告:For循環變量“泄漏”到全局命名空間

與其它一些編程語言不同,Python中的for循環會使用它所在的作用域,而且把它所定義的循環變量加在后面。

for a in range(5):
    if a == 4:
        print(a, '-> a in for-loop')
print(a, '-> a in global')
4 -> a in for-loop
4 -> a in global

如果我們提前在全局命名空間中明確定義了for循環變量,也是同樣的結果!在這種情況下,它會重新綁定已有的變量:

b = 1
for b in range(5):
    if b == 4:
        print(b, '-> b in for-loop')
print(b, '-> b in global')
4 -> b in for-loop
4 -> b in global

不過,在Python 3.x中,我們可以使用閉包來防止for循環變量進入全局命名空間。下面是一個例子(在Python 3.4中執行):

i = 1
print([i for i in range(5)])
print(i, '-> i in global')
[0, 1, 2, 3, 4]
1 -> i in global

為什么我要強調“Python 3.x”?因為在Python 2.x下執行同樣的代碼,打印結果是:

4 -> i in global

這是因為在Python 3.x中所做的一個變化,在What’s New In Python 3.0中有如下描述:

“列表推導式不再支持 [... for var in item1, item2, ...]這樣的語法形式,取而代之的是 [... for var in (item1, item2, ...)] 。也要注意列表推導式有不同的語義: 它們更接近於一個 list()構造器內的生成器表達式的語法糖,特別是循環控制變量不再泄漏到外圍作用域中。”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM