《最值得收藏的python3语法汇总》之命名空间和作用域

关于这个系列

《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。

你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。

这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。


命名空间(namespace)是对符号(变量、函数、类等等)名字的一种分组机制,它提供了在项目中避免名字冲突的一种方法。不同组的相同命名符号被视作两个独立的符号,因此隶属于不同命名空间的符号名称可以重复。

命名空间是高级编程语言的一种基本概念,C++、Java等都支持命名空间机制。C语言不支持命名空间,所以C语言的程序员需要自己保证命名在全局范围内不重复。

在Python中,命名空间采用了字典的结构来实现,它记录了符号名称和对象之间的对应关系。

Python有4种命名空间:

  • 局部命名空间(Local):函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)。
  • 闭合命名空间(Enclosing):外部嵌套函数的名字空间(例如closure)。
  • 全局命名空间(Global):模块(.py)中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
  • 内置命名空间(Built-in): Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。

 

需要注意的是,上图中的命名空间并没有从上到下的包含关系,比如:我们不能认为Global-ns是包含它下级的Local-ns的。事实上,所有的命名空间都是独立存在的。

 

 

 什么是作用域(scope)?

作用域是Python的一块文本区域,这个区域中,命名空间可以被“直接访问”。这里的直接访问指的是试图在命名空间中找到名字的绝对引用(非限定引用)。这里有必要解释下直接引用和间接引用:

直接引用:直接使用名字访问的方式,如name,这种方式尝试在名字空间中搜索名字name。

间接引用:使用形如objname.attrname的方式,即属性引用,这种方式不会在命名空间中搜索名字attrname,而是搜索名字objname,再访问其属性。

 

上面的四种命名空间的作用域如下:

  • 局部命名空间(Local):它的作用域仅限于本地,比如本函数、本类等。
  • 闭合命名空间(Enclosing):它的作用域仅限被嵌套的下层函数。
  • 全局命名空间(Global):它的作用域是整个模块(.py)。
  • 内置命名空间(Built-in): 它的作用域是整个python解释器运行实例。

我们可以看到,这些命名空间的作用域是相互包含的。

 

当解释器要查找一个符号名字时,它会采用L-E-G-B(由近及远)的顺序,依次在这四种命名空间中去查找。

我们看看下面的例子:


#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./11/11_1.py
# 命名空间和作用域

x = 1  # 位于Global-NS

def foo():
    x = 2  # 位于foo的Local-NS,相对于innerfoo来说,是外层嵌套函数NS,Enclosing-NS
    def innerfoo():
        x = 3  # 位于Local-NS
        print('locals', x)
    innerfoo()
    print('enclosing function locals ', x)

foo()
print('global ', x)

输出为:

locals 3

enclosing function locals  2

global  1

如果要执行innerfoo函数,查找变量x时,按照LEGB原则,获取到的是Local-NS中的x对应的对象。

这里需要注意的是,Local-NS和Enclosing-NS的概念是相对的。对于innerfoo来说,foo的Local-NS也是innerfoo的Enclosing-NS。

需要特别注意的是,Python中,只有模块、函数、类、推导式等可以产生作用域,而if、for、while等等这些控制语句,则不会产生作用域。这和其他编程语言存在差异。比如下面这个简单的例子:

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./11/11_2.py
# if不产生作用域

x = 100

if True:
    x = 200

print(x)

这个例子中,x被改变了,最后输出结果为200。所以if语句中使用的x变量就是Glocal-NS的中的名字x。if本身并没有产生一个新的作用域。

 

理解变量的定义和引用?

我们需要清楚地理解变量的定义和引用这两个行为的区别。

比如: x = 100,这是定义了变量x (define); 而print(x),这是引用了变量x。

对于引用变量的行为,python会采用LEGB原则到命名空间去查找名字,如果找不到则抛出异常;

在Python中,任何符号(变量名、函数名、类名等等)都是需要先定义后才能被引用的。

对于定义变量的行为,python会在对应的命名空间增加名字和对象的映射关系。同时要注意一点,定义行为是优先于引用行为的

我们再回头看看前面的例子,我们只看函数foo定义的代码片段:

def foo():

    x = 2
    def innerfoo():
        x = 3 
        print('locals', x)
    innerfoo()
    print('enclosing function locals ', x)

内层函数innerfoo中第一行代码x=3,它到底是新定义了一个Local-NS的变量名x呢?还是引用了外层函数foo的变量名x呢?根据我们前面提到的原则:定义行为优先于引用行为。所以,解释器会优先认为它是新定义的一个Local-NS变量名x,并添加到命名空间中。下一行print函数引用变量x时,更加LEGB顺序,首先查找到的就是这个新定义的变量名x。

这个例子中的x是一个不可变数据类型,我们再看一个可变数据类型,它的行为会让你感到更加“怪异”!

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./11/11_3.py
# 变量的定义和引用

list_1 = [100, 200]
list_2 = [100, 200]

def foo1():
    list_1 = [300]  # 这是定义

def foo2():
    list_2.append(300)  # 这是引用

foo1()
foo2()
print(list_1)
print(list_2)

输出为:

[100, 200]

[100, 200, 300]

List_1和list_2产生的结果是完全不一样的,foo1中是新定义了一个Local-NS的变量list_1,而foo2中是对Global-NS中list_2的引用。

通过前面的例子,我们可以看到,如果在作用域里面使用=号赋值运算符,那么解释器会优先认为是定义行为。那么如果我们要用=号修改外部变量,该怎么办呢?

我们需要使用到global和nonlocal关键字。

Global:告诉解释器,我使用的这个变量是Global-NS里面的变量。

Nonlocal:告诉解释器,我使用的这个变量是外层嵌套函数的变量。

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./11/11_4.py
# global

list_1 = [100, 200]
def foo1():
    global list_1  # 声明list_1使用的是全局的
    list_1 = [300]  # 这是引用,是对list_1的修改

foo1()
print(list_1)

输出为:

[300]

解释器看到global声明后,会在foo1中引用Global-NS中的list_1名字。所以,list_1=[300]不被认为是定义行为,因为已经定义过了。

同样,nonlocal也类似:

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./11/11_5.py
# non-local

def foo1():
    list_1 = [500]
    def foo1_inner():

nonlocal list_1
        list_1 = [300]  # 这是引用,是对list_1的修改
    foo1_inner()
    return list_1

print(foo1())

输出为:

[300]

解释器在看到nonlocal声明后,会引用外层函数foo1定义的变量list_1。

Global和nonlocal存在一个重要的区别,nonlocal声明的变量必须在外层函数中已经定义,否则解释器会报错。而global声明的变量如果没有定义,那么它会主动帮你定义一个。

大家在看一些官方手册时,还会提到一个“自由变量(free variable)”的概念。当一个变量被引用的地方不是它定义的作用域时,这个变量就叫做“自由变量”。比如上面例子中全局定义的list_1,被foo函数内部引用了,那么list_1就是一个自由变量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章