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

gna*_*oot 5 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')
Run Code Online (Sandbox Code Playgroud)

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

但作为描述在这里,当一个函数被调用时,它的本地帧扩展其父环境.因此,在运行时进行环境查找是没有问题的.但我不确定的是,静态分析器总能确定在编译时,在代码运行之前引用哪个闭包.在上面的例子中,很明显elvis有两个闭包,很容易跟踪它们,但其他情况不会那么简单.直觉上,我很担心静态分析的尝试一般会遇到停顿问题.

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

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

[编辑@评论:

就我的例子而言,我可以重申一下我的问题:我读过诸如"可以在编译时确定词法解析"这样的说法,但是想知道如何在编译时(通常)静态地计算ain f1和in 的值f2.

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

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

650*_*502 7

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

def foo(x):
    y = 1
    z = 2
    def bar(a):
        return (x, y, a)
    return bar
Run Code Online (Sandbox Code Playgroud)

env捕获解决方案如下:

  1. foo输入和本地框架是建立一个包含x,y,z,bar的名称.名称x绑定到参数,名称yz1和2,bar闭包的名称
  2. 分配给封闭bar居然抓住了整个父框架,所以当它被称为它可以查找名称a在自己的本地帧,可以查找xy而不是在拍摄父框架.

使用这种方法(这不是 Python使用的方法)z只要闭包保持活动状态,变量就会保持活动状态,即使它没有被闭包引用.

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

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

这需要在创建闭包时花费一些额外的时间,因为每个捕获的单元格需要在闭包对象内复制(而不是仅仅复制指向父框架的指针),但是具有不捕获整个框架的优点.例 z后不会保持活着foo的回报,只有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
>>>
Run Code Online (Sandbox Code Playgroud)

你可以看到生成的代码存储1y与一个STORE_DEREF(即写入到单元中的操作,从而使用双间接)和代替存储2z使用STORE_FAST(z未捕获并仅仅是在当前帧中的局部).当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
Run Code Online (Sandbox Code Playgroud)

你可以看到返回的闭包内部,xy使用LOAD_DEREF.无论嵌套函数层次结构中有多少级别"up"定义了一个变量,它实际上只是一个双向间接,因为在构建闭包时支付了价格.对于本地人来说,封闭变量的访问速度稍慢(通过常数因子)......在运行时不需要遍历"范围链".

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

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

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