Python变量作用域LEGB法则与闭包装饰器详解
本文深入解析Python变量作用域的LEGB查找规则,阐明Local、Enclosed、Global及Built-in的优先级。详细讲解闭包机制与装饰器的实现原理,包括带参装饰器的写法。通过global和nonlocal关键字演示如何修改全局及嵌套层变量,并结合Flask路由等实际场景说明其应用,帮助开发者规避作用域陷阱。
Python变量作用域与LEGB法则
本文重点解析Python中变量作用域LEGB的使用机制,通过具体示例代码进行详细说明,旨在为读者的学习或工作提供有价值的参考。
闭包与装饰器原理
闭包的核心在于函数内部嵌套定义另一个函数。装饰器则是闭包的一种特殊应用场景:当外函数的参数指向一个待装饰的函数地址时(此处泛指函数对象,不局限于地址概念),便形成了类似“@xxx”的语法糖。这种写法简洁优雅,其核心作用是在不修改原函数代码的前提下,为其扩展额外功能。
装饰器的基础写法
装饰器的基础写法示例
# 装饰器的通用写法
def out(func):
def inner(*args, **kwargs):
print("we are checking...", args[0])
return func(*args, **kwargs)
return inner
@out
def check_2019_nCov(name):
return f"now, {name} is very healthy..."
tmp = check_2019_nCov('youge')
print(tmp)
# output
we are checking... youge
now, youge is very healthy...
带参装饰器的实现
为装饰器传递参数的方法
虽然“@”语法要求外函数参数接收一个函数对象,但若需实现传参,只需在外层再包裹一层函数以接收具体参数。这种结构扩大了作用空间,使得参数能够被正确获取和处理。
# 最外层的函数作用是, 给装饰器传递参数
def get_param(*args, **kwargs):
def out(func):
def inner(*args, **kwargs):
print("get params", args, kwargs)
return func(*args, **kwargs)
return inner
return out
@get_param("youge")
def check_2019_nCov(name):
return f"now, {name} is very healthy..."
tmp = check_2019_nCov("youge")
print(tmp)
# output
get params ('youge',) {}
now, youge is very healthy...
装饰器在Web框架中的应用
在Web应用开发中,此类传参装饰器场景十分常见。以Flask框架为例,所有路由URL的定义(如route("/login"))均基于此原理,通过装饰器实现URL路径到视图函数的映射关系。
回顾上述过程,忽略了一个关键概念:命名空间及其变量作用域的划分方式。
命名空间与LEGB查找规则
LEGB法则详解
命名空间概念
前文已详细阐述,Python中变量的本质是指针,即对对象的引用。在Python中“万物皆对象”,变量指向存储数据的实际内存地址,也就是各种类(数据类型、数据结构)的实例。简言之,变量用于引用对象。
最直观的解释如下:
"A namespace is a mapping from names to objects"(变量名到对象的映射)。
"Most namespaces are currently implemented as Python dictionaries."(大多数命名空间当前通过Python字典实现)。
命名空间的主要作用是避免变量命名冲突。各命名空间彼此独立,同一空间内不允许重名,但不同空间之间互不影响。其逻辑类似于计算机系统中存储文件的组织方式。
for i in range(10):
print(i)
# 这两句话都用到了 i 但其各自的空间是不一样的.
[i for i in range(100)]
- 内置空间 (built-in names):包含Python内置名称,如内置函数、异常类等。
- 全局空间 (global names):包含常量、模块中定义的名称(如类、导入的模块等)。
- Enclosed:可能嵌套在函数内部的函数等形成的封闭空间。
- 局部名称 (local names):函数内部定义的名称(如函数内的局部变量)。
Python查找变量的顺序遵循:Local -> Enclosed -> Global -> Built-in。
从个人经验来看,理解局部与全局的相对性即可满足大部分需求。直观上,以一个Python文件为例:最外层定义的变量、类定义、函数定义以及通过from...import...引入的变量或函数名,均属于全局变量;而最外层的类或函数内部,则各自拥有独立的名字空间。
# var1 是 global
var1 = 666
def foo():
# var2 是局部
var2 = 666
def foo2():
# 内嵌的局部
var3 = 666
# print(var2)
print(var3) # G->L 是找不到的哦
# 在 foo2 中 寻找 var2 是 L->E 是ok的
# 在 foo 中 寻找 var2 是 E->L 是不行的
这一概念易于理解。以上述代码为例,根据L-E-G-B法则,只需理解“相对性”即可。
全局与局部变量的交互
全局与局部的关系
total = 0 # 全局
def sum(a, b):
"""重写内置sum"""
total = a + b
print("局部total:", total)
sum(1, 1)
print("全局total:", total)
# output
局部total: 2
全局total: 0
可以看到,局部变量的修改不会影响全局变量,但在局部内部可以访问全局变量。否则,闭包中的外函数接收的参数,内函数将无法获取。这正是外函数“扩充”了内函数的作用域,依据L->E->G->B法则进行搜索的结果。
使用global和nonlocal修改变量作用域
global和nonlocal关键字
name = "youge"
def change_name():
name = "youyou"
# 希望将 "youge" 改为 "youyou"
change_name()
print(name)
# output
youge
可以发现,直接修改局部变量无法影响全局,这是正常现象。因为在调用函数时,内部的name是一个Local变量,不会影响全局的name。若希望在函数内部修改全局变量,需使用global关键字声明该变量。
name = "youge"
def change_name():
global name
name = "youyou"
# 希望将 "youge" 改为 "youyou"
change_name()
print(name)
# output
youyou
方法很简单:在函数内部使用global将其声明为全局变量。同样,针对函数嵌套场景(如闭包、装饰器等),通过关键字nonlocal可将函数内的变量声明为函数外的Enclosed层变量。
name = "jack"
def outer():
name = "youge"
# 函数内有一个local函数空间
def inner():
name = "youyou"
print("local:", name)
inner() # 尝试改掉 嵌套层的 name
print("encolse:", name)
print("global:", name)
outer()
# output
global: jack
local: youyou
encolse: youge
现在若想在inner函数(Local层)中修改Enclosed层的name,只需在inner中将name声明为nonlocal即可。
name = "jack"
def outer():
name = "youge"
# 函数内有一个local函数空间
def inner():
nonlocal name
name = "youyou"
print("local:", name)
inner() # 尝试改掉 嵌套层的 name
print("encolse:", name)
print("global:", name)
outer()
# output
global: jack
local: youyou
encolse: youyou
在函数嵌套场景中,通过在Local层声明name为nonlocal,即可修改Enclosed层的name。但若在Local层声明为global,则无效。原因尚不明确,目前仅通过实验验证,暂未深入探究。
此外,补充一个新手常犯的小错误示例:
name = 'youge'
def change_name():
name = name + "youyou"
change_name()
print(name)
# output
UnboundLocalError: local variable 'name' referenced before assignment
原因在于,在函数内部空间中,对name未进行定义。在Python中,函数过程的存储通过递归栈实现。利用栈的FILO(先进后出)特性,遇到函数时,将其参与的成员依次入栈;若有return,则置为栈元素。
变量需先定义后使用。在Python中,“定义”指该变量指向某个实例对象,而非其他语言中的类型声明。这一点最容易引起混淆。
若需修改name为全局变量,可通过函数参数传递实现:
# 方式1: 定义个单独的函数来处理
name = 'youge'
def change_name(s):
name = s + "youyou"
print(name)
# 全局变量来传递给 函数空间, 即"先定义, 后执行")
change_name(name)
# output
yougeyouyou
# 方式2: 声明为全局即可, 不推荐
name = 'youge'
def change_name():
global name
name = name + "youyou"
change_name()
print(name)
# output
yougeyouyou
小结
- 闭包与装饰器的本质是函数嵌套。参数及函数能被传递的原因是,Python变量的本质是指针。
- Python使用命名空间来解决变量名冲突,其原理与计算机系统(如Linux)存储文件的逻辑一致。
- 变量名的查找规则为:Local -> Enclosed -> Global -> Built-in。
- 个人建议,理解全局与局部的“相对性”即可。此外,可利用global与nonlocal(针对E层)来改变变量的作用等级。
- 变量作用域虽常被使用,却容易被忽略。此处做个总结,建议常翻阅复习。关于作用域的讨论至此结束。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









