關於 LEGB 規則,我想補充的是...

施威銘研究室
5 min readJun 21, 2023

--

LEGB規則指的是Python解析變數範圍的順序,也就是Local、Enclosure、Global、Build-in的首字母縮寫。

Photo by Mourizal Zativa on Unsplash

我們來快速了解一下這4個變數範圍:

1. 區城範園 (Local):區城範圍指的就是 Python 直譯器當前的作業範圍,可以是一個函式的主體,也可以是全域範圍。

2. 閉包範圍 (Enclosing):閉包範圍是在區域範圍外面一層的變數範圍,會隨著區域範圍的位置而跟著改變位置。如果目前的區域範圍是一個在函式內層的函式,那麼閉包範圍就是外面那層函式。如果區域範圍已經是最外層的函式,那閉包範圍就是全域範圍。

3. 全域範圍 (Global):全域範圍是程式最外層的變數範圍,是固定不動的。所有在程式碼裡定義、但不在函式主體裡的名稱都包含在內。

4. 內建範圍(Built-in):內建範圍包含 Python 內建的所有名稱,像是關鍵字,或是round()和 abs()這些函式。總而言之,不需要由你自己定義就可以使用的名稱都包含在內建範圍裡。

一開始弄不懂變數範圍的概念也是很正常的,不用太擔心,累積一些實作經驗之後自然就會比較能掌握了。

打破LEGB 規則

想想看底下的程式碼會有什麼樣的輸出:

def deposit(amount): # 存入現金
balance += amount

def print_balance(): # 查詢餘額
print(f"帳戶餘額: {balance}")

balance = 0
deposit(100)
print_balance()

乍看之下,我們存入 100,最後印出餘額應該是 100,對吧?請執行這個程式,看看會發生什麼事。

結果居然出現了意想不到的結果,出現錯誤訊息:

Traceback (most recent call last):

File "C:\Users\Seditor\Downloads\F3945\untitled4.py", line 8, in <module>
deposit(100)

File "C:\Users\Seditor\Downloads\F3945\untitled4.py", line 2, in deposit
balance += amount

UnboundLocalError: local variable 'balance' referenced before assignment

等一下!根據 LEGB 規則, Python 應該會先發現 deposit()函式的區域範圍中没有 balance 這個名稱,然後就向外移動到全域範圍來解析名稱,對吧?

說的沒錯,但這裡實際發生的問題是在 balance += amount 這一行的等號左邊,程式想要把值指派給變數 balance,於是就先在區域範圍裡建立了一個新的名稱。然後,當Python 到等號的右邊做運算的時候,就會在區域範圍裡找到(剛剛才建立的)balance,但這個 balance還沒有被指派任何值。

如此一來,就發生了「在賦值前存取」的錯誤(referenced before assignment) 。

這種類型的錯誤很棘手,所以不管你在哪個變數範圍,最好都要讓變數和函式有個獨一無二的名稱。

這個問題還可以用 g1obal 關鍵字來解決:

def deposit(amount):
global balance
balance += amount

def print_balance():
print(f"帳戶餘額: {balance}")

balance = 0
deposit(100)
print_balance()

這一次就可以得到預期的輸出 100,但這是為什麼呢?

global balance 這一行是告訴 Python,要在全域範圍尋找名稱balance。這樣一來,balance += amount 這一行就不會建立新的區域變數了。

儘管把程式順利「修好了」,但使用 global 關鍵字通常會被當成是很不好的程式設計習慣。

如果你用了 global 來解決像這樣的問題,最好還是停下來思考一下,是不是有更好的解決方式?很多時候,你會發現其實有其他更好的寫法!

例如這一個銀行帳戶管理程式我們可以修改成比較完整的版本:

def create_account(name): # 建立帳戶
account = {}
account['name'] = name
account['balance'] = 0
return account

def deposit(account, amount): # 存入現金
account['balance'] += amount

def print_balance(account): # 查詢餘額
print(f"{account['name']} 的帳戶餘額: {account['balance']}")

john_account = create_account('John')
deposit(john_account, 100)
print_balance(john_account)

多了新增了 create_account() 函式,並且用字典型別來存放帳戶資訊。剛剛出錯的存入現金 deposit() 函式,會直接修改傳入的帳戶字典的 ‘balance’ 鍵的值,因此,當我們最後呼叫 print_balance() 函式時,會得到期望的帳戶餘額。

如果想對 Python 程式的細節有更深入的了解,可以參考旗標科技《Real Python 人氣站長教你動手寫程式 — 不說教也能心領神會的引導式實作課》一書。

--

--

施威銘研究室

致力開發AI領域的圖書、創客、教具,希望培養更多的AI人才。整合各種人才,投入創客產品的開發,推廣「實作學習」,希望實踐學以致用的理想。