范围规则的简短描述

2024-11-15 08:37:00
admin
原创
16
摘要:问题描述:Python 的作用域规则到底是什么?如果我有一些代码:code1 class Foo: code2 def spam..... code3 for code4..: code5 x() 在哪里x找到?一些可能的选择包括以下列表:在封闭...

问题描述:

Python 的作用域规则到底是什么?

如果我有一些代码:

code1
class Foo:
   code2
   def spam.....
      code3
      for code4..:
       code5
       x()

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

  1. 在封闭的源文件中

  2. 在类命名空间中

  3. 在函数定义中

  4. 在 for 循环中索引变量

  5. 在 for 循环中

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

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


解决方案 1:

实际上,Python 作用域解析的简明规则来自《学习 Python》第 3 版。(这些规则特定于变量名,而不是属性。如果引用时不使用句点,则适用这些规则。)

LEGB 规则

  • 本地- 在函数(deflambda)中以任何方式分配的名称,并且未在该函数中声明为全局名称

  • 封闭def函数 - 在任何和所有静态封闭函数(或)的本地范围内分配的名称lambda,从内到外

  • 全局(模块)——在模块文件的顶层指定的名称,或者通过执行文件global中的语句指定的名称def

  • 内置(Python) —内置名称模块中预先分配的名称:open、、等range`SyntaxError`

因此,在

code1
class Foo:
    code2
    def spam():
        code3
        for code4:
            code5
            x()

循环for没有自己的命名空间。按照 LEGB 顺序,作用域将是

  • L:本地def spam(在code3code4和 中code5

  • E:任何封闭函数(如果整个示例在另一个中def

  • xG:在模块中(在)是否有任何全局声明code1

  • B:xPython 中的任何内置函数。

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 通常使用三个可用的命名空间来解析你的变量。

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

有两个功能:globalslocals向您显示这两个命名空间的内容。

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

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

在这种情况下,本地是方法函数的主体Foo.spam

全球就是全球。

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

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

在类定义中,名称是类命名空间的一部分。 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名称解析仅知道以下类型的作用域:

  1. 内置范围提供内置函数,例如printintzip

  2. 模块全局作用域始终是当前模块的顶层,

  3. 三个可相互嵌套的用户定义范围,即

    1. 函数闭包作用域,来自任何封闭的def块、lambda表达式或理解。

    2. 函数局部作用域,在def块、lambda表达式或推导式内部,

    3. 范围,在class块内。

值得注意的是,其他构造如ifforwith语句没有自己的范围。

范围 TLDR:名称的查找从使用该名称的范围开始,然后是任何封闭范围(不包括类范围),到模块全局变量,最后是内置函数 - 使用此搜索顺序中的第一个匹配项。默认情况下,对范围的分配是当前范围 - 特殊形式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]

名称解析始终从访问名称的当前范围开始,然后向上查找层次结构,直到找到匹配项。例如,在和some_local内部查找从相应的函数开始 - 并立即分别找到和中定义的。当名称不是本地时,它会从定义它的最近封闭范围中获取 - 分别在和内部查找直到和 内置函数。outer_function`inner_functionsome_localouter_functioninner_functionsome_closureprintinner_function`outer_function


范围声明和名称绑定

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

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

解决方案 9:

在 Python 中,

任何被赋值的变量都是出现在赋值处的块的局部变量。

如果在当前范围内找不到变量,请参考 LEGB 命令。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用