Python变量作用域LEGB法则与闭包装饰器详解

陆景行
陆景行
发布时间:2026-10-13 | 5人浏览过
原创

本文深入解析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层)来改变变量的作用等级。
  • 变量作用域虽常被使用,却容易被忽略。此处做个总结,建议常翻阅复习。关于作用域的讨论至此结束。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表