欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  后端开发

Python和Lua的默认作用域以及闭包

程序员文章站 2022-04-08 20:12:51
...
默认作用域

前段时间学了下Lua,发现Lua的默认作用域和Python是相反的。Lua定义变量时默认变量的作用域是全局(global,这样说不是很准确,Lua在执行x = 1这样的语句时会从当前环境开始一层层往上查找x,只有在找不到x的情况下才定义全局变量)的,而Python定义变量时默认变量的作用域是局部(local)的(当前块)。另外,Lua可以再定义变量时在变量前加上local关键字来定义局部变量,而Python没有类似的关键字,Python的变量只能定义在当前块中。

我们知道,全局变量是不好的,而局部变量是好的,写程序应该尽量使用局部变量。所以一开始时我觉得Python的这种约定比较好,它的优点就是可以少打些字。写Lua程序时不断在心底默念“勿忘local,勿忘local”,然而还是有时会出现几个漏网之鱼并引发了一些神奇的bug。

闭包

第一次意识到Python默认作用域的问题是在使用闭包时碰到的。关于闭包,Lua教程上有一段代码:

function new_counter()
  local n = 0
  local function counter()
    n = n + 1
    return n
  end
  return counter
end
   
c1 = new_counter()
c2 = new_counter()
print(c1())  -- 打印1
print(c2())  -- 打印1
print(c1())  -- 打印2
print(c2())  -- 打印2

闭包的本质可以参考SICP第三章的环境模型。在这里可以简单的想象为函数counter有一个私有成员n。

现在问题来了:我想用Python实现同样功能的闭包?

首先直接从Lua代码依葫芦画瓢改写成Python代码:

def new_counter():
  n = 0
  def counter():
    n = n + 1
    return n
  return counter

然后傻眼:这个程序不能运行,第4行访问了未赋值的变量n。出错的原因并非是Python不支持闭包,而是Python的赋值操作访问不了上一层的变量n(实际上,Python认为这是定义局部变量,而非赋值。在Python中定义局部变量与赋值操作在语法上是冲突的,Python干脆只支持可重定义的定义语句)。由于Python默认作用域是局部的,所以当程序运行到n = n + 1时,Python认为这是一个变量定义操作,于是创建了一个(未初始化的)局部变量n——并且顺利地覆盖了new_counter这一层的n——然后试图把n + 1赋值给n,但是n未初始化,n + 1没法计算,所以程序报错。


可以用个小技巧来实现闭包赋值的功能:

def new_counter():
  n = [0]
  def counter():
    n[0] = n[0] + 1
    return n[0]
  return counter

这里n[0] = n[0] + 1不会出错的原因是这里的等号和前面n = n + 1的等号含义不一样。n[0] = n[0] + 1中的等号意思是修改n的某个属性。事实上这个等号最终调用了list的__setitem__方法。而n = n + 1中的等号意思是在当前环境将n + 1这个值绑定到符号n中(如果当前环境已存在符号n,就覆盖它)。

另外题外话:打死我不用这种写法,多难看呐。反正Python是面向对象语言,要实现个计数器,大不了写个类。

定义与赋值的分离

先总结一下Python与Lua的默认作用域的特点:

1、 Lua默认作用域是全局的,写程序时要牢记local关键字(除非确实要定义全局变量),不小心忘了local也不会提示,就等着纠bug吧。

2、 Python默认作用域是局部的,虽然写程序的思维负担少些,但是丧失了对上层变量赋值的能力(可以改,但会让语言更混乱)。

看来两种默认作用域都有问题?个人认为,出现以上问题的原因是:Python和Lua没有实现定义和赋值的分离。在Python和Lua中,像x = 1这样的语句既可以表示定义,也可以表示赋值。其实不只是这两种语言,其实很多高级语言都没有实现定义和赋值的分离。定义和赋值两者功能上很像,但是它们本质上是有差别的。

下面以x = 1为例解释定义与赋值:

定义的意思是:在当前环境中注册符号x,并初始化为1。如果x已经存在,则报错(不允许重定义)或者覆盖(允许重定义)。

赋值的意思是:从当前环境开始,一层层往上找直到第一次找到符号x,把它的值修改成1。如果找不到就报错(变量不存在)。

现在我们稍微修改一下Python来实现定义和赋值的分离:用“:=”表示定义,用“=”表示赋值。然后重写那个不能运行的new_counter例子(Python中赋值操作和定义局部变量冲突,换句话说,Python其实没有赋值操作,所以我们只需简单的把“=”全换成“:=”就行了),看看它错在哪:

def new_counter():
  n := 0
  def counter():
    n := n + 1
    return n
  return counter

这个程序为什么是错的就很明显了。第4行我们要的是赋值操作,而非定义操作。修改成正确的写法:

def new_counter():
  n := 0
  def counter():
    n = n + 1
    return n
  return counter

这样就能正确运行了(前提是有修改版的Python解释器XD)。

最后说一些Lua的情况。Lua感觉上就把定义和赋值的分离实现了一半。带有local关键字的等号语句肯定是定义了。问题是不带local的等号语句。对于这种语句Lua是这样做的:先试图做赋值,如果赋值失败(变量不存在),就在最外层环境(全局环境)定义变量。也就是说,不带local的等号语句把定义和赋值混在一起了。另外,如果实现了定义和赋值的分离,就不需要考虑默认作用域的问题了——定义全部是在当前环境下定义,都是定义局部变量。我实在想不出在一个函数体或者什么块中定义全局变量的好处。

相关标签: Python Lua