变量的作用域
起因
最近闲来无事, 在 Python 官网上看到了2.0版本, 是2001年的.
打算装起来体验一下最初发布的版本, 但是发现只有 Windows 版本, 所以我就装了个 Windows10的虚拟机, 就在我打算安装的时候, 发现:
这激起了我的好胜欲, 于是我就依次安装了Windows 8
, Windows 7
, Windows XP
, 功夫不负有心人, 终于在XP
系统上装上了. (现在的很多网站, 在XP
系统的 ie 上已经打不开了. )
想必大家没怎么见过Python 2.0
的安装过程 吧, 在此截图留念:
中间各种试用, 在此按下不表, 来看一下出问题的代码:
x = 2
def re_f():
x = 3
def tmp_f():
print x
tmp_f()
re_f()
这段代码的输出结果是什么? 按常理来说, 应该是3
, 没错吧. 但是, 你看:
??? 什么鬼? 为什么读到了全局变量? 我还特地有到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
的值, 但其修改还是影响了所有下层函数. 当然, 有时确实需要读取上层函数的修改, 但是, 也有很多情况是要读到其原始值的.
动态作用域
无法读取到全局变量的原因, 是变量的值在上层函数中已经被修改了, 其原本的值已经不存在了. 如何实现真正的局部变量, 保证不会对全局变量造成污染呢? 很简单, 只要函数的变量与全局变量, 实际指向的地址不同就可以了. 如何实现呢?
函数使用一张自己的变量名对照表, 就可以了. 大概就长这样:
这样, 函数使用的变量就是真正的局部变量了. 当函数fun_1
退出的时候, 会将对应的对照表销毁.
这个时候, 函数fun_2
读取变量$i
的时候, 会按照对照表的创建顺序, 在fun_2变量对照表
, fun_1变量对照表
, 全局变量对照表
依次查找, 看哪一个先找到.
哎, 这不就是闭包么. 动态作用域读取变量的结果, 其实与上方的回写变量的方式差不多, 不同的是, 动态作用域保留了全局变量原始的值. 既然原始值留下来了, 那自然就要能够读到, 否则留他何用, 读取的方式就是下面的静态作用域了.
静态作用域
静态作用域也是通过变量的对照表来实现, 与动态作用域不同的是, 每个函数能看到的变量对照表只有自己的和全局的, 上面的函数调用, 换成静态作用域大概如下:
这样就能让函数绕过上层, 直接访问全局变量了.
现象
了解了变量作用域相关内容, 也就能够解释最开始遇到的现象了.
再来回看一下最开始的问题, 为什么在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
我就搞了半天, 查作用域又查了三四个小时.