默认作用域

前段时间学了下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的默认作用域以及闭包的更多相关文章

  1. Python基础:11变量作用域和闭包

    一:变量作用域 变量可以是局部域或者全局域.定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域. 全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数, ...

  2. Python tricks(2) -- method默认参数和闭包closure

    Python的method可以设置默认参数, 默认参数如果是可变的类型, 比如list, map等, 将会影响所有的该方法调用. 下面是一个简单的例子 def f(a=None, l=[]): if ...

  3. python函数名称空间与作用域、闭包

    一.命名空间概念 1.命名空间(name space) 名称空间是存放名字的地方. 若变量x=1,1存放在内存中,命名空间是存放名字x.x与1绑定关系的地方. 2.名称空间加载顺序 python te ...

  4. Python 内置函数、作用域、闭包、递归

    一.内置函数如何使用 help()一下: 如想看min()咋用?在shell中:help(min) 二.部分内置函数 (一).排序:sorted() li = [(1, 2, 3, 4), (7, 8 ...

  5. Python内置函数、作用域、闭包、递归

    1.几个可能用到的内置函数 2.函数内变量的作用域 3.内嵌函数和闭包 4.递归函数 1.常见的内置函数 常见的内置函数:    查看内置函数:        print(dir(__builtins ...

  6. python 变量作用域、闭包

    先看一个问题: 下面代码输出的结果是0,换句话说,这个fucn2虽然已经用global声明了variable1,但还是没有改变变量的值 def func1(): variable1=0 def fun ...

  7. Python入门笔记(22):Python函数(5):变量作用域与闭包

    一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除,否则他们存活到脚本运行结束,且对于所有的函数都可访问. 当搜索一个标识符(也称变量.名字等),Pyt ...

  8. python中对变量的作用域LEGB、闭包、装饰器基本理解

    一.作用域 在Python程序中创建.改变.查找变量名时,都是在一个保存变量名的空间中进行,我们称之为命名空间,也被称之为作用域.python的作用域是静态的,在源代码中变量名被赋值的位置决定了该变量 ...

  9. Python学习(21):Python函数(5):变量作用域与闭包

    转自 http://www.cnblogs.com/BeginMan/p/3179040.html 一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除, ...

随机推荐

  1. 通过Anaconda在Ubuntu16.04上安装 TensorFlow(GPU版本)

    一. 安装环境 Ubuntu16.04.3 LST GPU: GeForce GTX1070 Python: 3.5 CUDA Toolkit 8.0 GA1 (Sept 2016) cuDNN v6 ...

  2. 201. Orchard学习 一、基础

    一.项目介绍 Orchard是一个免费和开源的社区交流项目,致力于在ASP.NET平台开发应用程序和可重用性组件.它将创建用于ASP.Net应用和扩展的共享组件,以及修改这些组件以便使其应用于终端用户 ...

  3. 六:理解控件的运行机制(例:基于WebControl命名空间的控件)

    Control类的Render方法在WebControl类中被被分成三部分1:RenderBeginTag,呈现标签的开始2:RenderContents,呈现标签的内容3:RenderEndTag, ...

  4. 深入学习sequoiadb巨杉数据库及python连接方式

    随着公司日益复杂与多变的需求,以及迅速扩展带来的海量数据业务,我们需要在提供高效服务的同时,降低其设备与程序维护成本.算了,不吹了,说白了就是需要从巨杉数据库中抓取大量的数据,但是我现在不会,所以需要 ...

  5. Everything(一款用于检索硬盘文件的工具)

    有时候文件夹一多,找不到文件,忘记放哪个盘符怎么办? Everything就能帮你解决,比电脑自带的快多啦,官网在此:http://www.voidtools.com/ (也不大,就几M,没有特别的安 ...

  6. mysql查看执行sql语句的记录日志

    开启日志模式 -- 1.设置 -- SET GLOBAL log_output = 'TABLE';SET GLOBAL general_log = 'ON';  //日志开启 -- SET GLOB ...

  7. php下载远程图片到本地

    在使用 PHP 做简单的爬虫的时候,我们经常会遇到需要下载远程图片的需求,所以下面来简单实现这个需求1:使用curl 比如我们有下面这两张图片: $images = [ 'https://img.al ...

  8. Java框架之Struts2(三)

    一.几种国际化输出方式 国际化:国际化的本质,就是查找,替换.程序界面上输出的是国际化消息的 key 所对应的值.Struts2 中的国际化机制,会根据 key 到资源文件中找对应的文本来替换上面的k ...

  9. JavaSE Map集合

    Map集合 在Map集合中保存的数据为一组数据,其中:一个数据为key,另外一个数据为value.而key和value具备对应的关系,在集合中它们属于一组(一对)数据.而每个key只能对应唯一的一个v ...

  10. 【Java并发编程】15、ReentrantLock实现原理深入探究

    原文已经写得非常详细了,直接把大神的文章转发过来了  https://www.cnblogs.com/xrq730/p/4979021.html 前言 这篇文章被归到Java基础分类中,其实真的一点都 ...