词汇范围有动态方面吗?

2024-05-13 00:33:09 发布

您现在位置:Python中文网/ 问答频道 /正文

对词法范围的访问可以在编译时根据源代码中的位置在编译时(或者通过静态分析器,因为我的示例是用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中的a所指的。源代码中的位置足以告诉我f从一个封闭范围中为a获取一个值。在

但是正如所描述的here,当一个函数被调用时,它的局部框架会扩展其父环境。所以在运行时进行环境查找是没有问题的。但我不确定的是,静态分析器总是能够在代码运行之前,计算出编译时引用哪个闭包。在上面的例子中,elvis有两个闭包,很容易跟踪它们,但其他情况就不那么简单了。直觉上,我很紧张静态分析的尝试可能会遇到一个总体上停滞不前的问题。在

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

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

[编辑@评论:

在我的例子中,我可以重申我的问题:我读过诸如“词法解析可以在编译时确定”之类的声明,但是我想知道在f1f2中对a值的引用是如何静态/在编译时计算出来的(一般情况下)。在

解决办法是,词法范围界定并不要求这么多。五十、 S.可以告诉我们,在编译时,当我在f时,名为a的东西将被定义(这显然可以静态地计算出来;这是词法范围的定义),但是确定它实际需要什么值(或者,哪个闭包是活动的)超出了L.S.的概念,2) 在运行时完成(不是静态的),所以在某种意义上是动态的,但是当然3)使用了与动态范围不同的规则。在

引用@PatrickMaupin的话,我们要传达的信息是“还有一些动态的工作要做。”]


Tags: 函数分析器return定义环境源代码def静态
3条回答

在Python中,如果一个变量被赋值(出现在赋值的LHS上),并且没有显式地声明为global或nonlocal,那么它就被确定为局部变量。在

因此,可以建立词法范围链来静态地确定哪个标识符将在哪个函数中找到。但是,一些动态的工作还是要做的,因为你可以任意嵌套函数,所以如果函数A包含函数B,其中包含函数C,那么对于函数C来说,从函数A访问变量,你必须为A找到正确的框架(闭包也是一样)

这是一个已解决的问题。。。不管怎样。Python使用纯词法作用域,闭包是静态确定的。另一种方法是在调用栈运行期间,以动态方式搜索堆栈,而不是以动态方式运行堆栈。在

这个解释足够了吗?在

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

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

环境捕获解决方案如下:

  1. ^输入{},并生成包含xyzbar名称的局部框架。名称x绑定到参数,名称y和{}绑定到1和2,名称bar绑定到闭包
  2. 分配给bar的闭包实际上捕获了整个父帧,因此当调用它时,它可以在它自己的本地帧中查找名称a,并且可以在捕获的父帧中查找x和{}。在

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

另一个更复杂的实现方法是:

  1. 在编译时分析代码,并发现分配给bar的闭包从当前作用域捕获名称x和{}。在
  2. 因此,这两个变量被分类为“单元”,它们与局部帧分开分配
  3. 闭包存储这些变量的地址,每次访问它们都需要一个双间接寻址(单元格是指向实际存储值的位置的指针)

这需要在创建闭包时额外花费一点时间,因为每个捕获的单元都需要复制到闭包对象内部(而不是只复制指向父帧的指针),但它的优点是不捕获整个帧,因此例如,z在{}返回后将不保持活动状态,只有x并且y将。在

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

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

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

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

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

^{pr2}$

如您所见,生成的代码使用STORE_DEREF1存储到y(写入单元格的操作,因此使用双间接寻址),而使用STORE_FAST将{}存储到z中(z没有被捕获,只是当前帧中的一个局部)。当foo的代码开始执行时,x已经被调用机制包装到一个单元中。在

bar只是一个局部变量,因此使用STORE_FAST对其进行写入,但是要构建闭包x和{}需要分别复制(它们在调用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和{}中使用LOAD_DEREF进行访问。无论一个变量在嵌套函数层次结构中“向上”了多少层,它实际上只是一个双间接的距离,因为代价是在构建闭包时支付的。对于局部变量,闭合变量的访问(通过常数因子)只是稍微慢一点。。。运行时不需要遍历“范围链”。在

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

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

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

相关问题 更多 >