作用域规则的简短描述

523
Python的作用域规则到底是什么?
如果我有一些代码:
code1
class Foo:
   code2
   def spam.....
      code3
      for code4..:
       code5
       x()

在哪里可以找到x?一些可能的选择包括以下列表:

  1. 在封闭的源文件中
  2. 在类命名空间中
  3. 在函数定义中
  4. 在for循环索引变量中
  5. 在for循环内部

此外,在执行期间还存在上下文,当函数spam被传递到其他地方时。也许lambda函数的传递方式有所不同?

肯定有一个简单的参考或算法存在。这对于中级Python程序员来说是一个令人困惑的世界。


4
作用域规则在Python文档中被描述得相当简洁,但也十分完整:https://docs.python.org/3/reference/executionmodel.html#naming-and-binding。 - jefe2000
9个回答

451

实际上,这是 Python 作用域解析的简洁规则,来自Learning Python, 3rd. Ed.。(这些规则仅适用于变量名,不适用于属性。如果您没有使用句点引用它,则应用这些规则。)

LEGB 规则

  • Local(局部)— 函数内任何方式分配的名称(deflambda),并且在该函数中未声明为全局的名称

  • Enclosing-function(嵌套函数)— 分配在所有静态封闭函数(deflambda)的局部作用域中,从内部到外部

  • Global(全局) — 分配在模块文件的顶层,或通过在文件中的def中执行global语句

  • Built-in(内置) — 预先分配在内置名称模块中的名称:openrangeSyntaxError

因此,在以下情况下:

code1
class Foo:
    code2
    def spam():
        code3
        for code4:
            code5
            x()
< p > for 循环没有自己的命名空间。按照LEGB顺序,作用域将会是

  • L:在def spam内的局部变量(在code3code4code5中)
  • E:任何封闭函数(如果整个示例都在另一个def中)
  • G:模块中是否声明了任何全局变量x(在code1中)?
  • B:Python中任何内置的x

xcode2中永远不会被找到(即使在您可能希望它出现的情况下,请参见Antti's answerhere)。


49
全球访问的一个注意事项 - 读取全局变量可以在没有显式声明的情况下进行,但是在不声明global(var_name)的情况下写入它将创建一个新的本地实例。 - Peter Gibson
12
实际上,@Peter,“global(var_name)”在语法上是不正确的。正确的语法应该是没有括号的“global var_name”。不过,你说得有道理。 - martineau
如果是这样,那么为什么下面的"bar"函数无法访问foo函数中的"y"变量:>>> def foo(x): ... y = x ... def bar(z): ... y = z ... bar(5) ... print x,y ... >>> foo(3) 3 3 - Jonathan Mayer
3
因为每个 y 都在被写入,而且没有 global y 的声明--请参考 @Peter 的评论。 - martineau
@LakshmanPrasad 它属于“E”类,但有一个特殊的行为值得一提:它是一个类变量,因此在其对象中它是“全局”的。如果你不知道自己在做什么而对它进行赋值,那么这将导致不可预测且难以调试的问题。 - Ctrl-C
1
@Ctrl-C 其实并不是这样的;就作用域而言,类属性没有什么特别之处。它们是共享的,因为 self.someClassAttribute 将引用相同的对象,无论 self 引用哪个实例,但名称本身必须被用作实例或类本身的属性。实际的特殊行为是,在评估类体中的语句时,类属性将遮盖包含作用域中存在的任何变量。例如,j = 0; class Foo: j = 3; print(j); # end of class; print(j) 将输出 3,然后是 0。 - chepner

161
基本上,在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",它将以类似于全局变量的方式在嵌套函数作用域中发挥作用。


121

关于Python3时间,没有一个全面的答案,所以我在这里做了个回答。这里描述的大多数内容都在Python 3文档的4.2.2名称解析中详细说明。

如其他答案所提供的那样,LEGB是四个基本作用域,即本地作用域、嵌套作用域、全局作用域和内置作用域。除此之外,还有一个特殊的作用域,即类体,它不包括定义在类内部的方法的封闭作用域; 类体内的任何赋值都会将变量绑定到类体内。

特别地,除了def和class之外,没有任何块语句创建变量作用域。在Python 2中,列表推导式不会创建变量作用域,但是在Python 3中,列表推导式中的循环变量会创建一个新作用域。

为了展示类体的特殊性

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中得到解决 - 没有推导表达式或生成器泄漏变量。


全局(Global)实际上指的是模块作用域(module scope); 主要的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的列表:


在 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 语句中列出的名称,与在全局语句中列出的不同,必须引用封闭作用域中的预先存在的绑定(无法明确确定应在哪个作用域中创建新绑定)。

也就是说,nonlocal 总是指向最内层外部的非全局作用域,其中名称已被绑定(即分配给、包括用作 for 目标变量、在 with 子句中或作为函数参数使用)。


任何未被视为当前作用域或任何封闭作用域本地的变量均为全局变量。全局名称将在模块全局词典中查找;如果未找到,则从内置模块中查找全局变量。模块的名称已从 Python 2 更改为 Python 3;在 Python 2 中,它是 __builtin__,而在 Python 3 中现在称为 builtins。如果您将一个属性分配给内置模块,那么此后任何模块都将看到它作为可读的全局变量,除非该模块使用其自己具有相同名称的全局变量进行了遮蔽。


阅读内置模块也可能很有用。假设您想在文件的某些部分中使用 Python 3 风格的 print 函数,但其他部分仍使用 print 语句。在 Python 2.6-2.7 中,您可以使用以下方法获取 Python 3 的 print 函数:

import __builtin__

print3 = __builtin__.__dict__['print']
from __future__ import print_function 实际上在 Python 2 中并没有导入 print 函数 - 它只是禁用了当前模块的解析规则,处理 print 就像处理其他变量标识符一样,并因此允许在内置函数中查找 print 函数。

很高兴终于看到一个提到特殊的类体范围的答案,这不在广为人知的LEGB规则之列。 - martineau

28

作用域的稍微更完整的例子:

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

6
这是很好的答案。然而,我认为应该强调methodmethod_local_ref之间的区别。method可以访问全局变量并像5. Global x那样打印它。但是method_local_ref不能这样做,因为后面它定义了一个同名的局部变量。您可以通过删除 x = 200 这一行来测试这一点,看到不同之处。 - kiril
@brianray:那z怎么办? - Malik A. Rumi
@kiril 我添加了一条关于那个的注释。 - brianray
@MalikA.Rumi 我移除了 z,因为它不是很有趣。 - brianray
令人惊讶的是,这是我在所有SO上找到的唯一关于Python作用域的清晰解释。只是使用了一个非常基本的例子。谢谢! - not2qubit
可能需要提到 e 绑定到 except 块并在之后被删除。更多讨论请参见此处 - Jens

24

Python 2.x的作用域规则在其他答案中已经概述过。我要补充的是,在Python 3.0中,还引入了非本地作用域的概念(通过 "nonlocal" 关键字表示)。这允许你直接访问外部作用域,并打开了一些巧妙技巧的可能性,包括词法闭包(无需涉及可变对象的丑陋技巧)。

编辑:这里有更多相关信息的PEP


16

Python使用三个命名空间--通常情况下--来解析变量。

在执行期间,至少有三个嵌套作用域具有直接访问的命名空间: 最内层的作用域首先被搜索,包含局部名称;任何封闭函数的命名空间,从最近的封闭作用域开始搜索; 其次是中间作用域,其中包含当前模块的全局名称;最外层作用域(最后搜索)是包含内置名称的命名空间。

有两个函数:globalslocals,可以显示这些命名空间的内容。

命名空间由软件包、模块、类、对象构造和函数创建。没有其他类型的命名空间。

在这种情况下,对名为x的函数的调用必须在局部命名空间或全局命名空间中解析。

在本例中,局部指的是方法函数Foo.spam的正文。

全局是全局的意思。

规则是搜索由方法函数(和嵌套函数定义)创建的嵌套局部空间,然后搜索全局。就是这样。

没有其他范围。for语句(和其他复合语句,如if和try)不会创建新的嵌套作用域。只有定义(软件包、模块、函数、类和对象实例)。

在类定义内部,名称是类命名空间的一部分。例如,code2必须由类名限定。通常是Foo.code2。但是,self.code2也可以工作,因为Python对象将包含类作为后备。

对象(类的实例)具有实例变量。这些名称位于对象的命名空间中。它们必须由对象限定(variable.instance)。

在类方法中,您有局部变量和全局变量。使用self.variable将实例作为命名空间选取。注意,self是每个类成员函数的参数,使其成为局部命名空间的一部分。

参见 Python作用域规则Python作用域变量作用域


6
这段内容已经过时。自从2.1版本(7年前)以后,由于嵌套函数引入了新的作用域,所以函数内的函数将会访问到它的本地作用域、包含函数的作用域和全局作用域(还有内置作用域)。请注意,这里没有其他需要翻译的内容。 - Brian
抱歉,这不再适用了。 Python 有两个可用的命名空间,全局和局部于某些东西。 - Rizwan Kassim

10

x在哪里找到?

你没有定义x,因此找不到它。:-) 如果你把它放在code1(全局)或code3(本地)中,它可能在那里找到。

类成员的code2对于同一类中方法内部的代码是不可见的,通常使用self来访问它们。code4/code5(循环)与code3处于相同的范围内,因此如果你在其中写入x,则会更改在code3中定义的x实例,而不是创建新的x。

Python是静态作用域的,因此如果你将“spam”传递给另一个函数,spam仍然可以访问它来自的模块中(在code1中定义的)和任何其他包含范围(见下文)。代码2的成员可以通过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

7

Python名称解析只知道以下几种作用域:

  1. 提供内置函数(如printintzip)的内置作用域,
  2. 始终是当前模块顶层的模块全局作用域,
  3. 三个用户定义的作用域可以相互嵌套,分别是
    1. 函数闭包作用域,从任何封闭的def块、lambda表达式或推导中获取,
    2. 函数局部作用域,在def块、lambda表达式或推导中,
    3. 作用域,在class块中。

值得注意的是,其他结构(如ifforwith语句)没有自己的作用域。

作用域概述: 名称的查找始于使用名称的作用域,然后是任何封闭的作用域(不包括类作用域),然后是模块全局作用域,最后是内置作用域——使用此搜索顺序中的第一个匹配项。 默认情况下,作用域的赋值是当前作用域——必须使用nonlocalglobal特殊形式从外部作用域分配名称。

最后,推导和生成器表达式以及:=赋值表达式在组合时有一个特殊规则。


嵌套作用域和名称解析

这些不同的作用域构建了一个层次结构,其中内置作用域和全局作用域始终形成基础,而闭包、局部和类作用域作为词法上定义的嵌套。也就是说,只有源代码中的嵌套才重要,而不是调用堆栈等其他因素。

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]

名称解析始终从访问名称的当前作用域开始,然后向上层次结构移动直到找到匹配项。例如,在outer_functioninner_function中查找some_local时,分别从相应的函数开始,并立即找到在outer_functioninner_function中定义的some_local。当名称不是本地名称时,它将从定义它的最近封闭作用域获取 - 在inner_function中查找some_closureprint时,分别搜索直到outer_function和内置。


作用域声明和名称绑定

默认情况下,一个名称属于任何绑定它的值的作用域。在内部作用域中再次绑定相同的名称会创建一个新变量,该变量具有相同的名称 - 例如,some_local分别存在于outer_functioninner_function中。就作用域而言,绑定包括任何设置名称值的语句 - 赋值语句,以及for循环的迭代变量或with上下文管理器的名称。值得注意的是,del也算作名称绑定。

当名称必须引用外部变量并且在内部作用域中绑定时,该名称必须声明为非本地名称。存在不同类型的封闭作用域的单独声明:nonlocal始终指向最近的闭包,global始终指向全局名称。值得注意的是,nonlocal从不指向全局名称,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
    ]

:= 赋值表达式在最近的函数、类或全局作用域中起作用。特别地,如果赋值表达式的目标已在最近作用域中声明为 nonlocalglobal,那么这个赋值表达式将像普通赋值一样遵循这一点。

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]

我认为你的答案不完整。except..as语句也会创建一个新的作用域。例如,如果你写了try: raise ValueError('x'); except ValueError as v: pass,你就不能在except子句的作用域之外访问v。 - John Henckel
1
@JohnHenckel 这不是一个新的作用域。except在完成后会从其作用域中删除其目标。目标遵循常规的作用域规则,例如,它甚至可以声明为global,在这种情况下将从全局作用域中删除。演示代码 - MisterMiyagi
哦,这很奇怪。谢谢你向我解释。 - John Henckel

2

在Python中,任何被赋值的变量都是局部变量,只能在该赋值所在的代码块内使用。

如果当前作用域中找不到变量,请参考LEGB顺序。

LEGB顺序指的是:Local、Enclosing、Global、Built-in。

最初的回答。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接