Python 函式作用域(local / global / nonlocal)
簡介
在 Python 中,函式(function)是程式結構的核心,而變數的可見範圍(scope)直接影響程式的可讀性、可維護性與執行結果。
了解 local、global 與 nonlocal 三種作用域的差異,能讓你在設計 API、撰寫迴圈或是實作閉包(closure)時,避免常見的錯誤與不可預期的行為。
本篇文章以 淺顯易懂 的語言說明作用域概念,搭配 實用範例,從基礎的局部變數到進階的 nonlocal 用法,協助初學者快速上手,同時提供給中階開發者作為最佳實踐的參考。
核心概念
1. 局部作用域(Local Scope)
函式內部宣告的變數預設屬於 局部作用域,只在該函式執行期間有效。函式結束後,局部變數會被回收,外部無法直接存取。
def add(a, b):
result = a + b # result 為局部變數
return result
total = add(3, 5) # total = 8
# print(result) # NameError: name 'result' is not defined
- 特點
- 只在函式內部可見。
- 允許與外部同名變數共存,互不干擾。
2. 全域作用域(Global Scope)
在模組最外層(不在任何函式或類別內)宣告的變數屬於 全域作用域,整個模組內都可以直接存取。若在函式內需要 修改 全域變數,必須使用 global 關鍵字告訴 Python 這個名稱指向全域空間。
counter = 0 # 全域變數
def increase():
global counter # 宣告要使用全域的 counter
counter += 1 # 直接修改全域變數
increase()
print(counter) # 1
- 注意:未使用
global而直接賦值,Python 會把變數視為 局部,導致UnboundLocalError。
3. 非局部作用域(Nonlocal Scope)
nonlocal 用於 嵌套函式(function inside another function),讓內層函式可以 讀取或修改 外層(但不是全域)函式的變數。這是實作 閉包 時常用的手法。
def outer():
total = 0 # outer 的局部變數
def inner(x):
nonlocal total # 取得 outer 中的 total
total += x
return total
return inner
acc = outer()
print(acc(5)) # 5
print(acc(3)) # 8
- 關鍵點
nonlocal只能作用於最近一層的封閉作用域。- 若變數在全域層級,仍須使用
global,不能用nonlocal。
4. 變數遮蔽(Variable Shadowing)
同名變數在不同作用域會互相 遮蔽,即內層的名稱會覆蓋外層的名稱。這在閱讀程式碼時容易產生混淆,應盡量避免。
value = 10 # 全域
def func():
value = 5 # 局部,遮蔽全域的 value
print(value) # 5
func()
print(value) # 10
5. 可變物件與不可變物件的作用域行為
- 不可變物件(int、float、str、tuple 等)在局部賦值時會重新綁定名稱,不會影響外層變數。
- 可變物件(list、dict、set 等)則可在局部直接 修改,外層參照會同步改變。
def modify():
lst = [1, 2, 3] # 局部變數,指向新的 list
lst.append(4) # 修改的是同一個物件
return lst
print(modify()) # [1, 2, 3, 4]
如果想在函式內改變外層的可變物件,仍不需要 global 或 nonlocal,只要直接操作物件本身即可。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
未使用 global 而賦值 |
會產生 UnboundLocalError,因為 Python 認為是局部變數但未初始化。 |
在函式開頭加 global var_name,或改用參數傳遞。 |
nonlocal 用錯層級 |
試圖在最外層函式使用 nonlocal 會觸發 SyntaxError。 |
只在 嵌套函式 中使用,或改為 global。 |
| 可變預設參數 | def f(arg=[]): 會在函式定義時就建立同一個 list,導致跨呼叫共享狀態。 |
使用 None 作為預設值,再在函式內部初始化:if arg is None: arg = []。 |
| 過度使用全域變數 | 使程式耦合度提升,難以測試與除錯。 | 儘量把資料作為參數傳遞,或使用類別封裝狀態。 |
| 名稱遮蔽 | 同名變數在不同作用域混淆,增加閱讀難度。 | 使用具描述性的名稱,例如 global_counter、inner_total。 |
最佳實踐
- 最小化全域變數:僅保留真正需要跨模組共享的常數或設定。
- 明確使用
global/nonlocal:在程式碼第一行就寫出,提升可讀性。 - 避免可變預設參數:永遠使用
None作為預設值。 - 利用閉包封裝狀態:在需要保持「私有」狀態時,使用
nonlocal建立閉包,代替全域變數。 - 寫單元測試:作用域錯誤往往在執行時才顯現,測試能提前捕捉。
實際應用場景
1. 計數器(Counter)
在不使用全域變數的情況下,透過 nonlocal 建立一個 閉包計數器,每次呼叫都返回遞增的值:
def make_counter(start=0):
count = start
def next_number():
nonlocal count
count += 1
return count
return next_number
counter = make_counter(10)
print(counter()) # 11
print(counter()) # 12
此技巧常見於 產生唯一 ID、序列產生器 等需求。
2. 配置檔(Config)共享
在大型專案中,常把配置信息放在模組的全域變數,並在需要修改時使用 global:
# config.py
DEBUG = False
MAX_CONNECTION = 10
# app.py
import config
def enable_debug():
global DEBUG # 這裡其實不需要 global,直接修改模組屬性即可
config.DEBUG = True
enable_debug()
print(config.DEBUG) # True
3. 裝飾器(Decorator)實作
裝飾器本質上是 函式返回函式,常利用 nonlocal 保存原始函式的參數或狀態:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal times
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice") # 會印出三次
4. 多執行緒共享狀態
在多執行緒環境下,使用 全域變數 + thread lock 來保證安全:
import threading
total_requests = 0
lock = threading.Lock()
def handle_request():
global total_requests
with lock:
total_requests += 1
# 處理其他邏輯...
threads = [threading.Thread(target=handle_request) for _ in range(100)]
for t in threads: t.start()
for t in threads: t.join()
print(total_requests) # 100
總結
- 局部作用域(local)是函式最基本的變數範圍,保證變數不會意外洩漏。
- 全域作用域(global)適合放置跨模組共享的常數或設定,但修改時必須使用
global關鍵字。 - 非局部作用域(nonlocal)是 Python 針對 嵌套函式 提供的特殊機制,讓內層函式能操作外層函式的變數,常用於閉包與裝飾器。
- 正確掌握這三種作用域,不僅能避免
UnboundLocalError、NameError等常見錯誤,還能寫出 可讀、可維護、可測試 的程式碼。
透過本文的範例與最佳實踐,你現在應該能在日常開發中自信地運用 global、nonlocal,以及設計出更清晰的函式介面。祝你在 Python 的世界裡寫出更好、更穩定的程式!