词法作用域有动态方面吗?
Does lexical scope have a dynamic aspect?
词法作用域的访问可以在编译时(或通过静态分析器,因为我的示例在 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所述,当一个函数被调用时,它的局部框架扩展了它的父环境。所以在 运行 时间进行环境查找是没有问题的。但我不确定的是,静态分析器总能在代码 运行 之前计算出 在编译时引用了哪个 闭包。在上面的例子中很明显 elvis
有两个闭包并且很容易跟踪它们,但其他情况就不会那么简单了。直觉上,我很担心静态分析的尝试通常会 运行 变成一个停止问题。
词法作用域是否真的具有动态方面,源代码中的位置告诉我们涉及封闭作用域但不一定涉及哪个闭包?或者这是编译器中已解决的问题,并且函数中对其闭包的所有引用真的可以静态地详细计算出来吗?
或者答案是否取决于编程语言——在这种情况下,词法范围并不像我想象的那么强大?
[编辑@评论:
就我的例子而言,我可以重申我的问题:我读过像 "Lexical resolution can be determined at compile time," 这样的声明,但想知道如何在 f1
和 f2
中引用 a
的值可以计算出 statically/at 编译时间(一般)。
解决办法是,词法作用域并没有要求那么多。 L.S。可以告诉我们,在编译时,只要我在 f
中,就会定义名为 a
的 something(这显然可以静态计算;这是词法作用域的定义),但确定它实际采用的值(或者,哪个闭包是活动的)是 1) 在 L.S 之外。概念,2) 在 运行 时间完成(不是静态的)所以在某种意义上是动态的,当然 3) 使用不同于动态范围的规则。
引述@PatrickMaupin 的外卖信息是"Some dynamic work still has to be done."]
这是一个已解决的问题......无论哪种方式。 Python 使用纯词法作用域,闭包是静态确定的。其他语言允许动态范围界定——闭包是在 运行 时间内确定的,在 运行 时间调用堆栈而不是解析堆栈中向上搜索。
这个解释是否充分?
在 Python 中,如果变量曾被赋值(出现在赋值的左轴)并且未明确声明为全局或非局部变量,则该变量被确定为局部变量。
因此可以沿着 词法 范围链来静态确定哪个标识符将在哪个函数中找到。但是,仍然需要做一些动态工作,因为您可以任意嵌套函数,所以如果函数 A 包含函数 B,函数 B 又包含函数 C,那么对于函数 C 访问函数 A 的变量,您必须找到正确的框架A.(闭包也是一样。)
闭包可以通过多种方式实现。其中之一是实际捕获环境……换句话说,考虑示例
def foo(x):
y = 1
z = 2
def bar(a):
return (x, y, a)
return bar
环境捕获解决方案如下:
输入 foo
并构建包含 x
、y
、z
、bar
名称的本地框架。名称 x
绑定到参数,名称 y
和 z
绑定到 1 和 2,名称 bar
绑定到闭包
- 分配给
bar
的闭包实际上捕获了整个父框架,因此当它被调用时,它可以在其自己的本地框架中查找名称 a
并且可以查找 x
和 y
而不是在捕获的父框架中。
使用这种方法(即 而不是 Python 使用的方法)只要闭包仍然有效,变量 z
就会保持有效,即使它没有被闭包引用。
另一个选项,实施起来稍微复杂一些,而不是像:
- 在编译时分析代码并发现分配给
bar
的闭包从当前范围捕获名称 x
和 y
。
- 这两个变量因此被分类为"cells"并且它们与局部帧分开分配
- 闭包存储这些变量的地址,每次访问它们都需要双重间接(单元格是指向实际存储值的位置的指针)
这需要在创建闭包时花费一些额外的时间,因为每个捕获的单元格都需要在闭包对象中复制(而不是仅仅复制指向父框架的指针),但具有不捕获的优点整个框架,例如 z
在 foo
returns 之后不会保持活动状态,只有 x
和 y
会。
这就是 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
(写入单元格的操作,因此使用双重间接寻址),而是存储 2
使用 STORE_FAST
转换为 z
(z
未被捕获,只是当前帧中的局部)。当 foo
的代码开始执行时 x
已经被调用机制包装到一个单元格中。
bar
只是一个局部变量,所以STORE_FAST
是用来写它的,但是构建闭包x
和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
是通过 LOAD_DEREF
访问的。无论在嵌套函数层次结构中定义了多少层 "up" 一个变量,它实际上只是一个双向间接访问,因为在构建闭包时已经付出了代价。就局部变量而言,封闭变量的访问速度(按常数因子)只是稍微慢一些……不需要在运行时遍历"scope chain"。
像 SBCL(用于生成本机代码的 Common Lisp 的优化编译器)这样更复杂的编译器也会执行 "escape analysis" 来检测闭包是否真的可以在封闭函数中存活下来。
当这没有发生时(即,如果 bar
仅在 foo
内部使用,而不是存储或返回),可以在堆栈而不是堆上分配单元格,从而减少运行时间 "consing"(在堆上分配需要垃圾回收才能回收的对象)。
这种区别在文献中称为"downward/upward funarg";即,如果捕获的变量仅在较低级别可见(即在闭包中或在闭包内部创建的更深的闭包中)或在较高级别(即如果我的 caller 将能够访问我俘虏的当地人)。
要解决向上函数参数问题,需要垃圾收集器,这就是为什么 C++ 闭包不提供此功能的原因。
词法作用域的访问可以在编译时(或通过静态分析器,因为我的示例在 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所述,当一个函数被调用时,它的局部框架扩展了它的父环境。所以在 运行 时间进行环境查找是没有问题的。但我不确定的是,静态分析器总能在代码 运行 之前计算出 在编译时引用了哪个 闭包。在上面的例子中很明显 elvis
有两个闭包并且很容易跟踪它们,但其他情况就不会那么简单了。直觉上,我很担心静态分析的尝试通常会 运行 变成一个停止问题。
词法作用域是否真的具有动态方面,源代码中的位置告诉我们涉及封闭作用域但不一定涉及哪个闭包?或者这是编译器中已解决的问题,并且函数中对其闭包的所有引用真的可以静态地详细计算出来吗?
或者答案是否取决于编程语言——在这种情况下,词法范围并不像我想象的那么强大?
[编辑@评论:
就我的例子而言,我可以重申我的问题:我读过像 "Lexical resolution can be determined at compile time," 这样的声明,但想知道如何在 f1
和 f2
中引用 a
的值可以计算出 statically/at 编译时间(一般)。
解决办法是,词法作用域并没有要求那么多。 L.S。可以告诉我们,在编译时,只要我在 f
中,就会定义名为 a
的 something(这显然可以静态计算;这是词法作用域的定义),但确定它实际采用的值(或者,哪个闭包是活动的)是 1) 在 L.S 之外。概念,2) 在 运行 时间完成(不是静态的)所以在某种意义上是动态的,当然 3) 使用不同于动态范围的规则。
引述@PatrickMaupin 的外卖信息是"Some dynamic work still has to be done."]
这是一个已解决的问题......无论哪种方式。 Python 使用纯词法作用域,闭包是静态确定的。其他语言允许动态范围界定——闭包是在 运行 时间内确定的,在 运行 时间调用堆栈而不是解析堆栈中向上搜索。
这个解释是否充分?
在 Python 中,如果变量曾被赋值(出现在赋值的左轴)并且未明确声明为全局或非局部变量,则该变量被确定为局部变量。
因此可以沿着 词法 范围链来静态确定哪个标识符将在哪个函数中找到。但是,仍然需要做一些动态工作,因为您可以任意嵌套函数,所以如果函数 A 包含函数 B,函数 B 又包含函数 C,那么对于函数 C 访问函数 A 的变量,您必须找到正确的框架A.(闭包也是一样。)
闭包可以通过多种方式实现。其中之一是实际捕获环境……换句话说,考虑示例
def foo(x):
y = 1
z = 2
def bar(a):
return (x, y, a)
return bar
环境捕获解决方案如下:
-
输入
foo
并构建包含x
、y
、z
、bar
名称的本地框架。名称x
绑定到参数,名称y
和z
绑定到 1 和 2,名称bar
绑定到闭包- 分配给
bar
的闭包实际上捕获了整个父框架,因此当它被调用时,它可以在其自己的本地框架中查找名称a
并且可以查找x
和y
而不是在捕获的父框架中。
使用这种方法(即 而不是 Python 使用的方法)只要闭包仍然有效,变量 z
就会保持有效,即使它没有被闭包引用。
另一个选项,实施起来稍微复杂一些,而不是像:
- 在编译时分析代码并发现分配给
bar
的闭包从当前范围捕获名称x
和y
。 - 这两个变量因此被分类为"cells"并且它们与局部帧分开分配
- 闭包存储这些变量的地址,每次访问它们都需要双重间接(单元格是指向实际存储值的位置的指针)
这需要在创建闭包时花费一些额外的时间,因为每个捕获的单元格都需要在闭包对象中复制(而不是仅仅复制指向父框架的指针),但具有不捕获的优点整个框架,例如 z
在 foo
returns 之后不会保持活动状态,只有 x
和 y
会。
这就是 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
(写入单元格的操作,因此使用双重间接寻址),而是存储 2
使用 STORE_FAST
转换为 z
(z
未被捕获,只是当前帧中的局部)。当 foo
的代码开始执行时 x
已经被调用机制包装到一个单元格中。
bar
只是一个局部变量,所以STORE_FAST
是用来写它的,但是构建闭包x
和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
是通过 LOAD_DEREF
访问的。无论在嵌套函数层次结构中定义了多少层 "up" 一个变量,它实际上只是一个双向间接访问,因为在构建闭包时已经付出了代价。就局部变量而言,封闭变量的访问速度(按常数因子)只是稍微慢一些……不需要在运行时遍历"scope chain"。
像 SBCL(用于生成本机代码的 Common Lisp 的优化编译器)这样更复杂的编译器也会执行 "escape analysis" 来检测闭包是否真的可以在封闭函数中存活下来。
当这没有发生时(即,如果 bar
仅在 foo
内部使用,而不是存储或返回),可以在堆栈而不是堆上分配单元格,从而减少运行时间 "consing"(在堆上分配需要垃圾回收才能回收的对象)。
这种区别在文献中称为"downward/upward funarg";即,如果捕获的变量仅在较低级别可见(即在闭包中或在闭包内部创建的更深的闭包中)或在较高级别(即如果我的 caller 将能够访问我俘虏的当地人)。
要解决向上函数参数问题,需要垃圾收集器,这就是为什么 C++ 闭包不提供此功能的原因。