Python中的可变范围和名称解析

gob*_*lin 26 python scope

我认为我从根本上不了解Python如何完成变量范围和名称解析之类的事情。特别是broken()以下功能无法正常工作的事实令我感到惊讶。而且,尽管我在网上闲逛了一段时间以寻找有用的解释,但我仍然不明白。任何人都可以解释或链接到有关这些内容在Python中如何工作的良好描述,并提供足够的详细信息,使您broken()在阅读相关材料后似乎不起作用是显而易见的?

# Why does this code work fine
def okay0():
    def foo():
        L = []
        def bar():
            L.append(5)
        bar()
        return L
    foo()

# and so does this
def okay1():
    def foo():
        def bar():
            L.append(5)
        L = []
        bar()
        return L
    foo()

# but the following code raises an exception?
def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

# Example
test_list = [okay0, okay1, broken]
for test_function in test_list:
    try:
        test_function()
    except:
        print("broken")
    else:
        print("okay")
Run Code Online (Sandbox Code Playgroud)

cax*_*atl 18

在另一个函数中定义的一个函数可以访问其父级的作用域。

根据您的具体情况,L始终在中定义foo()。在前两个示例中,bar()也是在中定义的foo(),因此它可以L通过上面的规则进行访问(即,foo()是bar()的父项)。

然而,在broken(),bar()和foo()是兄弟。他们对彼此的范围一无所知,因此bar()看不见L。

从文档中:

尽管范围是静态确定的,但它们是动态使用的。在执行过程中的任何时候,至少有三个嵌套作用域可以直接访问其名称空间:

  • 最里面的作用域(首先搜索)包含本地名称
  • 从最接近的封闭范围开始搜索的任何封闭函数的范围都包含非本地名称,也包含非全局名称
  • 倒数第二个范围包含当前模块的全局名称
  • 最外面的范围(最后搜索)是包含内置名称的名称空间

现在,okay1如果L在文字上定义之后,为什么会起作用bar()?

Python在必须实际运行代码之前不会尝试解析标识符(动态绑定,如@Giusti的答案所述)。

当Python执行该函数时,它将看到一个标识符,L并在本地名称空间中寻找它。在cpython实现上,它是一个实际的字典,因此它将在字典中查找名为的键L。

如果找不到它,它将检查任何封闭函数的范围,即代表该封闭函数的本地名称空间的其他字典。

需要注意的是,即使L是定义后bar(),当bar()被称为,L已定义。因此,当bar()执行时,L已经存在于的本地名称空间中foo(),当Python L在内看不到该名称空间时将进行搜索bar()。

文档的支持部分:

名称空间是从名称到对象的映射。大多数名称空间目前都实现为Python字典,但通常不会以任何方式引起注意(性能除外),并且将来可能会发生变化。

(...)

函数的本地名称空间是在调用函数时创建的,并在函数返回或引发函数中未处理的异常时删除。(实际上,忘记是描述实际情况的更好方法。)当然,递归调用每个都有自己的本地名称空间。

作用域是Python程序的文本区域,可直接访问名称空间。这里的“直接访问”是指对名称的不合格引用试图在名称空间中找到该名称。

  • 这里的关键是* defined *,您应该将其与* / *进行对比,以真正回答OP问题。 (3认同)

giu*_*sti 6

它比看起来简单。

第一种情况可能是最明显的:

 def okay0():
    def foo():
        L = []
        def bar():
            L.append(5)
        bar()
        return L
    foo()
Run Code Online (Sandbox Code Playgroud)

这里您所拥有的是常规范围规则。L并且bar属于同一范围,并且L首先声明。因此bar()可以访问L。

第二个样本也类似:

def okay1():
    def foo():
        def bar():
            L.append(5)
        L = []
        bar()
        return L
    foo()
Run Code Online (Sandbox Code Playgroud)

在这里,L和bar()属于同一个范围。他们是本地的foo()。它看起来可能有所不同,因为Python使用动态绑定。即,仅在调用函数时解析名称Lin foo()的解析。到那时,Python已经知道这L是包含的同一函数的局部变量foo(),因此访问是有效的。

但是,尽管Python具有动态绑定,但它没有动态范围,因此将失败:

def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()
Run Code Online (Sandbox Code Playgroud)

在这里,有两个名为的变量L。一个是本地的foo(),另一个是本地的bar()。由于这些函数不是嵌套的,并且Python没有动态范围,因此它们是两个不同的变量。由于bar()未L在分配中使用,因此您将获得异常。