【问题标题】:Variable scope and name resolution in PythonPython中的变量范围和名称解析
【发布时间】:2020-01-07 21:30:27
【问题描述】:

我认为我根本不了解 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")

【问题讨论】:

  • 你试图从不同的范围访问一个本地,即bar的范围内,L不存在。
  • broken() 中,在foo() 内部L 是一个局部变量(因为它有一个赋值,并且没有global 声明)。除了foo() 之外,唯一可能被引用(甚至存在)的地方是嵌套在foo() 中的函数。
  • 有人曾经说过,要真正获得 Python,你必须了解它的 namespaces。这听起来很简单,但它实际上是用这种语言提供的非常深刻的建议。如果你问自己这种类型的问题,你就在路上了!
  • 请注意,具有词法作用域的任何语言(包括 Java 或 C/C++)都会以相同的方式运行......所以并不是你不理解 Python,您似乎不太了解 lexical scoping,这是自 70 年代以来几乎所有语言的 范围规则。
  • @GiacomoAlzetta。对一个有趣且发人深省的问题不必要地刻薄。当然,这不是更广泛的 Python 社区会采用的基调,而且对它更好。

标签: python scope


【解决方案1】:

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

在您的特定情况下,L 始终在 foo() 中定义。在前两个示例中,bar() 也在foo() 中定义,因此它可以通过上述规则访问L(即foo()bar() 的父级)。

但是,broken()bar()foo() 是兄弟姐妹。他们对彼此的作用域一无所知,所以bar() 看不到L

来自documentation

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

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

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

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

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

如果没有找到,它会检查任何封闭函数的作用域,即表示封闭函数的本地命名空间的其他字典。

请注意,即使Lbar() 之后定义,当bar()调用时,L 已经被定义。所以,当bar()被执行时,L已经存在于foo()的本地命名空间中,当Python在bar()中没有看到L时就会搜索到。

支持文档:

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

(...)

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

作用域是 Python 程序的文本区域,其中命名空间可直接访问。这里的“可直接访问”意味着对名称的非限定引用会尝试在命名空间中查找该名称。

【讨论】:

  • 这里的关键是定义,你应该对比调用/执行来真正回答OP问题。
  • @Calimo,我注意到我的答案不完整,没有解释为什么okay1 有效,并为此添加了一些文本。在那,我采纳了你的建议,让我知道你的想法。答案现在可能有点太长了,但我认为我不能再缩短它了
【解决方案2】:

它比看起来简单。

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

 def okay0():
    def foo():
        L = []
        def bar():
            L.append(5)
        bar()
        return L
    foo()

这里只有常规范围规则。 Lbar 属于同一个作用域,L 先声明。所以bar()可以访问L

第二个样本也类似:

def okay1():
    def foo():
        def bar():
            L.append(5)
        L = []
        bar()
        return L
    foo()

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

然而,虽然 Python 具有动态绑定,但它确实具有动态范围,因此这将失败:

def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

这里有两个变量名为L。一个是foo() 的本地,另一个是bar() 的本地。由于这些函数不是嵌套的并且 Python 没有动态范围,它们是两个不同的变量。因为bar() 没有在赋值中使用L,所以会出现异常。

【讨论】:

  • 我的回答没有涵盖为什么okay1 起作用,所以我添加了一个部分,部分基于您的回答和@Calimo 的评论。希望你能适应它。
【解决方案3】:

broken() 函数抛出以下错误:

NameError: name 'L' is not defined

这是因为 L 是在 foo() 中定义的,并且是该函数的本地函数。当您尝试在 bar() 等其他函数中引用它时,它不会被定义。

def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

基本上,如果您在函数中声明一个变量,那么它将是该函数的本地变量......

【讨论】:

    【解决方案4】:

    fixed 中带有L = ... 的行在fixed 的范围内声明了L。 (return 在它确保没有实际执行分配之前,仅用于范围确定。)nonlocal L 的行声明 L 内部 foo 引用外部范围的 L,在此案例,fixed 的。否则,由于对L 的赋值存在于foo 中,它会引用foo 中的L 变量。

    基本上:

    • 对变量的赋值使其作用域为封闭函数。
    • nonlocalglobal 声明会覆盖范围,而是分别使用(最内层?最外层?)范围和声明的变量或全局范围。
    def fixed():
        def foo():
            nonlocal L  # Added
            L = []
            bar()
            return L
        def bar():
            L.append(5)
        foo()
        return  # Added
        L = ...  # Added
    

    【讨论】:

    • 你能在不赋值的情况下声明一个变量吗?
    • @Bergi 不是 AFAIK。
    【解决方案5】:

    你想知道的最重要的概念是environment evaluation model,它简单但功能强大。

    让我给你一个好的material

    如果你想看Python文档,可以看4. Execution model — Python 3.7.4 documentation,很简洁。

    在代码块中使用名称时,将使用最近的解析 封闭范围。对代码块可见的所有此类范围的集合是 调用块的环境

    【讨论】:

      猜你喜欢
      • 2013-08-09
      • 2016-06-25
      • 1970-01-01
      • 1970-01-01
      • 2017-12-08
      • 1970-01-01
      • 2011-02-17
      • 2011-04-15
      • 1970-01-01
      相关资源
      最近更新 更多