变量的作用域

179 阅读6分钟

起因

最近闲来无事, 在 Python 官网上看到了2.0版本, 是2001年的.

image-20210116142522829

打算装起来体验一下最初发布的版本, 但是发现只有 Windows 版本, 所以我就装了个 Windows10的虚拟机, 就在我打算安装的时候, 发现:

image-20210116142802711

这激起了我的好胜欲, 于是我就依次安装了Windows 8, Windows 7, Windows XP, 功夫不负有心人, 终于在XP系统上装上了. (现在的很多网站, 在XP系统的 ie 上已经打不开了. )

想必大家没怎么见过Python 2.0的安装过程 吧, 在此截图留念:

12346image-20210116143753009image-20210116143925706

中间各种试用, 在此按下不表, 来看一下出问题的代码:

x = 2

def re_f():
    x = 3
    def tmp_f():
        print x
    tmp_f()

re_f()

这段代码的输出结果是什么? 按常理来说, 应该是3, 没错吧. 但是, 你看:

image-20210116144427156

??? 什么鬼? 为什么读到了全局变量? 我还特地有到Python 3.0的环境中跑了一遍, 发现结果确实是3啊. 不懂就要问, 于是我开始搜寻各种资料, 发现这设计到了变量的作用域.

回顾历史

要想理解这个现象, 就得把时间线往回拉, 拉到什么时候呢? 就从汇编说起.

在早期的汇编中, 对一个变量定义后, 就作为全局变量作用于整个程序. 在编译之后, 将所有该变量名替换为正确的地址, 相当于维护了一个变量名到地址的映射表.

当然, 这并没有什么问题, 但是随着时间推移, 程序的规模越来越大, 问题就出现了.

你定义了一个变量 x=2, 调用了一个系统函数之后, 回来发现x变成9了. 因为系统函数中也存在变量x, 这很明显会引发各种各样的问题, 开发难度大幅度提升.

如何解决这个问题呢? 出现问题的根源就是, 定义的变量都是全局变量, 每个修改其变量的人, 都会影响所有使用者. 接下来有了各种解决办法:

长变量名

既然出问题的原因是使用了同名变量, 那我让所有变量的名字都不一样就可以嘛.

在函数sort中的所有变量, 都加上_sort后缀, 比如变量i, 就定义为i_sort, 但无法避免另外一个sort函数, 那就在后缀再拼上一个文件名? 但如果文件名也一样呢? 毕竟很多时候, 你需要调用各种现有的库, 你无法保证没有冲突.

很显然, 这并不能解决本质问题.

变量回写

既然同名这个方向走不通了, 那就往全局方向使劲吧. 如果能让变量只在当前函数起作用, 而不会被其他人随意修改, 不就能够解决这个问题了么?

说起来容易, 如何实现呢? 如果说, 我在函数退出的时候, 把变量再改回我进来时候的样子, 不就能假装什么都没有发生吗? 比如这样:

function test(){
  // 这里用到了变量 i, 那就先把原来的值记下来
  $old_i = $i;
  // 然后就可以随意对变量 i 修改了
  // 返回时将变量改回去
  $i = $old_i;
  return;
}

但是, 这种处理方法有如下问题 :

问题1: 若old_i变量也是个全局变量怎么办

对于这个问题还是很好处理的, 编译器是有全局变量的对照表的, 随便找一个不存在的变量还是很容易的, 这个赋值的操作直接交给编译器来处理就好.

问题2: 上层函数的修改会影响下层函数

举个简单的例子:

$i = 1;

function fun_1(){
  $old_i = $i;
  $i = 2;
  fun_2();
  $i = $old_i;
  return;
}

这里有一个全局变量i, 在fun_2中读到的变量i值是多少? 是2. 函数fun_1本无意修改i的值, 但其修改还是影响了所有下层函数. 当然, 有时确实需要读取上层函数的修改, 但是, 也有很多情况是要读到其原始值的.

动态作用域

无法读取到全局变量的原因, 是变量的值在上层函数中已经被修改了, 其原本的值已经不存在了. 如何实现真正的局部变量, 保证不会对全局变量造成污染呢? 很简单, 只要函数的变量与全局变量, 实际指向的地址不同就可以了. 如何实现呢?

函数使用一张自己的变量名对照表, 就可以了. 大概就长这样:

image-20210116160120405

这样, 函数使用的变量就是真正的局部变量了. 当函数fun_1退出的时候, 会将对应的对照表销毁.

这个时候, 函数fun_2读取变量$i的时候, 会按照对照表的创建顺序, 在fun_2变量对照表, fun_1变量对照表, 全局变量对照表 依次查找, 看哪一个先找到.

哎, 这不就是闭包么. 动态作用域读取变量的结果, 其实与上方的回写变量的方式差不多, 不同的是, 动态作用域保留了全局变量原始的值. 既然原始值留下来了, 那自然就要能够读到, 否则留他何用, 读取的方式就是下面的静态作用域了.

静态作用域

静态作用域也是通过变量的对照表来实现, 与动态作用域不同的是, 每个函数能看到的变量对照表只有自己的和全局的, 上面的函数调用, 换成静态作用域大概如下:

image-20210116160536236

这样就能让函数绕过上层, 直接访问全局变量了.

现象

了解了变量作用域相关内容, 也就能够解释最开始遇到的现象了.

再来回看一下最开始的问题, 为什么在Python 2.0中, 闭包读取到的变量是全局变量呢? 很明显, 其使用了静态作用域导致的. 那么在2.0中如何解决这个问题呢? 传参, 修改之后的代码:

x = 2

def re_f():
    x = 3
    def tmp_f(x):
        print x
    tmp_f(x)

re_f()

再次执行, 结果与预期一致, 是3

而到了Python 2.1.3就已经改为动态作用域了. (也不知道为什么2.1比2.2还要晚一年发布)

在函数中如果想修改外部变量, 需要对变量进行声明, 若不声明则创建本地变量. 在 Python 中有两个关键字对变量进行声明:

  • global: 声明全局变量, 既通过静态作用域的方式查找变量
  • nolocal: 通过动态作用域的方式查找变量

当然, Python中通过上面关键字标识的变量修改, 会直接修改外层变量的值, 个人还是推荐以返回值的形式处理.

我是真的闲, 为了装Python2.0我就搞了半天, 查作用域又查了三四个小时.