所有的语言学习中,最重要的就是作用域问题。理解作用域,对于理解语言机制,理解应用,至关重要。
通常来说,一种语言,一般来说至少具有三种作用域,也是最常见的三种:
- 全局作用域:哪里都可以访问,具有共享的作用。
- 函数作用域:函数内部可以访问,但是在函数外部不可见。这保证了函数的独立性,从设计上来说,保证内聚的重要机制
- 局部作用域:一般是指块级作用域。这种机制确保在一个函数,局部的内聚和独立性。对于函数代码的组织防止污染具有重要的作用。
全局和函数作用域
Python支持在.py文件中定义全局作用域变量。同时,这样定义的变量可以在本文件内任意地方访问,也支持通过模块的方式导入到其他文件中访问。这与c++等语言有比较大的差异,与js类似。
实际上,我们可以理解为,整个程序都是以模块的方式组织起来的。每一个全局变量,都挂载到了一个模块上,也可以理解为其命名空间。
在当前模块中,只有在全局空间,可以对其进行修改,例如:
# main.py
globalVar = 'aaa'
globalVar = 'bbb' # succsss
但是在局部作用域中,只能读,不能访问。
# main.py
def fun():
print(globalVar) # bbb
globalVar = 'ccccc'
实际上,在函数环境下, 对一个同名全局作用域进行赋值,创建了一个新的函数级变量。这来源于python的变量查找机制。在读取时,他会从内向外逐步查找。但在当前作用域没有找到时,他会进行创建一个新的变量。
那如何改变更加外层作用域的变量呢?
python给出的方案是声明:这又是python的另一种设计哲学了-声明优化隐含。
def fun():
global globalVar
nonlocal outerVal
实际上,这样的方式其实是声明了一个对外层变量的引用。从其实现上来说,函数变量实际上存储于函数内部的一个字典中。而global或nonlocal关键字,在编译时会被特殊处理,也许其内部是一个if语句,使这个变量的修改直接映射到全局作用域中的变量去。
而对于nonlocal,其实现机制类似,不过,他不能找到全局作用域的变量。他会沿着闭包链,一直向外查找 函数级变量。这一点上,其闭包机制与js有很大不同。js会直接捕获闭包范围内的引用并可以改变。但python机制有很大不同。当其赋值时,实际是创建了一个新的变量并对其进行重绑定。
def fun1():
var out1 = 111
def fun2():
nonlocal out1
print(out1)
def fun3():
nonlocal out1
作用域与闭包
闭包是很多语言都支持的现代编程语言核心概念。
python中的闭包与js类似,通过返回函数变量来得到。由于函数可以读取到外部的变量,因此这个函数在移动时,会将读取到的变量也随之带走。
闭包在实现时,实际将捕获到的 变量放入了一 个新的临时空间cell中。这个cell保持了对原来范围内变量的引用,从而保证原来的上下文被销毁后,引用的变量仍然存在。
没有局部作用域
很多同学在使用python时,一定会遇到一个奇怪的现象,跟普通的编程序员有较大不同,比如:
if true:
val1 = 333
print(val1)
类似这样的,在局部作用域中初始化的变量,在后面出了这个局部作用域也可以访问,完全不会报错。而其他大部分语言并不支持这样做。
事实上,python根本没有局部作用域。python本质是动态的脚本语言。在后一个print语言中,报不报错,完全取决于在之前的执行过程中,有没有同域的名为val1的变量被创建。
如果是这样就会报错:
if false:
val1 = 333
print(val1) # NameError
不过这样的设计,个人倒觉得往往并不是很友好,很容易出现局部变量未定义的错误。而使用try catch又显的难以控制和繁琐。倒不如,将这种会出现错误的方式直接从语言层面规避。
没有变量,只有标签
为什么会有这样一些奇奇怪怪问题呢? 答案在于python本身的设计和实现机制。
正常情况下,在其他语言中,所谓的变量我们可以理解为一个盒子。盒子可以装东西,这个东西就是变量的值。
而你必须先有盒子,才能在里面装东西。这就是为什么,变量要先声明才能用。从其底层实现来说,变量名实际是一个内存地址,比如在c里面。内存地址可以指向一块分配的内存,也可以直接存某个数据。
但是在python中,实现要简单的多。python中没有盒子。所谓的变量名,本质是一个字符串。变量,实际上存在于global或者local的变量字典中。 变量名实际就是这个数据的key值。
python对象,实际是一个PyObject类型的结构体。其中包含了管理这个对象所需的信息:引用计数、类型指针、实际的数据等。python环境负责管理了这些,因此,我们看到的对象、变量,属于构建在这个基础之上的一些语法规则。
变量赋值,实际上是在命名空间(一个字典中)查找这个名称是否存在,不管是否存在,都会直接写入进去。所谓NameError,实际就是这个字典的key不存在报出来的错误。
这就解释了,为什么区块级作用域不存在,if 中赋的值,在后面也能用:不管在任何地方,只要对这个变量赋了值,dict中就存在了,引用就不会报错。
nonlocal,global关键字,实际是告诉编译器,不要在local字典中去找,而要去其他地方找这个变量。