系統(tǒng)講解Python函數(shù)中global與nonlocal關鍵字的使用
一、開篇:變量的"活動范圍"
想象每個變量都有一張"通行證",規(guī)定了它能在哪些代碼區(qū)域被訪問。有些變量是整個程序通用的(全局變量),有些只在某個函數(shù)內部有效(局部變量)。這種"通行范圍",就是作用域(Scope)。
先看一個經典的困惑:
# 這段代碼會輸出什么?
x = 10 # 全局變量
def my_function():
print(x) # 訪問全局變量x
x = 20 # 嗯?這行在print后面,會影響前面的print嗎?
print(x)
# my_function() # 如果運行這行,會報錯還是正常輸出?
# 答案是:UnboundLocalError!
# 因為x=20讓Python認為x是局部變量,但print(x)時x還沒被賦值
這段代碼揭示了一個關鍵事實:Python在編譯函數(shù)時,會根據賦值語句來決定變量的作用域。如果函數(shù)內部有對某個變量的賦值,Python就認為它是局部變量——即使賦值語句在使用語句的后面。
這篇文章,我們就來徹底搞懂Python的變量作用域——LEGB規(guī)則、global關鍵字、nonlocal關鍵字,以及它們在實際開發(fā)中的應用。
二、LEGB規(guī)則:Python的變量查找法則
2.1 四層作用域
# Python變量查找遵循LEGB規(guī)則(由內向外):
# L - Local(局部作用域):函數(shù)內部
# E - Enclosing(嵌套作用域):外層函數(shù)的作用域
# G - Global(全局作用域):模塊級別
# B - Built-in(內置作用域):Python內置的名字
# ========== 內置作用域 ==========
# Python啟動時就存在,如print、len、range等
# ========== 全局作用域 ==========
# 模塊級別的變量和函數(shù)
global_var = "我是全局變量"
def outer_function():
# ========== 嵌套作用域 ==========
enclosing_var = "我是嵌套變量(屬于outer_function)"
def inner_function():
# ========== 局部作用域 ==========
local_var = "我是局部變量(屬于inner_function)"
print(f"局部: {local_var}")
print(f"嵌套: {enclosing_var}")
print(f"全局: {global_var}")
print(f"內置: {len}") # Python內置函數(shù)
inner_function()
outer_function()
# 局部: 我是局部變量(屬于inner_function)
# 嵌套: 我是嵌套變量(屬于outer_function)
# 全局: 我是全局變量
# 內置: <built-in function len>
2.2 LEGB查找過程的直觀演示
# ?? 詳細演示:Python按順序查找變量
# 找到就停,不到最后一層不報錯
name = "Global: 張三" # G層
def outer():
name = "Enclosing: 李四" # E層
def inner():
name = "Local: 王五" # L層
print(f"inner中的name: {name}")
# 輸出 → Local: 王五 (找到了,停止)
def inner_no_local():
# 這層沒有name
print(f"inner_no_local中的name: {name}")
# 輸出 → Enclosing: 李四 (L層沒有,找到了E層)
def inner_use_global():
# L層和E層都沒有
print(f"inner_use_global中的name: {name}")
# 等等——E層有name="李四",所以輸出→Enclosing: 李四
inner()
inner_no_local()
inner_use_global()
outer()
# 在全局作用域中訪問
print(f"全局中的name: {name}")
# 輸出 → Global: 張三
2.3 賦值修改了作用域
# ?? 核心規(guī)則:在函數(shù)內部,如果對變量進行了賦值
# Python就認為這個變量是該函數(shù)的局部變量
# 即使外面有同名的全局變量!
x = 100 # 全局變量
def read_only():
"""只讀取,不賦值——可以訪問全局變量"""
print(f"read_only: x = {x}") # 正常,訪問全局x
def has_assignment():
"""有賦值語句——x被視為局部變量"""
# print(f"賦值前: x = {x}") # UnboundLocalError!
x = 200 # 這個賦值讓Python將x當作局部變量
print(f"賦值后: x = {x}")
read_only() # read_only: x = 100
has_assignment()
# 如果取消注釋print(f"賦值前: x = {x}"),會報錯:
# UnboundLocalError: local variable 'x' referenced before assignment
# 為什么?Python在編譯函數(shù)時掃描了整個函數(shù)體
# 發(fā)現(xiàn)了 x = 200 這行賦值語句
# 于是決定:x是這個函數(shù)的局部變量
# 所以,賦值之前的print(x)也在找局部變量x——但x還沒被賦值!
三、global關鍵字:在函數(shù)內部修改全局變量
3.1 global的基本用法
# global關鍵字:聲明一個變量是全局變量
# 這樣即使在函數(shù)內部對它賦值,也是修改全局的那個
count = 0 # 全局計數(shù)器
def increment():
"""使用global聲明,修改全局變量"""
global count
count += 1
print(f"計數(shù)器: {count}")
def increment_bad():
"""沒有global——創(chuàng)建了局部變量"""
count = 999 # 這是局部變量,不影響全局的count
print(f"局部count: {count}")
increment() # 計數(shù)器: 1
print(f"全局count: {count}") # 全局count: 1
increment_bad() # 局部count: 999
print(f"全局count: {count}") # 全局count: 1 —— 沒變!
increment() # 計數(shù)器: 2
print(f"全局count: {count}") # 全局count: 2 —— 繼續(xù)累加
3.2 global的多種使用場景
# 場景一:同時聲明多個全局變量
a = 1
b = 2
c = 3
def modify_all():
global a, b, c # 一次聲明多個
a = 10
b = 20
c = 30
modify_all()
print(a, b, c) # 10 20 30
# 場景二:在嵌套函數(shù)中使用global
total = 0
def outer():
def inner():
global total # 聲明使用的是全局的total
total += 1 # 修改全局變量
inner()
outer()
print(total) # 1
# ?? 注意:global聲明的是全局變量,不是外層函數(shù)的變量
# 對于嵌套函數(shù)中外層函數(shù)的變量,需要用nonlocal(下一節(jié)講)
# 場景三:在函數(shù)內部創(chuàng)建全局變量
def create_global():
global new_variable
new_variable = "我在函數(shù)內部被創(chuàng)建為全局變量!"
print("全局變量已創(chuàng)建")
create_global()
print(new_variable) # 我在函數(shù)內部被創(chuàng)建為全局變量!
# ?? 雖然可以這樣做,但強烈不推薦——讓代碼難以追蹤
3.3 什么時候應該(和不應該)使用global
# ? 適用場景:全局狀態(tài)管理(但要謹慎)
# 例如:應用配置、全局計數(shù)器、緩存
app_config = {
"debug": False,
"log_level": "INFO",
"max_connections": 100,
}
def set_debug_mode(enabled):
"""修改全局配置"""
global app_config
app_config["debug"] = enabled
print(f"調試模式: {'開啟' if enabled else '關閉'}")
# ?? 但要注意:如果你只是修改字典的內容(而不是重新賦值字典變量)
# 可以不需要global!
def set_debug_mode_no_global(enabled):
"""不需要global——因為我們沒有給app_config重新賦值"""
app_config["debug"] = enabled # 修改的是字典的內容,不是變量本身
print(f"調試模式: {'開啟' if enabled else '關閉'}")
# ?? 關鍵區(qū)別:
# app_config["key"] = value → 不需要global(修改對象的內容)
# app_config = new_dict → 需要global(給變量重新賦值)
# ? 不推薦:濫用global
# 這種代碼讓人抓狂——不知道變量從哪里冒出來的
def do_work():
global result # 突然出現(xiàn)一個全局變量
result = 42 # 調用者完全不知道result被設置了
global status
status = "done"
# ... 更多代碼 ...
# ? 更好的做法:用返回值
def do_work_better():
result = 42
status = "done"
return result, status # 明確地返回
# 或者用類封裝
class Worker:
def __init__(self):
self.result = None
self.status = "idle"
def do_work(self):
self.result = 42
self.status = "done"
return self.result, self.status
四、nonlocal關鍵字:修改外層函數(shù)的變量
4.1 nonlocal的基本概念
# nonlocal:在嵌套函數(shù)中,聲明變量來自外層函數(shù)(Enclosing作用域)
# 它不是全局的(不是G層),也不是局部的(不是L層),而是中間的E層
def outer():
message = "Hello" # 外層函數(shù)的局部變量
def inner():
nonlocal message # 聲明:我要用外層的message,不是局部的
message = "你好" # 修改的是外層的message
print(f"inner中: {message}")
print(f"調用inner前: {message}")
inner()
print(f"調用inner后: {message}")
# 輸出:
# 調用inner前: Hello
# inner中: 你好
# 調用inner后: 你好 ← message被inner修改了!
outer()
4.2 nonlocal vs global 的區(qū)別
# ?? 對比實驗:global vs nonlocal
x = "全局X"
def outer():
x = "外層X"
def inner_global():
global x # 聲明使用全局變量x
x = "被global修改"
print(f"inner_global中: x={x}")
def inner_nonlocal():
nonlocal x # 聲明使用外層變量x
x = "被nonlocal修改"
print(f"inner_nonlocal中: x={x}")
print(f"outer開始: x={x}")
inner_global()
print(f"inner_global后, outer中的x: {x}") # 還是"外層X"——沒變!
inner_nonlocal()
print(f"inner_nonlocal后, outer中的x: {x}") # 變成"被nonlocal修改"
print(f"全局x: {x}") # 全局x變成了"被global修改"
outer()
print(f"最終全局x: {x}")
# 最終全局x: 被global修改
# ?? 總結:
# global → 跳轉到G層(全局作用域)
# nonlocal → 跳轉到E層(嵌套作用域,最近的外層函數(shù))
4.3 nonlocal的典型應用
# 場景一:計數(shù)器(閉包)
def make_counter(start=0):
"""創(chuàng)建一個計數(shù)器函數(shù)"""
count = start # 外層變量
def counter():
nonlocal count
count += 1 # 修改外層變量
return count
return counter
counter1 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter1()) # 3
counter2 = make_counter(100)
print(counter2()) # 101
print(counter2()) # 102
print(counter1()) # 4 —— counter1獨立運行
# 場景二:求平均值(不斷累加)
def make_averager():
"""創(chuàng)建一個求平均值的函數(shù),持續(xù)接收新數(shù)據"""
total = 0
count = 0
def averager(new_value):
nonlocal total, count
total += new_value
count += 1
return total / count
return averager
avg = make_averager()
print(avg(10)) # 10.0
print(avg(20)) # 15.0
print(avg(30)) # 20.0
print(avg(40)) # 25.0
# ?? 如果沒有nonlocal,這段代碼會直接報錯
# 因為 averager 中的 total += new_value 相當于 total = total + new_value
# Python會認為total是局部變量,但它還沒被賦值!
4.4 nonlocal的查找規(guī)則
# nonlocal查找最近的外層作用域(不包括全局作用域)
def level1():
x = "level1"
def level2():
x = "level2"
def level3():
nonlocal x # 找最近的包含x的外層——是level2的x
x = "被level3修改"
print(f"level3中: x={x}")
print(f"level3調用前, level2的x: {x}")
level3()
print(f"level3調用后, level2的x: {x}")
level2()
level1()
# level3調用前, level2的x: level2
# level3中: x=被level3修改
# level3調用后, level2的x: 被level3修改
# ?? 如果外層所有函數(shù)都沒有這個變量,nonlocal會報錯
def func():
def inner():
# nonlocal nonexistent_var # SyntaxError! 外層沒有這個變量
pass
inner()
五、閉包中的變量綁定
5.1 Python的變量綁定是"引用"而非"拷貝"
# ?? 閉包中的變量綁定是"晚期綁定"
# 捕獲的是變量的引用,而不是變量的值
# ?? 經典陷阱:lambda在循環(huán)中
functions = []
for i in range(5):
functions.append(lambda: i) # 每個lambda都引用同一個i
# 循環(huán)結束后i=4,所有l(wèi)ambda看到的i都是4
for f in functions:
print(f(), end=" ") # 4 4 4 4 4 —— 不是0 1 2 3 4!
print()
# ? 解決方案:使用默認參數(shù)"凍結"當前值
functions_v2 = []
for i in range(5):
functions_v2.append(lambda x=i: x) # 默認參數(shù)在定義時求值
for f in functions_v2:
print(f(), end=" ") # 0 1 2 3 4
print()
# ? 或者用函數(shù)工廠
def make_printer(n):
return lambda: n # n是make_printer的參數(shù),每次調用都創(chuàng)建新的作用域
functions_v3 = [make_printer(i) for i in range(5)]
for f in functions_v3:
print(f(), end=" ") # 0 1 2 3 4
5.2 閉包中修改可變對象
# 如果閉包中的變量是可變對象(list, dict等)
# 修改其內容不需要nonlocal
# 只有給變量名重新賦值才需要nonlocal
def make_basket():
"""購物籃——存放可變列表對象"""
items = [] # 可變列表
def add_item(item):
# 不需要nonlocal!因為我們在修改列表的內容,不是給items重新賦值
items.append(item)
return len(items)
def get_items():
return list(items) # 返回副本
return add_item, get_items
add, get = make_basket()
print(add("蘋果")) # 1
print(add("香蕉")) # 2
print(add("橙子")) # 3
print(get()) # ['蘋果', '香蕉', '橙子']
# ?? 關鍵區(qū)別:
# items.append(x) → 不需要nonlocal(修改對象內容,items指向同一個對象)
# items = [] → 需要nonlocal(給變量重新賦值,items指向新對象)
六、globals()和locals()查看作用域
# ?? 使用內置函數(shù)查看當前作用域中的所有變量
global_x = 10
global_y = 20
def demo():
local_a = 100
local_b = 200
print("=== 局部作用域 (locals()) ===")
for name, value in locals().items():
print(f" {name} = {value}")
print("\n=== 全局作用域 (globals()) ===")
# 只顯示我們定義的變量
for name, value in globals().items():
if not name.startswith("_"):
print(f" {name} = {value}")
demo()
# 在模塊級別,locals()和globals()返回同一個字典
print("\n=== 模塊級別 ===")
print(f"locals() is globals(): {locals() is globals()}") # True
七、實戰(zhàn)案例
7.1 實現(xiàn)函數(shù)調用計數(shù)器
def make_tracked_function(func):
"""裝飾器:統(tǒng)計函數(shù)被調用的次數(shù)"""
call_count = 0
def wrapper(*args, **kwargs):
nonlocal call_count
call_count += 1
print(f"→ {func.__name__} 第{call_count}次被調用")
result = func(*args, **kwargs)
return result
def get_count():
"""查看調用次數(shù)"""
return call_count
def reset_count():
"""重置計數(shù)器"""
nonlocal call_count
call_count = 0
wrapper.get_count = get_count
wrapper.reset_count = reset_count
return wrapper
@make_tracked_function
def calculate(a, b):
return a + b
print(calculate(1, 2)) # → calculate 第1次被調用 → 3
print(calculate(3, 4)) # → calculate 第2次被調用 → 7
print(calculate(5, 6)) # → calculate 第3次被調用 → 11
print(f"總調用次數(shù): {calculate.get_count()}") # 3
calculate.reset_count()
print(f"重置后: {calculate.get_count()}") # 0
7.2 配置管理器
# 使用作用域管理配置——避免全局變量污染
def create_config_manager(initial_config=None):
"""創(chuàng)建配置管理器(使用閉包封裝配置數(shù)據)"""
if initial_config is None:
config = {}
else:
config = dict(initial_config) # 復制一份,避免外部修改
def get(key, default=None):
"""獲取配置項"""
return config.get(key, default)
def set(key, value):
"""設置配置項"""
nonlocal config # 如果set需要替換整個config,就需要nonlocal
config[key] = value
print(f"配置已更新: {key} = {value}")
def update(new_config):
"""批量更新配置"""
config.update(new_config)
def get_all():
"""獲取所有配置"""
return dict(config) # 返回副本
def reset(initial_config=None):
"""重置配置"""
nonlocal config
if initial_config is None:
config = {}
else:
config = dict(initial_config)
return get, set, update, get_all, reset
# 使用配置管理器
get_config, set_config, update_config, get_all_config, reset_config = create_config_manager({
"debug": False,
"host": "localhost",
"port": 8080,
})
print(get_config("host")) # localhost
set_config("debug", True) # 配置已更新: debug = True
update_config({"timeout": 30, "retries": 3})
print(get_all_config())
# {'debug': True, 'host': 'localhost', 'port': 8080, 'timeout': 30, 'retries': 3}
八、最佳實踐
# ? 最佳實踐一:優(yōu)先使用局部變量
# 局部變量訪問最快,且不污染全局作用域
# ? 最佳實踐二:避免使用global
# 用函數(shù)參數(shù)和返回值來傳遞數(shù)據,而不是修改全局狀態(tài)
# ? 不推薦
total = 0
def add_to_total(x):
global total
total += x
# ? 推薦
def add(x, y):
return x + y
# ? 最佳實踐三:模塊級別的常量用全大寫
# PEP 8規(guī)范:全局常量用大寫字母+下劃線
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
DATABASE_URL = "mysql://localhost/mydb"
# ? 最佳實踐四:用類代替閉包管理復雜狀態(tài)
# 當閉包變得復雜時(多個函數(shù)、多個狀態(tài)變量),用類更清晰
# 閉包適合簡單場景(1-2個狀態(tài))
# 類適合復雜場景(多個方法和屬性)
# ? 最佳實踐五:理解"修改"和"重新賦值"的區(qū)別
# my_list.append(x) → 修改對象,不需要nonlocal/global
# my_list = [x] → 重新賦值,需要nonlocal/global
# my_dict["key"] = v → 修改對象,不需要nonlocal/global
# my_dict = {"k": v} → 重新賦值,需要nonlocal/global
九、總結
Python的作用域規(guī)則是LEGB——從Local到Enclosing到Global到Built-in,逐層向外查找。理解這個規(guī)則是寫出正確Python代碼的基礎。
核心要點:
| 關鍵字 | 作用 | 跳轉層級 | 使用場景 |
|---|---|---|---|
| (無需聲明) | 讀取變量 | LEGB逐層查找 | 只讀訪問外部變量 |
global | 修改全局變量 | 跳到G層 | 修改模塊級變量 |
nonlocal | 修改外層變量 | 跳到最近的E層 | 閉包中修改捕獲的變量 |
使用建議:
- 優(yōu)先使用局部變量——最快、最安全
- 謹慎使用global——能不用就不用,用參數(shù)和返回值代替
- nonlocal是閉包的好伙伴——但復雜場景用類替代
- 理解"修改對象"vs"重新賦值"——前者不需要關鍵字聲明
掌握了作用域規(guī)則,你就真正理解了Python的變量世界。下一篇文章,我們將深入探討全局變量與局部變量的優(yōu)先級規(guī)則——以及那些讓你意想不到的邊界情況。
以上就是系統(tǒng)講解Python函數(shù)中global與nonlocal關鍵字的使用的詳細內容,更多關于Python global與nonlocal關鍵字的資料請關注腳本之家其它相關文章!
相關文章
Python實現(xiàn)遍歷子文件夾并將文件復制到不同的目標文件夾
這篇文章主要介紹了如何基于Python語言實現(xiàn)遍歷多個子文件夾,將每一個子文件夾中大量的文件,按照每一個文件的文件名稱的特點復制到不同的目標文件夾中,感興趣的可以了解下2023-08-08

