范围规则的简短描述
- 2024-11-15 08:37:00
- admin 原创
- 16
问题描述:
Python 的作用域规则到底是什么?
如果我有一些代码:
code1
class Foo:
code2
def spam.....
code3
for code4..:
code5
x()
在哪里x
找到?一些可能的选择包括以下列表:
在封闭的源文件中
在类命名空间中
在函数定义中
在 for 循环中索引变量
在 for 循环中
此外,当函数传递到其他地方时,执行期间也存在上下文spam
。也许lambda 函数的传递方式有点不同?
某处一定有一个简单的参考或算法。对于中级 Python 程序员来说,这是一个令人困惑的世界。
解决方案 1:
实际上,Python 作用域解析的简明规则来自《学习 Python》第 3 版。(这些规则特定于变量名,而不是属性。如果引用时不使用句点,则适用这些规则。)
LEGB 规则
本地- 在函数(
def
或lambda
)中以任何方式分配的名称,并且未在该函数中声明为全局名称封闭
def
函数 - 在任何和所有静态封闭函数(或)的本地范围内分配的名称lambda
,从内到外全局(模块)——在模块文件的顶层指定的名称,或者通过执行文件
global
中的语句指定的名称def
内置(Python) —内置名称模块中预先分配的名称:
open
、、等range
`SyntaxError`
因此,在
code1
class Foo:
code2
def spam():
code3
for code4:
code5
x()
循环for
没有自己的命名空间。按照 LEGB 顺序,作用域将是
L:本地
def spam
(在code3
、code4
和 中code5
)E:任何封闭函数(如果整个示例在另一个中
def
)x
G:在模块中(在)是否有任何全局声明code1
?B:
x
Python 中的任何内置函数。
x
永远不会被发现code2
(即使在您可能期望的情况下,请参见Antti 的回答或此处)。
解决方案 2:
本质上,Python 中唯一引入新作用域的东西是函数定义。类有点特殊,因为直接在主体中定义的任何内容都放在类的命名空间中,但它们不能从它们包含的方法(或嵌套类)中直接访问。
在您的示例中,只有 3 个范围会搜索 x:
spam 的范围 - 包含 code3 和 code5 中定义的所有内容(以及 code4,你的循环变量)
全局范围 - 包含 code1 中定义的所有内容,以及 Foo(以及其后发生的任何变化)
内置命名空间。有点特殊 - 它包含各种 Python 内置函数和类型,例如 len() 和 str()。通常,它不应被任何用户代码修改,因此它只包含标准函数,不包含其他任何内容。
只有当您将嵌套函数(或 lambda)引入到图片中时,才会出现更多范围。不过,它们的行为与您预期的差不多。嵌套函数可以访问本地范围内的所有内容,以及封闭函数范围内的所有内容。例如。
def foo():
x=4
def bar():
print x # Accesses x from foo's scope
bar() # Prints 4
x=5
bar() # Prints 5
限制:
除本地函数变量之外的作用域中的变量可以被访问,但不能在没有进一步语法的情况下重新绑定到新参数。相反,赋值将创建一个新的局部变量,而不是影响父作用域中的变量。例如:
global_var1 = []
global_var2 = 1
def func():
# This is OK: It's just accessing, not rebinding
global_var1.append(4)
# This won't affect global_var2. Instead it creates a new variable
global_var2 = 2
local1 = 4
def embedded_func():
# Again, this doen't affect func's local1 variable. It creates a
# new local variable also called local1 instead.
local1 = 5
print local1
embedded_func() # Prints 5
print local1 # Prints 4
为了在函数范围内实际修改全局变量的绑定,您需要使用 global 关键字指定该变量是全局的。例如:
global_var = 4
def change_global():
global global_var
global_var = global_var + 1
目前,没有办法对封闭函数作用域中的变量执行相同操作,但 Python 3 引入了一个新的关键字“ nonlocal
”,其作用方式与全局类似,但适用于嵌套函数作用域。
解决方案 3:
关于 Python3 时间没有彻底的答案,所以我在这里回答了。这里描述的大部分内容在Python 3 文档的4.2.2 名称解析中都有详细说明。
正如其他答案所述,有 4 个基本作用域,即 LEGB,分别用于本地、封闭、全局和内置。除此之外,还有一个特殊作用域,即类主体,它不包含在类中定义的方法的封闭作用域;类主体内的任何赋值都会使变量从此绑定在类主体中。
特别是,除了和 之外,没有任何块语句会创建变量作用域。在 Python 2 中,列表推导式不会创建变量作用域,但在 Python 3 中,列表推导式中的循环变量会在新作用域中创建。def
`class`
展示班级团体的特点
x = 0
class X(object):
y = x
x = x + 1 # x is now a variable
z = x
def method(self):
print(self.x) # -> 1
print(x) # -> 0, the global x
print(y) # -> NameError: global name 'y' is not defined
inst = X()
print(inst.x, inst.y, inst.z, x) # -> (1, 0, 1, 0)
因此,与函数体不同,您可以在类体中将变量重新分配给相同的名称,以获取具有相同名称的类变量;对该名称的进一步查找将解析为类变量。
对于许多 Python 新手来说,最大的惊喜之一是for
循环不会创建变量作用域。在 Python 2 中,列表推导式也不会创建作用域(而生成器和字典推导式则会创建作用域!)相反,它们会泄漏函数或全局作用域中的值:
>>> [ i for i in range(5) ]
>>> i
4
在 Python 2 中,可以使用推导式来巧妙(或者说可怕)地在 lambda 表达式中创建可修改变量 - lambda 表达式确实会创建变量范围,就像def
语句一样,但在 lambda 中不允许使用语句。在 Python 中,赋值是语句,这意味着 lambda 中不允许进行变量赋值,但列表推导式是一种表达式...
此行为已在 Python 3 中修复 - 没有理解表达式或生成器泄漏变量。
全局实际上意味着模块范围;主要的 Python 模块是__main__
;所有导入的模块都可以通过sys.modules
变量访问;为了访问__main__
可以使用sys.modules['__main__']
,或者import __main__
;在那里访问和分配属性是完全可以接受的;它们将显示为主模块全局范围内的变量。
如果在当前作用域中(类作用域除外)曾为某个名称赋值,则该名称将被视为属于该作用域,否则将被视为属于任何赋值给该变量的封闭作用域(可能尚未赋值,或根本没有赋值),或最终属于全局作用域。如果该变量被视为局部变量,但尚未设置或已被删除,则读取该变量值将导致UnboundLocalError
,它是 的子类NameError
。
x = 5
def foobar():
print(x) # causes UnboundLocalError!
x += 1 # because assignment here makes x a local variable within the function
# call the function
foobar()
范围可以声明它明确想要修改全局(模块范围)变量,使用 global 关键字:
x = 5
def foobar():
global x
print(x)
x += 1
foobar() # -> 5
print(x) # -> 6
即使它被隐藏在封闭范围内,这也是可能的:
x = 5
y = 13
def make_closure():
x = 42
y = 911
def func():
global x # sees the global value
print(x, y)
x += 1
return func
func = make_closure()
func() # -> 5 911
print(x, y) # -> 6 13
在 Python 2 中,没有简单的方法来修改封闭范围内的值;通常这是通过使用可变值来模拟的,例如长度为 1 的列表:
def make_closure():
value = [0]
def get_next_value():
value[0] += 1
return value[0]
return get_next_value
get_next = make_closure()
print(get_next()) # -> 1
print(get_next()) # -> 2
然而在 Python 3 中,nonlocal
有以下方法可以解决此问题:
def make_closure():
value = 0
def get_next_value():
nonlocal value
value += 1
return value
return get_next_value
get_next = make_closure() # identical behavior to the previous example.
文档说nonlocal
非本地语句中列出的名称与全局语句中列出的名称不同,它们必须引用封闭范围内的预先存在的绑定(无法明确确定应在其中创建新绑定的范围)。
即nonlocal
始终指的是名称已被绑定(即分配给、包括用作for
目标变量、在with
子句中或作为函数参数)的最内层外部非全局范围。
任何不被视为当前作用域或任何封闭作用域的本地变量都是全局变量。在模块全局字典中查找全局名称;如果未找到,则从内置模块中查找全局名称;模块的名称从 python 2 更改为 python 3;在 python 2 中它原来是__builtin__
而在 python 3 中它现在称为builtins
。如果分配给内置模块的属性,它将作为可读全局变量对任何模块可见,除非该模块使用具有相同名称的全局变量覆盖它们。
读取内置模块也很有用;假设你想在文件的某些部分使用 Python 3 风格的打印函数,但文件的其他部分仍然使用该print
语句。在 Python 2.6-2.7 中,你可以使用以下命令获取 Python 3print
函数:
import __builtin__
print3 = __builtin__.__dict__['print']
实际上from __future__ import print_function
并没有在 Python 2 的任何地方导入该函数 - 相反,它只是禁用当前模块中语句print
的解析规则,像任何其他变量标识符一样处理,从而允许在内置函数中查找该函数。print
`print`print
解决方案 4:
范围的一个稍微更完整的示例:
from __future__ import print_function # for python 2 support
x = 100
print("1. Global x:", x)
class Test(object):
y = x
print("2. Enclosed y:", y)
x = x + 1
print("3. Enclosed x:", x)
def method(self):
print("4. Enclosed self.x", self.x)
print("5. Global x", x)
try:
print(y)
except NameError as e:
print("6.", e)
def method_local_ref(self):
try:
print(x)
except UnboundLocalError as e:
print("7.", e)
x = 200 # causing 7 because has same name
print("8. Local x", x)
inst = Test()
inst.method()
inst.method_local_ref()
输出:
1. Global x: 100
2. Enclosed y: 100
3. Enclosed x: 101
4. Enclosed self.x 101
5. Global x 100
6. global name 'y' is not defined
7. local variable 'x' referenced before assignment
8. Local x 200
解决方案 5:
Python 2.x 的作用域规则已在其他答案中概述。我唯一想补充的是,在 Python 3.0 中,还有非本地作用域的概念(由“nonlocal”关键字表示)。这允许您直接访问外部作用域,并开启了一些巧妙技巧的能力,包括词法闭包(无需涉及可变对象的丑陋黑客)。
编辑:这是包含更多相关信息的PEP。
解决方案 6:
Python 通常使用三个可用的命名空间来解析你的变量。
在执行期间的任何时候,至少有三个嵌套作用域的命名空间可以直接访问:最内层作用域,首先被搜索,包含本地名称;任何封闭函数的命名空间,从最近的封闭作用域开始搜索;中间作用域,接下来被搜索,包含当前模块的全局名称;最外层作用域(最后搜索)是包含内置名称的命名空间。
有两个功能:globals
和locals
向您显示这两个命名空间的内容。
命名空间由包、模块、类、对象构造和函数创建。没有任何其他类型的命名空间。
在这种情况下,对命名函数的调用x
必须在本地名称空间或全局名称空间中解析。
在这种情况下,本地是方法函数的主体Foo.spam
。
全球就是全球。
规则是搜索方法函数(和嵌套函数定义)创建的嵌套局部空间,然后搜索全局空间。就是这样。
没有其他作用域。该for
语句(以及其他复合语句,如if
和try
)不会创建新的嵌套作用域。只有定义(包、模块、函数、类和对象实例)。
在类定义中,名称是类命名空间的一部分。 code2
例如,必须由类名限定。一般来说Foo.code2
。但是,self.code2
也可以工作,因为 Python 对象将包含类视为后备。
对象(类的实例)具有实例变量。这些名称位于对象的命名空间中。它们必须由对象限定。(variable.instance
.)
在类方法中,您有局部变量和全局变量。您说self.variable
选择实例作为命名空间。您会注意到这self
是每个类成员函数的参数,使其成为本地命名空间的一部分。
参见Python 作用域规则、Python 作用域、变量作用域。
解决方案 7:
在哪里可以找到 x?
由于您尚未定义 x,因此未找到它。:-)如果您将其放在那里,则可以在 code1(全局)或 code3(本地)中找到它。
code2(类成员)对于同一个类的方法内部的代码不可见 - 您通常使用 self 访问它们。code4/code5(循环)与 code3 位于同一范围内,因此如果您在那里写入 x,您将更改 code3 中定义的 x 实例,而不是创建新的 x。
Python 是静态作用域的,因此如果将“spam”传递给另一个函数,spam 仍然可以访问它来自的模块中的全局变量(在 code1 中定义)以及任何其他包含作用域(见下文)。code2 成员将再次通过 self 访问。
lambda 与 def 没什么不同。如果在函数内部使用 lambda,则与定义嵌套函数相同。从 Python 2.2 开始,可以使用嵌套作用域。在这种情况下,您可以在任何函数嵌套级别绑定 x,Python 将选择最内层的实例:
x= 0
def fun1():
x= 1
def fun2():
x= 2
def fun3():
return x
return fun3()
return fun2()
print fun1(), x
2 0
fun3 从最近的包含作用域(即与 fun2 关联的函数作用域)看到实例 x。但在 fun1 中定义的其他 x 实例和全局实例不受影响。
在 nested_scopes 之前 — — 在 Python 2.1 之前的版本中,以及在 2.1 中,除非你特别要求使用 from-future-import 的功能 — — fun1 和 fun2 的作用域对 fun3 不可见,因此 S.Lott 的答案成立,你会得到全局的 x:
0 0
解决方案 8:
Python名称解析仅知道以下类型的作用域:
内置范围提供内置函数,例如
print
,int
或zip
,模块全局作用域始终是当前模块的顶层,
三个可相互嵌套的用户定义范围,即
函数闭包作用域,来自任何封闭的
def
块、lambda
表达式或理解。函数局部作用域,在
def
块、lambda
表达式或推导式内部,类范围,在
class
块内。
值得注意的是,其他构造如if
、for
或with
语句没有自己的范围。
范围 TLDR:名称的查找从使用该名称的范围开始,然后是任何封闭范围(不包括类范围),到模块全局变量,最后是内置函数 - 使用此搜索顺序中的第一个匹配项。默认情况下,对范围的分配是当前范围 - 特殊形式nonlocal
和global
必须用于从外部范围分配名称。
最后,理解和生成表达式以及:=
赋值表达式组合起来有一个特殊规则。
嵌套作用域和名称解析
这些不同的作用域构成了一个层次结构,其中内置函数和全局函数始终构成基础,而闭包、局部函数和类作用域则按照词汇定义进行嵌套。也就是说,只有源代码中的嵌套才重要,而不是调用堆栈之类的东西。
print("builtins are available without definition")
some_global = "1" # global variables are at module scope
def outer_function():
some_closure = "3.1" # locals and closure are defined the same, at function scope
some_local = "3.2" # a variable becomes a closure if a nested scope uses it
class InnerClass:
some_classvar = "3.3" # class variables exist *only* at class scope
def inner_function(self):
some_local = "3.2" # locals can replace outer names
print(some_closure) # closures are always readable
return InnerClass
尽管class
创建了一个作用域,并且可能具有嵌套的类、函数和推导式,但作用域的名称class
对于封闭的作用域而言是不可见的。这将创建以下层次结构:
┎ builtins [print, ...]
┗━┱ globals [some_global]
┗━┱ outer_function [some_local, some_closure]
┣━╾ InnerClass [some_classvar]
┗━╾ inner_function [some_local]
名称解析始终从访问名称的当前范围开始,然后向上查找层次结构,直到找到匹配项。例如,在和some_local
内部查找从相应的函数开始 - 并立即分别找到和中定义的。当名称不是本地时,它会从定义它的最近封闭范围中获取 - 分别在和内部查找直到和 内置函数。outer_function
`inner_functionsome_local
outer_functioninner_function
some_closureprint
inner_function`outer_function
范围声明和名称绑定
默认情况下,名称属于任何与值绑定的作用域。在内部作用域中再次绑定相同的名称会创建一个具有相同名称的新变量 - 例如,some_local
分别存在于outer_function
和中inner_function
。就作用域而言,绑定包括设置名称值的任何语句 - 赋值语句,也包括循环的迭代变量for
或上下文管理器的名称with
。值得注意的是,del
也算作名称绑定。
当名称必须引用外部变量并绑定在内部作用域中时,必须将该名称声明为非本地。对于不同类型的封闭作用域,存在单独的声明:nonlocal
always 引用最近的闭包,global
always 引用全局名称。值得注意的是,nonlocal
never 引用全局名称并global
忽略所有同名闭包。没有引用内置作用域的声明。
some_global = "1"
def outer_function():
some_closure = "3.2"
some_global = "this is ignored by a nested global declaration"
def inner_function():
global some_global # declare variable from global scope
nonlocal some_closure # declare variable from enclosing scope
message = " bound by an inner scope"
some_global = some_global + message
some_closure = some_closure + message
return inner_function
值得注意的是,函数本地和nonlocal
在编译时解析。nonlocal
名称必须存在于某个外部作用域中。相反,global
名称可以动态定义,并且可以随时在全局作用域中添加或删除。
理解和赋值表达式
列表、集合和字典推导以及生成器表达式的作用域规则与函数的作用域规则几乎相同。同样,赋值表达式的作用域规则与常规名称绑定的作用域规则几乎相同。
推导式和生成器表达式的作用域与函数作用域属于同一类型。作用域中绑定的所有名称(即迭代变量)都是推导式/生成器和嵌套作用域的局部变量或闭包。所有名称(包括可迭代变量)都使用函数内部适用的名称解析进行解析。
some_global = "global"
def outer_function():
some_closure = "closure"
return [ # new function-like scope started by comprehension
comp_local # names resolved using regular name resolution
for comp_local # iteration targets are local
in "iterable"
if comp_local in some_global and comp_local in some_global
]
赋值表达式:=
适用于最近的函数、类或全局作用域。值得注意的是,如果赋值表达式的目标已声明nonlocal
或global
位于最近的作用域中,则赋值表达式会像常规赋值一样遵守这一点。
print(some_global := "global")
def outer_function():
print(some_closure := "closure")
但是,理解/生成器内的赋值表达式适用于理解/生成器最近的封闭范围,而不是理解/生成器本身的范围。当嵌套多个理解/生成器时,将使用最近的函数或全局范围。由于理解/生成器范围可以读取闭包和全局变量,因此赋值变量在理解中也是可读的。从理解到类范围的赋值是无效的。
print(some_global := "global")
def outer_function():
print(some_closure := "closure")
steps = [
# v write to variable in containing scope
(some_closure := some_closure + comp_local)
# ^ read from variable in containing scope
for comp_local in some_global
]
return some_closure, steps
虽然迭代变量对于它所绑定的理解来说是本地的,但是赋值表达式的目标不会创建局部变量,而是从外部范围读取:
┎ builtins [print, ...]
┗━┱ globals [some_global]
┗━┱ outer_function [some_closure]
┗━╾ <listcomp> [comp_local]
解决方案 9:
在 Python 中,
任何被赋值的变量都是出现在赋值处的块的局部变量。
如果在当前范围内找不到变量,请参考 LEGB 命令。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件