对python中变量作用域、命名空间、引用和赋值的理解

1 命名空间(namespace)

A namespace is a mapping from names to objects

命名空间是名字和对象的映射。可以简答地把namespace理解为一个字典,实际上很多当前的Python实现namespace就是用的字典。各个命名空间是独立的,没有任何关系的,因此一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

常见的命名空间包括:

  • python built-in names,包括内置函数、内置常量、内置类型
  • 模块的global names,包括模块定义的函数,类,变量
  • 简单函数的所有local names,以及嵌套函数的enclosing names
  • 类对象的class names, 包括所有属性和方法

命名空间有创建时间和生存期,具体而言:

  • python built-in names: 在Python解释器启动的时候被创建,在解释器退出的时候才被删除;
  • 模块的global names: 在该module被import的时候创建,在解释器退出的时候退出;
  • 函数的local namespace,它在函数每次被调用的时候创建,函数返回的时候被删除;
  • 嵌套函数的enclosing names,它在外层函数每次被调用的时候创建,函数返回的时候被删除,注意当闭包存在时,enclosing names可能变为内层函数对象的环境变量。
  • 类对象的class names, 当类被建立时创建,在解释器退出的时候才被删除。

可见,一个模块的引入,函数的调用,类的定义都会引入命名空间,函数中的再定义函数,类中的成员函数定义会在局部namespace中再次引入局部namespace。

2 作用域(scope)

A scope is a textual region of a Python program where a namespace is directly accessible.

作用域是Python中某个命名空间中的名字可以被直接引用的上下文区间。

在Python中并不是所有的语句块中都会产生作用域。只有当变量在Module(模块)、Class(类)、def(函数)中定义的时候,才会有作用域的概念。

具体而言,常见的作用域由内到外(按照程序的层级关系)包括:

  • 函数内部的局部作用域,即local scope
  • 嵌套函数中外层函数的局部作用域,即enclosing scope
  • 模块的全局作用域,即global scope
  • Python内置对象的最外层作用域,即build-in scope
  • 类对象的作用域,即class scope

显然,作用域的分类与命名空间是一一对应的。在【命名空间】中曾提及不同的命名空间是可以重名而没有任何影响。 那么当在不同的命名空间中取相同的变量名时,程序又是如何区分的呢?这就涉及到变量调用的搜索顺序 :

在python中,变量不需要事先声明,赋值后即可调用使用。调用的法则遵从LEGB法则,其中L为local,E为enclosing,G为global,B为built-in,即变量首先在局部搜索,比如一个函数中,如果没有搜寻到,则继续在enclosing中搜寻,比如果还是没有,则搜索全局变量,如果还是没有,就搜索内建变量名,最终找不到的话,则抛出异常。这符合从底层到顶层的设搜索计逻辑。

值得注意的是,如果定义了nonlocal(见下文),那么将不遵从LEGB法则,而直接从enclosing中搜索。

3 locals, nonlocal & global

locals是Python的内置函数,可以方便的查看函数会以字典类型返回当前位置的全部局部变量。

nonlocalglobal是Python中两个关键词,均用于在函数定义中澄清变量的作用域。下面来详细介绍下nonlocalglobal的基本用法。

3.1 global的基本用法

(1)直接引用global变量
首先定义一个嵌套函数,解释器在解释时遇到内部变量x,会按照LEGB的顺序直接查找,直到在global namespace中找到x,返回其引用的常数对象值。

x = 5
def outer(): 
    def inner():
    	y = x+5   # 按照LEGB的顺序寻找变量名x,发现为global变量
        print(y)   
    return inner

if __name__=='__main__':
    func = outer()
    func()    # Output: 10

(2)函数内赋值同样名称的变量
此时解释器会将其作为局部变量(无视global中的x)。

x = 5
def outer(): 
    def inner():
    	x = -5
    	y = x+5   # 按照LEGB的顺序寻找变量名x,发现在local中有定义
        print(y)  
    return inner

if __name__=='__main__':
    func = outer()
    func()    # Output: 0

(3)函数内先引用,再重新赋值
此时在本地命名空间内,解释器发现同时有赋值和引用操作,会认为这是局部变量,而在引用前又未进行赋值初始化,因此会找不该局部变量而报错UnboundLocalError

x = 5
def outer(): 
    def inner():     # local namespace中同时有赋值和引用操作,解释器会认为是局部变量
        y = x+5    # x 作为局部变量并未先赋值初始化,因此报错!
        print(y)  
        x = -5   

    return inner

if __name__=='__main__':
    func = outer()
    func()    # UnboundLocalError: local variable 'x' referenced before assignment

要在不改变上述程序逻辑顺序的同时修正错误,可以显式声明为global对象。
(4)显示声明global

x = 5
def outer(): 
    def inner():
        global x   # 声明为全局变量
        y = x+5   
        print(y)  
        x = -5   # 重新为全局变量赋值
        print(x)
    return inner

if __name__=='__main__':
    func = outer()
    func()    # Output: 10    -5

(5)可变对象的情况
以上(1)-(4)对比了不可变变量时,是否采用global的异同,而对于可变对象,也遵循相同的原则,因此这里不给出具体的示例,而直接给出相关原则。

基本原则:
a) 若在local scope内直接引用全局变量,则无需显式声明global,可直接使用;
b) 若在local scope内只直接操作全局变量(可变变量时),则全局变量会同步结果;
c) 若在local scope内只为同样名称的变量重新赋值,则视为是local变量,后续操作与外部定义的同名全局变量无任何关系;
d) 若local scope内先对全局变量进行操作,在对同名变量进行赋值操作,则会报错。

3.2 nonlocal的基本用法

nonlocal是用来界定local变量和enclosing变量(嵌套函数中外部函数中定义的变量)的关键词,其原则与local变量和global变量间的原则相同,这里不加赘述。

3.3 global和nonlocal的对比

相同点:两者都是为了规定变量的作用域,从而避免变量定义和搜索时的错误。

不同点
(1)两者的功能不同。global关键字标识全局变量,对该变量进行修改就是修改全局变量,而nonlocal关键字标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误(最上层的函数使用nonlocal修饰变量必定会报错)。

(2)两者使用的范围不同。global关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global修饰后也可以直接使用,而nonlocal关键字只能用于嵌套函数中,并且外层函数中定义了相应的局部变量,否则会发生错误(见(1))。

最后,给出一个包含局部变量、嵌套变量和全局变量三类变量的例子供参考:

def scope_test():
    def do_local():
        spam = "local spam" #此函数定义了另外的一个spam字符串变量,并且生命周期只在此函数内。此处的spam和外层的spam是两个变量。
    def do_nonlocal():
        nonlocal  spam        # 使用外层的spam变量
        spam = "nonlocal spam"  # 修改了外层的spam变量
    def do_global():
        global spam
        spam = "global spam"
    spam = "test spam"    # 外部函数中定义的enclosing变量
    do_local()
    print("After local assignmane:", spam)   # 首先搜索到enclosing变量
    do_nonlocal()     # 为enclosing变量重新赋值
    print("After nonlocal assignment:",spam)   # 仍然是enclosing变量
    do_global()
    print("After global assignment:",spam)  # 仍然是 enclosing变量
 
scope_test()
print("In global scope:",spam)    # 函数外面,因此是global变量

# After local assignmane: test spam
# After nonlocal assignment: nonlocal spam
# After global assignment: nonlocal spam
# In global scope: global spam

4. 赋值和引用

在前面globalnonlocal的介绍中,涉及到Python中的两个重要概念:赋值引用,这里做简单的介绍。

4.1 赋值(assignment)

Assignments do not copy data — they just bind names to objects.

在Python中,对象是独立的,不同作用域中的不同名字都可以被绑定在同一个对象上,当然对这个对象的修改会影响所有的引用。赋值操作就是名字和对象的绑定或重绑定。

函数调用的参数传递是赋值(也可以说是引用),不是拷贝。

4.2 引用(reference)

引用就是指变量名指向内存中一块对象的关系,赋值的过程就是引用。

【Reference】

  1. nboundlocalerror报错原因

你可能感兴趣的:(python,变量作用域,命名空间,引用,赋值,global)