【问题标题】:Does lexical scope have a dynamic aspect?词法作用域有动态方面吗?
【发布时间】:2015-12-20 01:29:03
【问题描述】:

可以在编译时(或通过静态分析器,因为我的示例是在 Python 中)仅根据源代码中的位置来计算访问词法范围似乎很常见。

这是一个非常简单的例子,其中一个函数有两个闭包,a 的值不同。

def elvis(a):
  def f(s):
    return a + ' for the ' + s
  return f

f1 = elvis('one')
f2 = elvis('two')
print f1('money'), f2('show')

我的想法没有问题,当我们在阅读函数f的代码时,当我们看到a时,它并没有在f中定义,所以我们弹出到封闭函数并找到一个在那里,这就是f 中的a 所指的。源代码中的位置足以告诉我f 从封闭范围中获取a 的值。

但正如here 所述,当调用函数时,其本地框架会扩展其父环境。所以在运行时进行环境查找是没有问题的。但我不确定的是,静态分析器总是可以在代码运行之前在编译时确定 which 闭包。在上面的例子中,很明显elvis 有两个闭包,很容易跟踪它们,但其他情况就不会这么简单了。直觉上,我很担心静态分析的尝试可能会遇到一般的停止问题。

那么词法作用域真的有一个动态的方面吗,源代码中的位置告诉我们涉及到一个封闭作用域,但不一定是指哪个闭包?或者这是编译器中已解决的问题,函数内对其闭包的所有引用真的可以静态详细地计算出来?

或者答案是否取决于编程语言——在这种情况下,词法作用域并不像我想象的那么强大?

[编辑@cmets:

就我的示例而言,我可以重申我的问题:我阅读了诸如“可以在编译时确定词法分辨率”之类的声明,但想知道如何在 f1f2 中引用 a 的值静态/在编译时(通常)制定。

解决方案是,词法作用域并没有要求太多。 LS可以告诉我们,在编译时,只要我在 f 中,就会定义名为 asomething(这显然可以静态计算;这是词法范围的定义),但是确定它实际需要的 value 是 1) 超出 LS概念,2) 在运行时完成(非静态),因此在某种意义上是动态的,但当然 3) 使用不同于动态范围的规则。

引用@PatrickMaupin 的话,要传达的信息是“仍然需要完成一些动态工作。” ]

【问题讨论】:

标签: python programming-languages lexical-scope


【解决方案1】:

可以通过多种方式实现闭包。其中之一是实际捕获环境......换句话说,考虑这个例子

def foo(x):
    y = 1
    z = 2
    def bar(a):
        return (x, y, a)
    return bar

环境捕获解决方案如下:

  1. 输入foo 并构建包含xyzbar 名称的本地框架。名称 x 绑定到参数,名称 yz 绑定到 1 和 2,名称 bar 绑定到闭包
  2. 分配给bar 的闭包实际上捕获了整个父框架,因此当它被调用时,它可以在其自己的本地框架中查找名称a,并且可以在捕获的父框架中查找xy

使用这种方法(即不是 Python 使用的方法)变量z 将保持活动状态,只要闭包仍然存在,即使它没有被闭包引用。

另一种选择,实现起来稍微复杂一些,例如:

  1. 在编译时分析代码并发现分配给bar 的闭包从当前范围捕获名称xy
  2. 因此这两个变量被归类为“单元格”,它们与本地框架分开分配
  3. 闭包存储这些变量的地址,每次访问它们都需要双重间接(单元格是指向实际存储值的位置的指针)

这需要在创建闭包时花费一些额外的时间,因为每个捕获的单元格都需要复制到闭包对象中(而不是仅仅复制指向父框架的指针),但具有不捕获整个的优点例如,foo 返回后,z 将不会保持活动状态,只有 xy 会保持活动状态。

这就是 Python 所做的……基本上是在编译时发现闭包(命名函数或 lambda)时执行子编译。在编译期间,当存在解析为父函数的查找时,变量被标记为单元格。

一个小烦恼是,当一个参数被捕获时(如foo 示例中),还需要在序言中进行额外的复制操作以转换单元格中传递的值。这在 Python 中在字节码中是不可见的,而是由调用机制直接完成的。

另一个烦恼是,即使在父上下文中,每次访问捕获的变量都需要双重间接。

优点是闭包只捕获真正引用的变量,当它们不捕获任何生成的代码时,它与常规函数一样高效。

要了解它在 Python 中的工作原理,您可以使用 dis 模块检查生成的字节码:

>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (1)
              3 STORE_DEREF              1 (y)

  3           6 LOAD_CONST               2 (2)
              9 STORE_FAST               1 (z)

  4          12 LOAD_CLOSURE             0 (x)
             15 LOAD_CLOSURE             1 (y)
             18 BUILD_TUPLE              2
             21 LOAD_CONST               3 (<code object bar at 0x7f6ff6582270, file "<stdin>", line 4>)
             24 LOAD_CONST               4 ('foo.<locals>.bar')
             27 MAKE_CLOSURE             0
             30 STORE_FAST               2 (bar)

  6          33 LOAD_FAST                2 (bar)
             36 RETURN_VALUE
>>>

如您所见,生成的代码使用STORE_DEREF(写入单元格的操作,因此使用双重间接)将1 存储到y 中,而是使用a 将2 存储到z STORE_FASTz 未被捕获,只是当前帧中的一个局部变量)。当foo 的代码开始执行时,x 已经被调用机制包装到一个单元格中。

bar 只是一个局部变量,所以STORE_FAST 用于写入它,但是要构建闭包xy 需要单独复制(在调用之前将它们放入一个元组中MAKE_CLOSURE 操作码)。

闭包本身的代码通过以下方式可见:

>>> dis.dis(foo(12))
  5           0 LOAD_DEREF               0 (x)
              3 LOAD_DEREF               1 (y)
              6 LOAD_FAST                0 (a)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE

您可以看到在返回的闭包内xy 使用LOAD_DEREF 访问。无论在嵌套函数层次结构中定义了多少层“上”变量,它实际上只是一个双重间接,因为在构建闭包时会付出代价。相对于局部变量,封闭变量的访问速度(通过常数因子)仅稍慢...在运行时不需要遍历“作用域链”。

更复杂的编译器,如 SBCL(Common Lisp 生成本机代码的优化编译器)也会进行“转义分析”,以检测闭包是否真的能在封闭函数中存活下来。 如果没有发生这种情况(即,如果 bar 仅在 foo 内部使用,并且未存储或返回),则可以在堆栈中而不是在堆中分配单元格,从而降低运行时“consing”的数量(分配对象在需要回收垃圾收集的堆上)。

这种区别在文献中被称为“向下/向上 funarg”;即,如果捕获的变量仅在较低级别(即在闭包中或在闭包内创建的更深的闭包中)或在较高级别(即如果我的 caller 将能够访问我捕获的本地变量)可见)。

为了解决向上的 funarg 问题,需要一个垃圾收集器,这就是 C++ 闭包不提供这种能力的原因。

【讨论】:

  • 精彩的展览!
  • 这回答了一个更大的问题,我的只是其中的一小部分!假设我错过了一块拼图(提到向下向上的 funarg 特别有帮助)。
  • 使用dis 是一个有启发性的好处——我很高兴我用 Python 编写了我的示例代码的另一个原因。
【解决方案2】:

在 Python 中,如果变量曾经被赋值(出现在赋值的 LHS 上)并且没有显式声明为全局或非局部变量,则该变量被确定为局部变量。

因此,可以对 lexical 范围链进行处理,以静态确定哪个标识符将在哪个函数中找到。但是,仍然需要做一些动态的工作,因为你可以任意嵌套函数,所以如果函数 A 包含函数 B,函数 B 又包含函数 C,那么函数 C 要访问函数 A 中的变量,你必须找到正确的框架A.(闭包也是一样。)

【讨论】:

    【解决方案3】:

    这是一个已解决的问题......无论哪种方式。 Python 使用纯词法作用域,并且闭包是静态确定的。其他语言允许动态作用域——并且闭包是在运行时确定的,在运行时调用堆栈而不是解析堆栈中向上搜索。

    这样解释够吗?

    【讨论】:

    • 在运行时确定?
    • 哎呀。我更改了措辞,但没有完成编辑。现已修复。
    猜你喜欢
    • 2013-11-03
    • 2016-09-23
    • 1970-01-01
    • 2014-04-19
    • 1970-01-01
    • 2012-05-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多