一、开篇:变量的"活动范围"
想象每个变量都有一张"通行证",规定了它能在哪些代码区域被访问。有些变量是整个程序通用的(全局变量),有些只在某个函数内部有效(局部变量)。这种"通行范围",就是作用域(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在编译函数时,会根据赋值语句来决定变量的作用域。如果函数内部有对某个变量的赋值,python就认为它是局部变量——即使赋值语句在使用语句的后面。
这篇文章,我们就来彻底搞懂python的变量作用域——legb规则、global关键字、nonlocal关键字,以及它们在实际开发中的应用。
二、legb规则:python的变量查找法则
2.1 四层作用域
# python变量查找遵循legb规则(由内向外):
# l - local(局部作用域):函数内部
# e - enclosing(嵌套作用域):外层函数的作用域
# g - global(全局作用域):模块级别
# b - built-in(内置作用域):python内置的名字
# ========== 内置作用域 ==========
# python启动时就存在,如print、len、range等
# ========== 全局作用域 ==========
# 模块级别的变量和函数
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内置函数
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 赋值修改了作用域
# ⚠️ 核心规则:在函数内部,如果对变量进行了赋值
# python就认为这个变量是该函数的局部变量
# 即使外面有同名的全局变量!
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在编译函数时扫描了整个函数体
# 发现了 x = 200 这行赋值语句
# 于是决定:x是这个函数的局部变量
# 所以,赋值之前的print(x)也在找局部变量x——但x还没被赋值!
三、global关键字:在函数内部修改全局变量
3.1 global的基本用法
# global关键字:声明一个变量是全局变量
# 这样即使在函数内部对它赋值,也是修改全局的那个
count = 0 # 全局计数器
def increment():
"""使用global声明,修改全局变量"""
global count
count += 1
print(f"计数器: {count}")
def increment_bad():
"""没有global——创建了局部变量"""
count = 999 # 这是局部变量,不影响全局的count
print(f"局部count: {count}")
increment() # 计数器: 1
print(f"全局count: {count}") # 全局count: 1
increment_bad() # 局部count: 999
print(f"全局count: {count}") # 全局count: 1 —— 没变!
increment() # 计数器: 2
print(f"全局count: {count}") # 全局count: 2 —— 继续累加
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
# 场景二:在嵌套函数中使用global
total = 0
def outer():
def inner():
global total # 声明使用的是全局的total
total += 1 # 修改全局变量
inner()
outer()
print(total) # 1
# ⚠️ 注意:global声明的是全局变量,不是外层函数的变量
# 对于嵌套函数中外层函数的变量,需要用nonlocal(下一节讲)
# 场景三:在函数内部创建全局变量
def create_global():
global new_variable
new_variable = "我在函数内部被创建为全局变量!"
print("全局变量已创建")
create_global()
print(new_variable) # 我在函数内部被创建为全局变量!
# 💡 虽然可以这样做,但强烈不推荐——让代码难以追踪
3.3 什么时候应该(和不应该)使用global
# ✅ 适用场景:全局状态管理(但要谨慎)
# 例如:应用配置、全局计数器、缓存
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 '关闭'}")
# 💡 关键区别:
# app_config["key"] = value → 不需要global(修改对象的内容)
# app_config = new_dict → 需要global(给变量重新赋值)
# ❌ 不推荐:滥用global
# 这种代码让人抓狂——不知道变量从哪里冒出来的
def do_work():
global result # 突然出现一个全局变量
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关键字:修改外层函数的变量
4.1 nonlocal的基本概念
# nonlocal:在嵌套函数中,声明变量来自外层函数(enclosing作用域)
# 它不是全局的(不是g层),也不是局部的(不是l层),而是中间的e层
def outer():
message = "hello" # 外层函数的局部变量
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 的区别
# ⌨️ 对比实验: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层(嵌套作用域,最近的外层函数)
4.3 nonlocal的典型应用
# 场景一:计数器(闭包)
def make_counter(start=0):
"""创建一个计数器函数"""
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():
"""创建一个求平均值的函数,持续接收新数据"""
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的查找规则
# 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修改
# ⚠️ 如果外层所有函数都没有这个变量,nonlocal会报错
def func():
def inner():
# nonlocal nonexistent_var # syntaxerror! 外层没有这个变量
pass
inner()
五、闭包中的变量绑定
5.1 python的变量绑定是"引用"而非"拷贝"
# 💡 闭包中的变量绑定是"晚期绑定"
# 捕获的是变量的引用,而不是变量的值
# ⚠️ 经典陷阱:lambda在循环中
functions = []
for i in range(5):
functions.append(lambda: i) # 每个lambda都引用同一个i
# 循环结束后i=4,所有lambda看到的i都是4
for f in functions:
print(f(), end=" ") # 4 4 4 4 4 —— 不是0 1 2 3 4!
print()
# ✅ 解决方案:使用默认参数"冻结"当前值
functions_v2 = []
for i in range(5):
functions_v2.append(lambda x=i: x) # 默认参数在定义时求值
for f in functions_v2:
print(f(), end=" ") # 0 1 2 3 4
print()
# ✅ 或者用函数工厂
def make_printer(n):
return lambda: n # n是make_printer的参数,每次调用都创建新的作用域
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()) # ['苹果', '香蕉', '橙子']
# 💡 关键区别:
# items.append(x) → 不需要nonlocal(修改对象内容,items指向同一个对象)
# items = [] → 需要nonlocal(给变量重新赋值,items指向新对象)
六、globals()和locals()查看作用域
# ⌨️ 使用内置函数查看当前作用域中的所有变量
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
七、实战案例
7.1 实现函数调用计数器
def make_tracked_function(func):
"""装饰器:统计函数被调用的次数"""
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():
"""查看调用次数"""
return call_count
def reset_count():
"""重置计数器"""
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"总调用次数: {calculate.get_count()}") # 3
calculate.reset_count()
print(f"重置后: {calculate.get_count()}") # 0
7.2 配置管理器
# 使用作用域管理配置——避免全局变量污染
def create_config_manager(initial_config=none):
"""创建配置管理器(使用闭包封装配置数据)"""
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}
八、最佳实践
# ✅ 最佳实践一:优先使用局部变量
# 局部变量访问最快,且不污染全局作用域
# ✅ 最佳实践二:避免使用global
# 用函数参数和返回值来传递数据,而不是修改全局状态
# ❌ 不推荐
total = 0
def add_to_total(x):
global total
total += x
# ✅ 推荐
def add(x, y):
return x + y
# ✅ 最佳实践三:模块级别的常量用全大写
# pep 8规范:全局常量用大写字母+下划线
max_connections = 100
default_timeout = 30
database_url = "mysql://localhost/mydb"
# ✅ 最佳实践四:用类代替闭包管理复杂状态
# 当闭包变得复杂时(多个函数、多个状态变量),用类更清晰
# 闭包适合简单场景(1-2个状态)
# 类适合复杂场景(多个方法和属性)
# ✅ 最佳实践五:理解"修改"和"重新赋值"的区别
# my_list.append(x) → 修改对象,不需要nonlocal/global
# my_list = [x] → 重新赋值,需要nonlocal/global
# my_dict["key"] = v → 修改对象,不需要nonlocal/global
# my_dict = {"k": v} → 重新赋值,需要nonlocal/global
九、总结
python的作用域规则是legb——从local到enclosing到global到built-in,逐层向外查找。理解这个规则是写出正确python代码的基础。
核心要点:
| 关键字 | 作用 | 跳转层级 | 使用场景 |
|---|---|---|---|
| (无需声明) | 读取变量 | legb逐层查找 | 只读访问外部变量 |
global | 修改全局变量 | 跳到g层 | 修改模块级变量 |
nonlocal | 修改外层变量 | 跳到最近的e层 | 闭包中修改捕获的变量 |
使用建议:
- 优先使用局部变量——最快、最安全
- 谨慎使用global——能不用就不用,用参数和返回值代替
- nonlocal是闭包的好伙伴——但复杂场景用类替代
- 理解"修改对象"vs"重新赋值"——前者不需要关键字声明
掌握了作用域规则,你就真正理解了python的变量世界。下一篇文章,我们将深入探讨全局变量与局部变量的优先级规则——以及那些让你意想不到的边界情况。
以上就是系统讲解python函数中global与nonlocal关键字的使用的详细内容,更多关于python global与nonlocal关键字的资料请关注代码网其它相关文章!
发表评论