python不区分声明和赋值是否是一个正确的选择


python或是曾经的js中可能写出一段这样似乎有点诡异的代码

funcs = []
for i in range(10):
    funcs.append(lambda: print(i))
for func in funcs:
    func()

这段代码执行之后打印出来的东西是什么呢?看起来是似乎应该是

0
1
2
3
...

当然如果曾经真的写出过这种代码的话,就会知道结果实际上是

9
9
9
...

有些人声称这是由于lambda的行为是惰性的,所以lambdah函数被调用的时候,i已经变成9了,似乎很有道理,但预期输出

0
1
2
3
...

的理由又是什么?一个函数内部绑定的取决与这个函数产生时,这个函数外部的变量的值是多少吗?换个说法,上面那段python程序如果简单地利用javascript改写成

funcs = []
for(index of Array(10).keys()){
    const i = index
    funcs.push(()=>console.log(i))
}
for(func of funcs){
    console.log(func)
}

的话,输出变成了

0
1
2
3
...

其他很多编程语言的行为也类似这段js。有趣的是如果把const改成var之后javascript的行为又会变得和python一样,难道javascript的lambda就不是惰性的吗?

其实“在函数产生时,函数内部会绑定外部环境的名称和值”,这种预期在主流编程语言中一开始就是不存在的。所以一开始的预期的理由就是不对的。

那么问题来了,是什么造成了python和javascript,以及其他(大部分的)主流编程语言的不同?前面已经某种程度说明,这个一个函数对其外部的“环境”的绑定的行为不同,那什么又是环境?可能很多人对编程语言的函数访问外部变量只有一个模糊的认识,如果函数没法访问它外部的变量,从“视觉”上看也是很不合理的。对现代大多数编程语言来说,这个所谓的“环境”就是一个作用域。一个作用域中有一些绑定了名字的值,而产生一个新函数时,这个函数就将自己的外层作用域的所有名称绑定到自身。当这个函数内部使用一个名字时,就可能会到外部的作用域查找。具有这种行为的性质一般被称为“静态作用域”,或者“词法作用域”。关于动态作用域又是另一件事了,这里暂且继续说明为什么循环中python和javascript的行为不同。

既然已经知道函数绑定的是外部的“名字”(变量名)而不是值,照此推测那段python程序,的确,i这个值绑定的名字的值已经变了,如此看来,python的行为岂不才是正常的?js难道不遵循这个规则吗?答案其实已经很明显了,js中的每次循环,产生一个新函数时,i已经不是同一个函数中的i了,所以虽然js中,那些i都叫i,但已经不是同一个i,绑定的值也更加不同了。

那么怎么让js表现python这样的行为?很简单,把i的声明提升到循环之外就可以了。

似乎js在这一点上比python要强不少,毕竟它更符合直觉,并且还能轻易地模仿python,但python就没办法优雅的模仿js了。

如果想到这里,似乎对这个问题已经了解地比较彻底了,但是,如果站在一个编程语言的设计或是创造者的角度看,为什么python不像js一样,每一次循环都产生一个新的作用域呢?如果对python比较了解的话,也会知道python除了函数定义外,所有的缩进语法几乎全都不会产生新的作用域,相对的大多数主流语言中的代码块,大括号,几乎都会产生新的作用域,又是什么造成了这种不同?

如果假设python所有的缩进都产生了新作用域,又是什么样子呢?从这个“思想实验”大概就能知道为什么python没有采取这种做法。如果python这么做的话,会发现

is_even = False
number = 2
if number % 2 == 0:
    is_even = True
print(is_even)

这段代码打印的将只能是False,因为if产生了新作用域,=这个中缀符号的行为是在=所处的作用域将值绑定到名字(变量)上,因此这种做法会让python甚至不能简单地在if或者while中改变外部变量的值,这当然是不可接受的。

那类似javascript为什么又不存在问题?稍微思考就会发现,上面那个例子中,之所以python没有改变外部的变量,是因为它认为这个被改变的量所处的作用域适合自己“同级”的。
如果非要这么做,补救的办法也是有的,那就是

is_even = False
number = 2
if number % 2 == 0:
    global is_even // notice 'global'
    is_even = True
printI(is_even)

显式声明变量属于外层,javascript为啥又不需要global呢?原因正是在于 - - 声明。python区分不了=是在声明还是在赋值。表面上看,声明和赋值确实是非常相似的,其行为都是将一个值绑定到一个变量名上。这么看声明符号似乎确实是可以省略的,但声明却还有另一个重要作用:标记一个变量所处的作用域在哪里。python不区分声明和赋值看起来将编程语言简化了一些,却导致了丑陋的globalnonlocal的出现,同时也迫于这一点不得不尽量减少作用域的产生,否则python代码中将充满nonlocalglobal

所以,python舍弃变量声明带来的好处是否多过缺点呢?如果仅仅是单文件的脚本的话,确实不容易遇到这种做法造成的缺点,但如果是一个超过几千行的工程,并且使用者喜欢函数式编程风格的话,缺点就被放大了。

一点题外话:变量声明的意义是什么

就我所见不少教别人类似java或者c的人喜欢说声明是在声明一个变量的类型,或者告诉我所谓静态类型语言之所以要静态是因为要先确定类型才能执行,真的吗?其实看过一点现代一些的编程语言就会发现,几乎所有新设计的静态类型编程语言都开始支持一种叫“类型推导”的特性,使用这种特性,在声明一种变量时,不需要写出它的类型。所以静态类型的产生并非是为了让代码通过编译。类型即约束,类型系统恰恰是为了让一些代码通不过编译器的检查。这样看来,标志一个变量的作用域才是声明最大的意义。


文章作者: Gregor
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Gregor !
 上一篇
为什么我觉得CSAPP是一本坏书 为什么我觉得CSAPP是一本坏书
我发现许多研究电脑程序的人说技术含量都一定会说“底层”,认为所谓的“底层”是电脑程序的技术含量所在。当然这些他们所说的“底层”,从源代码看到CPU如何执行这段程序,技术含量肯定比写几个CRUD高一些,但显然这并不是计算机科学的全部,甚至只是
2019-06-12
下一篇 
又一点感想 又一点感想
好久都没有水文了。其实并不是没东西好水,只是最近越来越觉得以前自己搞的东西都好幼稚。最近还在入门Haskell,准备先看了范畴论。其实真的想了解的话自己找教材就可以了,没必要看别人的二手货,我也没什么别的看法。至于《深入理解计算机系统》那本
2019-02-13
  目录