7.3 静态作用域是完美的吗
时至今日,很多语言都选择了使用静态作用域。那么现在的静态作用域已经没有改善空间,很完美了吗?不是的。还有一些尚待解决的问题。
专栏
其他语言中的作用域
本章中的示例代码主要是用 Perl 语言写的。这是因为,Perl 语言具有对说明问题非常有利的特征,没有任何声明时使用的变量为全局变量,带 local 的变量为动态作用域(从 1991 年的 Perl 4 开始),带 my 的变量为静态作用域(从 1991 年的 Perl 5 开始)。现在执行禁止松弛代码的 use strict 模式时,用 local 声明的动态作用域变量和没有包的全局变量都会导致编译错误。
其他语言又是如何呢? 1958 年问世的早期 LISP 语言是动态作用域。1975 年问世的作为 LISP 语言的一种的 Scheme 语言采用了静态作用域。1994 年问世的 JavaScript 语言和 Perl 语言一样,把没有任何声明的变量视为全局作用域,把用 var 声明的变量视为静态作用域。而 1991 年问世的 Python 语言和 1995 年问世的 Ruby 中,即使不带任何修饰的变量也被视为静态作用域。
在以后的程序设计中,我们应尽量避免使用全局对照表这种大家共用的空间,而去使用那些能把变更的影响范围减小的方式,写出便于理解的代码。
我们以从一开始就采用静态作用域的 Python 语言为例来说明。在 2000 年发布的 Python 2.0 中,对照表(作用域)有三个层次。范围从大到小,依次为内置的、全局的、局部的(图 7.7)。简单来讲,就是每一个程序都有一张整体对照表(内置对照表),一张文件级别的对照表(全局对照表),一张函数级别的对照表(局部对照表)。

图 7.7 Python 语言的作用域的三个层次:局部的、全局的、内置的
Python 语言的内置作用域是程序的任何地方都能参照到的对照表,比如字符串函数 str 和运行时错误 RuntimeError 这样的。因为带有包装好的函数和异常,所以被称为内置 9。Python 语言的全局作用域是针对每个文件的对照表。有些语言也称其为文件作用域。局部作用域则是针对每个函数的对照表。
9有些语言也可能将其称为全局的。如我们之前反复强调的一个要点,关键字用语的使用是因程序而异的。
Python 语言是赋值即变量定义的语言,它没有与 JavaSript 语言的 var x 和 Perl 语言的 my $x 相当的变量声明。执行函数中的赋值,没有任何声明语句就定义了局部变量。也就是说,想要在函数中为某数值取名字,只需把数值赋值给你喜欢的名字的变量。这样做不会对函数以外的部分产生任何影响。
乍一看,这似乎是百利无害的大好事。然而人们设计的东西不管看起来是多么地正确,往往还是会有错误的地方。现在看来,这种机制至少有两个问题。
嵌套函数的问题
第一个问题就是看似嵌套结构体的作用域其实并非是嵌套结构体。Python 语言支持把函数作为嵌套结构体即函数嵌套,函数中允许定义新的函数。下面的代码中,函数 foo 中定义了函数 bar,来显示 x 的值。
Python
x = "global"
def foo():
x = "foo"
def bar():
print x ❶
bar()
foo()
这句 print 语句要输出的值很直观吗?认为输出 foo 的人应该不在少数吧。然而,到 Python 2.0 情况都不是这样。这句代码输出为 global(图 7.8)。

图 7.8 容易误认为是使用代码上相近的定义
这是什么情况?从源代码表面上看,因为函数 foo 中包含函数 bar,所以很多人会以为函数 foo 的作用域中也包含函数 bar 的作用域。这就是说,bar 的作用域中找不到名字 x 时,参照相邻外部的 foo 的作用域,抱有这种想法的人很多(图 7.9 ❶)。
实际上 Python 2.0 的设计并不是这样的。函数 bar 的局部作用域中找不到名字 x 时,接下来去找的是全局作用域(图 7.9 ❷)。
这种程序设计带来了很多误解,并且有时会招致一些奇怪的解决偏方 10。因此,认识到这是一个需要修正的问题后,2011 年发布的 Python 2.1 已经把设计修改为❶指示的逻辑。
10利用函数参数的默认值在函数定义时确定的特点,写成 bar(x=x) 的形式把 foo 的作用域中的 x 值带入 bar 的作用域。说的是这种解决方法。

图 7.9 嵌套结构体作用域的解释:想像(❶)与 Python 2.0 中的现实(❷)
外部作用域的再绑定问题
第二个问题是无法变更嵌套作用域外部的变量。这个问题起因是 Python 语言赋值即变量定义的特点。函数中执行变量赋值语句时,这个变量就成为该函数的局部变量。赋值带来的操作是,当这个作用域中有这个名字的变量时,对该名字的变量进行再次绑定 11,当这个作用域中没有这个名字的变量时,定义一个新的局部变量。不管哪种情况,这对于外部的作用域来说都没有影响。也就是说,无法变更此作用域外部的变量。
11绑定,简单来讲就是将名字和值关联在一起。再绑定也就是对变量 x 关联另外的值。
Python
def foo():
x = "old"
def bar():
x = "new"
# 打算修改外面的x的值
# 却创建了一个新的本地变量
bar()
print x
foo() #-> old(没有能修改)
Python 语言中的解决方法
对于如何解决这一问题,曾经有过非常激烈的讨论。大家提出的方案中,有像 JavaScript 语言那样在变量定义的作用域中用 var 来做声明的方法。这一方法确实能解决这个问题,但是相应的失去了与过去的代码的互换性。所以,这种方法是不可取的。
直到 2006 年 Python 3.0 中才出现了一种方法,即在函数开始时声明变量为 nonlocal 性质。
nonlocal 这个关键字的选取也是在众多备选项中挑选出来的,因为它出现的频度在过去代码中是最低的12。
12“PEP 3104 — Access to Names in Outer Scopes”,http://www.python.org/dev/peps/pep-3104/。
Python 3.0
def foo():
x = "old"
def bar():
nonlocal x # 非本地变量声明
x = "new" # 修改外部作用域
bar()
print(x)
foo() #-> new(变量修改了)
Ruby 语言中的解决方法
Ruby 语言也是一种不需要对变量进行声明的语言,所以它面临与 Python 同样的问题。
Ruby 语言中像函数这样发挥作用的有两种,方法和代码段(Block)。在 Ruby 1.9 版本中,方法在进行嵌套时作用域是不嵌套的(❶)。
Ruby
def foo()
x = "outside"
def bar() # 方法嵌套
p x #-> 出错,无法访问外部的x(❶)
end
bar()
end
foo()
另外,当方法中含有代码段时,方法的局部作用域中有的名字在代码段中被视为方法的局部变量,除此以外的被视为代码段的局部变量 13。这也就是说,在代码段中对变量赋值时可能发生一种情况,即原本想定义一个局部变量,却因为与外部作用域中的名字重复,无意中造成了变量值的变更。在这方面需要特别注意。检查是否使用了相同名字的变量在这里并不怎么困难,因为需要检查到的范围只有一个方法。
13像在 foo{|x, y; z| …} 中这样,通过前置的分号来强制 z 成为局部变量。
Ruby
def foo()
x = "old" # foo方法的作用域中有变量名x
lambda {x = "new"; y = "new"}.call # 在方法中创建代码段
# ↑x是方法foo的、y是方法lambda的本地变量
p x #-> 修改成为new
p y #-> 出错,y是lambda的本地变量此处无法访问
end
foo # 调用foo
