词汇范围是否具有动态方面?

时间:2015-09-22 21:51:31

标签: python programming-languages lexical-scope

对于词法范围的访问似乎很常见,可以在编译时(或通过静态分析器,因为我的示例是在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中未对其进行定义,因此我们会弹出到封闭函数并在那里找到一个,这就是af所指的内容。源代码中的位置足以告诉我f从封闭范围获取a的值。

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

词法作用域确实有一个动态的方面,源代码中的位置告诉我们涉及一个封闭的范围但不一定涉及哪个封闭?或者这是编译器中解决的问题,并且函数内的所有引用都可以通过静态详细解决?

或者答案是否取决于编程语言 - 在这种情况下,词法范围并不像我想象的那么强烈?

[编辑@comments:

就我的例子而言,我可以重申一下我的问题:我读过诸如“可以在编译时确定词汇解析”的说法,但想知道af1f2的价值是如何引用的{1}}可以在编译时(通常)静态地计算出来。

解决方案是,词汇范围没有那么多。 L.S.我可以告诉我们,在编译时,每当我在a时,都会定义名为f某事(这显然可以静态地解决;这是定义的词法范围),但确定它实际需要的(或哪个闭包是活动的)是1)超出LS概念,2)在运行时完成(非静态)因此在某种意义上是动态的,当然3)使用与动态范围不同的规则。

引用@PatrickMaupin的内容消息是“仍有一些动态的工作要做。” ]

3 个答案:

答案 0 :(得分:6)

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

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

env捕获解决方案如下:

    输入了
  1. foo,并构建了包含xyzbar名称的本地框架。名称x绑定到参数,名称yz为1和2,名称bar为关闭
  2. 分配给bar的闭包实际上捕获了整个父框架,因此当它被调用时,它可以在自己的本地框架中查找名称a,并且可以查找x和{{1}而是在捕获的父框架中。
  3. 使用这种方法(即不是 Python使用的方法),只要闭包保持活动状态,变量y就会保持活动状态,即使它没有被闭包引用。

    另一个选项,实现起来稍微复杂一些,比如:

    1. 在编译时分析代码并发现分配给z的闭包从当前范围捕获名称barx
    2. 因此,这两个变量被归类为“单元格”,它们与本地框架分开分配
    3. 闭包存储这些变量的地址,每次访问它们都需要双重间接(单元格是指向实际存储值的位置的指针)
    4. 这需要在创建闭包时花一点额外的时间,因为每个捕获的单元格都需要复制到闭包对象内部(而不是仅仅复制指向父框架的指针),但是具有不捕获整个框架的优点因此,例如yz返回后不会保持活动状态,只有foox才会生效。

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

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

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

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

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

      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 >>> 存储到1y(写入单元格的操作,因此使用双重间接),而是存储{{1}使用STORE_DEREF 2进入zSTORE_FAST未被捕获,只是当前帧中的本地)。当z的代码开始执行foo已被调用机制包装到单元格中时。

      x只是一个局部变量,因此bar用于写入它,但构建闭包STORE_FASTx需要单独复制(它们在调用y操作码之前将其置于元组中。

      闭包本身的代码可见:

      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 x内部使用y进行访问。无论嵌套函数层次结构中有多少级别“up”定义了一个变量,它实际上只是一个双向间接,因为在构建闭包时支付了价格。对于本地人来说,封闭变量的访问速度稍慢(通过常数因子)......在运行时不需要遍历“范围链”。

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

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

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

答案 1 :(得分:1)

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

这是否足够解释?

答案 2 :(得分:1)

在Python中,如果变量被分配给(在分配的LHS上出现)并且未明确声明为全局变量或非本地变量,则变量被确定为本地变量。

因此,可以通过词法范围链来静态地确定在哪个函数中找到哪个标识符。但是,仍然需要进行一些动态工作,因为你可以任意嵌套函数,所以如果函数A包含函数B,其中包含函数C,那么对于函数C来访问函数A中的变量,你必须找到正确的框架。答(关闭同样的事情。)