gpt4 book ai didi

python - 词法范围有动态方面吗?

转载 作者:太空狗 更新时间:2023-10-30 00:49:09 25 4
gpt4 key购买 nike

可以在编译时(或通过静态分析器,因为我的示例是在 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有两个闭包,很容易跟踪它们,但其他情况不会那么简单。直觉上,我很担心静态分析的尝试可能会遇到一般的停机问题。

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

或者答案是否取决于编程语言——在这种情况下,词法范围并不像我想象的那样是一个强大的概念?

[编辑@评论:

就我的示例而言,我可以重申我的问题:我读过诸如“可以在编译时确定词法解析”之类的声明,但想知道如何引用 a 的值。在 f1f2可以静态地/在编译时(通常)计算出来。

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

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

最佳答案

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

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

环境捕获解决方案如下:
  • foo输入并构建包含 x 的本地框架, y , z , bar名称。姓名 x绑定(bind)到参数,名称 yz到 1 和 2,名称 bar关闭
  • 分配给 bar 的闭包实际上捕获了整个父框架,因此当它被调用时,它可以查找名称 a在它自己的本地框架中,可以查找 xy而是在捕获的父帧中。

  • 使用这种方法(即 而不是 Python 使用的方法)变量 z只要闭包还活着,即使闭包没有引用它,它也会一直活着。

    另一种选择,实现起来稍微复杂一些,而不是像:
  • 在编译时分析代码并将闭包分配给 bar发现捕获名称 xy从当前范围。
  • 因此,这两个变量被归类为“单元格”,它们与本地框架
  • 分开分配。
  • 闭包存储这些变量的地址,每次访问它们都需要双重间接(单元格是指向实际存储值的位置的指针)

  • 这需要在创建闭包时多花一点时间,因为每个捕获的单元格都需要复制到闭包对象内部(而不是仅仅复制指向父帧的指针),但具有不捕获整个帧的优点,因此对于示例 zfoo 之后将不再存活返回,仅 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
    >>>

    如您所见,生成的代码存储 1进入 ySTORE_DEREF (写入单元格的操作,因此使用双重间接寻址)并存储 2进入 z使用 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

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

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

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

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

    关于python - 词法范围有动态方面吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32727556/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com