Python 词法范围有动态方面吗?

Python 词法范围有动态方面吗?,python,programming-languages,lexical-scope,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') 我不反对这样的想法,当我们阅读

对词法作用域的访问可以在编译时(或者通过静态分析器,因为我的示例是用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
的值

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

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

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

[编辑@评论:

就我的例子而言,我可以重申我的问题:我读过“词法解析可以在编译时确定”这样的说法,但我想知道如何静态地/在编译时(通常)计算
f1
f2
中对
a
值的引用

解决方案是,词法作用域并不要求太多。L.S.可以在编译时告诉我们,每当我在
f
中时,就会定义一个名为
a
的东西(这显然可以静态计算;这是词法作用域的定义),但要确定它实际需要什么值(或者,哪个闭包是活动的)1)超出了L.S.的概念,2)在运行时完成(不是静态的),所以在某种意义上是动态的,当然3)使用了不同于动态范围的规则


引用@PatrickMaupin的话,这句话的大意是“一些动态的工作仍有待完成。”]

这是一个已解决的问题。。。不管怎样。Python使用纯粹的词法作用域,闭包是静态确定的。其他语言允许动态作用域——闭包是在运行时确定的,在运行时调用堆栈而不是解析堆栈中搜索闭包


这就足够了吗?

在Python中,如果变量被赋值(出现在赋值的LHS上),并且没有显式声明为全局或非局部,则该变量被确定为局部变量


因此,可以通过词法作用域链静态确定在哪个函数中找到哪个标识符。但是,仍然需要做一些动态工作,因为您可以任意嵌套函数,因此如果函数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
    名称的本地框架。名称
    x
    绑定到参数,名称
    y
    z
    绑定到1和2,名称
    bar
    绑定到闭包
  • 分配给
    bar
    的闭包实际上捕获了整个父帧,因此当调用它时,它可以在自己的本地帧中查找名称
    a
    ,并可以在捕获的父帧中查找
    x
    y
  • 使用这种方法(即而不是Python使用的方法),只要闭包保持活动状态,变量
    z
    就会保持活动状态,即使闭包没有引用它

    另一个选项的实现稍微复杂一些,例如:

  • 在编译时,将分析代码,并发现分配给
    bar
    的闭包从当前范围捕获名称
    x
    y
  • 因此,这两个变量被分类为“单元”,它们与本地帧分开分配
  • 闭包存储这些变量的地址,对它们的每次访问都需要一个双间接寻址(单元格是指向值实际存储位置的指针)
  • 这需要在创建闭包时支付一点额外的时间,因为每个捕获的单元格都需要在闭包对象内部复制(而不仅仅是复制指向父帧的指针),但其优点是不捕获整个帧,因此例如
    z
    foo
    返回后将不会保持活动状态,只有
    x
    y
    将被删除

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

    一个小麻烦是,当捕捉到一个参数时(如
    foo
    示例中),还需要在p
    >>> 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
    >>>
    
    >>> 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