Python和Lua的默认作用域以及闭包
默认作用域
前段时间学了下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的默认作用域以及闭包的更多相关文章
- Python基础:11变量作用域和闭包
一:变量作用域 变量可以是局部域或者全局域.定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域. 全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数, ...
- Python tricks(2) -- method默认参数和闭包closure
Python的method可以设置默认参数, 默认参数如果是可变的类型, 比如list, map等, 将会影响所有的该方法调用. 下面是一个简单的例子 def f(a=None, l=[]): if ...
- python函数名称空间与作用域、闭包
一.命名空间概念 1.命名空间(name space) 名称空间是存放名字的地方. 若变量x=1,1存放在内存中,命名空间是存放名字x.x与1绑定关系的地方. 2.名称空间加载顺序 python te ...
- Python 内置函数、作用域、闭包、递归
一.内置函数如何使用 help()一下: 如想看min()咋用?在shell中:help(min) 二.部分内置函数 (一).排序:sorted() li = [(1, 2, 3, 4), (7, 8 ...
- Python内置函数、作用域、闭包、递归
1.几个可能用到的内置函数 2.函数内变量的作用域 3.内嵌函数和闭包 4.递归函数 1.常见的内置函数 常见的内置函数: 查看内置函数: print(dir(__builtins ...
- python 变量作用域、闭包
先看一个问题: 下面代码输出的结果是0,换句话说,这个fucn2虽然已经用global声明了variable1,但还是没有改变变量的值 def func1(): variable1=0 def fun ...
- Python入门笔记(22):Python函数(5):变量作用域与闭包
一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除,否则他们存活到脚本运行结束,且对于所有的函数都可访问. 当搜索一个标识符(也称变量.名字等),Pyt ...
- python中对变量的作用域LEGB、闭包、装饰器基本理解
一.作用域 在Python程序中创建.改变.查找变量名时,都是在一个保存变量名的空间中进行,我们称之为命名空间,也被称之为作用域.python的作用域是静态的,在源代码中变量名被赋值的位置决定了该变量 ...
- Python学习(21):Python函数(5):变量作用域与闭包
转自 http://www.cnblogs.com/BeginMan/p/3179040.html 一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除, ...
随机推荐
- SpringBoot 遇到 com.google.guava » guava 组件运行异常问题修复方案
环境 Apache Maven : 3.5.4 org.springframework.boot » spring-boot-starter-parent : 2.0.3.RELEASE io.spr ...
- oracle查看当前用户,数据库实例
#sysdba用户登录[oracle@oracle ~]$ sqlplus / as sysdba #查看当前用户sql>show user; #查看当前数据库实例sql>show par ...
- .net core通过发布nuget实现引用项目
1 获取 api key 登录 https://www.nuget.org/account 进行api key获取操作 2 安装 nuget package explorer 3 进入要发布的项目目 ...
- java-Timer类使用方法
Timer: public Timer()创建一个新计时器.相关的线程不 作为守护程序运行. 方法摘要: void schedule(TimerTask task, Date time) ...
- 远程连接服务器或云数据库上的mysql服务 - 赖大大
主要问题有两种: 1.mysql的权限问题. 2.服务器的防火墙和数据库的安全组没设好的问题. 1.权限问题: 首先登录上mysql mysql> use mysql; #使用mysq ...
- JVM笔记11-类加载器和OSGI
一.JVM 类加载器: 一个类在使用前,如何通过类调用静态字段,静态方法,或者new一个实例对象,第一步就是需要类加载,然后是连接和初始化,最后才能使用. 类从被加载到虚拟机内存中开始,到卸载出内存为 ...
- 关于setTimeout的的JS知识
https://www.jianshu.com/p/3e482748369d?from=groupmessage
- Django之stark组件
现在让我说啥是stark组件,我也说不清楚.反正从今天讲的知识来看,今天完成的就是自己写一个模块,这个模块包含了admin后台管理工具的一些比较好用的功能,我们把它提炼出来,也就是相当于自己写一个ad ...
- Apollo 3 定时/长轮询拉取配置的设计
前言 如上图所示,Apollo portal 更新配置后,进行轮询的客户端获取更新通知,然后再调用接口获取最新配置.不仅仅只有轮询,还有定时更新(默认 5 分钟一次).目的就是让客户端能够稳定的获取到 ...
- IOS 获取的时间显示为NAN 解决方法
var regTime= item.BeginDate.replace(/\-/g, "/"); ------ item.BeginDate 我的日期年月日的 (/\-/g, ...