Python基础教程之名称空间以及作用域

前言

所谓“基础不狠,人站不稳”,对于任何一种编程语言来说基础往往都是重中之重,以Python为例,其中的两大分水岭就是函数编程和面向对象,而今天所要巩固的知识点后续会多次使用,那就是名称空间和作用域

名称空间

什么是名称空间

在Python中名称空间是用存储对象和名字绑定关系的地方,那么问题来了,什么是对象,什么是名字,什么是绑定关系?

1)在目前,我们对于对象的认知可以暂时只停留在人云亦云的“Python中一切都是对象”基础上,函数是对象、类是对象、变量,模块、所有一切都是对象,有这样的认知就可以了,后续有机会将继续补充;

2)名字,很简单,每一次我们对模块、变量、函数、类的定义都需要取名字,而这些名字都会放在名称空间之中;

3)Python对于名字和数据之间给出了绑定关系,举个例子,当我们在定义a = 6时,Python就自动将变量a这个名字与6这个对象给出了绑定关系,我们可以使用del语句将绑定关系解除。

明白了名称空间是用于存储对象和名字绑定关系的地方,那么接下来就可以细致了解一下名称空间可以分为哪几类了:

1)内置名称空间 —— 用于存放各种内置函数(built-in functions)、内置模块(built-in modules),例如abs()就是内置函数,内置名称空间可以在Python任何一处使用;

2)全局名称空间 —— 全局名称空间中的名字可以在同一个模块中任意处使用;

3)局部名称空间 —— 局部名称空间中的名字仅仅只能够在函数内部使用。

名称空间的意义

名称空间最大的作用就是防止名字重复造成的引用不当,我们可以在全局名称空间中定义一个a = 6同时也可以在局部名称空间中定义一个a = 7,这两者之间是不会产生任何冲突的,这就是名称空间最大的作用,防止名字重复造成的引用不当。

名称空间的查找顺序

知道了名称空间的意义,那么肯定会有读者意识到,我在全局定义一个a = 6,在局部定义一个a = 7,那么接下来调用a这个名字的时候,Python究竟会从哪个空间开始寻找a所对应的对象呢?

我只能说,这位读者你很上道,我们将以实例解答这个问题;

a = 6              # 在全局名称空间中定义一个a
b = 8            # 在全局名称空间中定义一个b,为了测验调用函数时能否找到全局中的b
def test():
    a = 7        # 在局部名称空间中定义一个a
    return a,b
print(test())
print(a)    

(7,8)
6

从以上我们的测验中,调用函数test时输出的a将会是7,而当直接使用print(a)时输出的a将会是6。

所以我们可以大胆的下结论:

1)当调用函数的时候,函数寻找名字的顺序将会是 局部名称空间—>全局名称空间—>内置名称空间;

2)当没有调用函数,直接使用名字的时候查找顺序就是 全局名称空间 —>内置名称空间;

3)只要在某个名称空间(局部也好、全局也罢)中找到了对应的名字,就停止寻找;

4)在不同名称空间中定义相同名字是可行的,后续定义的并不会将原先覆盖掉。

局部名称空间详解

在局部名称空间中有一个非常神奇的事情,因为函数是可以相互嵌套的,在一个函数中嵌套另外一个函数是很正常的现象:

def test_1():           # 定义一个函数
    def test_2():       # 在test_1中定义一个嵌套函数
        print('球球好心人给个赞吧')
# 这是最简单的函数嵌套,
# 但也是最不规范的函数嵌套,
# 因为如果不改进的话,则无法使用嵌套的test_2函数

以上就是最简单形式的函数嵌套,那么问题接踵而至,上文中说过局部命中空间是在函数中产生的,那么如果我在一个函数中定义一个嵌套函数,是不是意味着我在局部名称空间中创建了一个局部名称空间?

对头!

但是在术语上我们会称test_2为最内部名称空间,而test_1则是被我们称为附属函数名称空间;

我们可以如此反复俄罗斯套娃:

def test_1():           # 定义一个函数
    def test_2():       # 在test_1中定义一个嵌套函数
        def test_3():   # 在内嵌函数test_2中再定义一个嵌套函数
            # 省略一万层....
            print('球球好心人给个赞吧')
        print('球球好心人给个赞吧')

嵌套函数中的查找顺序

在前文中已经介绍过了关于嵌套函数所产生的附属函数名称空间、内部名称空间,那么如果在附属函数名称空间和内部名称空间都定义一个相同名字,那么查找顺序是如何呢?

b = 10                    # 在全局定义一个b
def test_1():           # 定义一个函数
    def test_2():       # 在test_1中定义一个嵌套函数
        a = 6           # 在内部名称空间中定义一个a
        return a,b
    a = 7                # 在附属名称空间中定义一个a
    b = 8               # 在附属名称空间中定义一个b
​​​​​​​print(test_1())            # 调用函数

如果真的如上文中这样写的话,那么将不会输出任何结果哦,因为我们只调用了test_1,而作为嵌套的内部函数test_2没有被使用到,想要使用嵌套函数的话,就只能通过将嵌套函数作为返回值,返回出去

所以将代码修改一下

b = 10                    # 在全局定义一个b
def test_1():           # 定义一个函数
    def test_2():       # 在test_1中定义一个嵌套函数
        a = 6           # 在内部名称空间中定义一个a
        return a,b
    a = 7                # 在附属名称空间中定义一个a
    b = 8               # 在附属名称空间中定义一个b
    return test_2
print(test_1()())        # 调用函数

10
(6,8)

按照修改后,我们所得到的结果将会是6,当我们调用嵌套函数的时候,嵌套函数会从自身的局部空间中开始寻找是否有该名称

就像调用嵌套函数test_2一般,它从自己的局部名称空间开始寻找,找到了a = 6后就停止寻找

所以我们又可以下结论了:

1)当调用嵌套函数的时候,它的查找顺序是 内部名称空间—>附属函数名称空间—>全局名称空间—>内置名称空间;

2)找到对应的名字后就会停止寻找。

关于嵌套函数的使用

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2
print(test_1()())                # 仔细看一下调用函数的过程

为什么调用函数过程中需要写两个括号test_1()(),而不是直接test_1()呢?

我们仔细看一下test_1函数的返回值,test_1的返回值是一个函数对象test_2,所以我们如果调用函数的话只写一个括号将会得到一个函数对象,也就是test_2

来实例示范一下;

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2
print(test_1(), type(test_1()))        # 打印输出一下结果


以上就是打印输出的结果,代表了函数对象

可能有读者想要唱反调了,我就是想直接写一个test_1()就能够直接得到想要的结果该怎么办呢?

我只能说,这位看官你很有成为天才的潜力,因为懒才是人类进步的基石,这个需求可以实现,但是我们要这样改代码:

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2()
print(test_1())                # 调用函数

(6,8)

我们的确得到了想要的结果,仔细想一下为什么呢?

还是因为返回值,函数test_1的返回值是test_2(),也就是说返回的结果是函数test_2运行后的结果,又开始俄罗斯套娃了,我拿到的返回值是另外一个函数的返回值!?

我只能说没错,是这样的。

但是用这个方法需要注意一点,那就是内嵌函数必须是无参数的!

b = 10                    
def test_1():           
    def test_2(c):           # 随便定义一个参数
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2()
print(test_1())                # 调用函数

TypeError: test_1..test_2() missing 1 required positional argument: ‘c’
这将会报错,给出的错误是函数test_2()缺失一个名为’c’的位置参数
所以想要使用这种方法还是需要注意下的,
但是换另外一种思路,如果内嵌函数需要参数,那么我返回的时候先把参数定义不行么?
这种方法的确是可行的,
但是如果这样的话那不如直接使用默认参数,在定义的时候直接将参数c定义好

以上就是关于俄罗斯套娃的名称空间的讲解,接下来我们要介绍一下作用域了,如果能够将名称空间中的知识点李姐,那么作用域也不过尔尔。

作用域

什么是作用域

作用域是根据名称空间所产生的,意思就是名字的作用范围;

在上文之中我们其实已经或多或少涉及到了作用域了。

b = 10                            # 这个全局变量的作用域就是该模块中的全部范围        
def test_1():           
    def test_2():       
        a = 6           
        return a,b                # 正因为全局变量的作用于是全部范围,才能够返回b
    a = 7                
    b = 8               
    return test_2
print(test_1()())

或多或少读者对于这个作用域已经有些许了解,

我直接将结论摆出:

内置名称空间 —— 其作用域是Python中的所有模块,能够在所有的模块中使用;
全局名称空间 —— 其作用域是该模块的所有范围,能够在模块内随意使用;
局部名称空间 —— 其作用域仅仅在于该函数内部,只能够在函数内部使用。

可能正是因为作用域的不同,所以查找顺序也会不同,作用域越大的名称空间反而查找的优先级越低,

正如上文中的,即使全局和局部中都有a这个名字,调用函数的时候也会先从局部开始。

global语句

不同的名称空间可以定义相同的名字,这样不会有任何冲突,可这也意味着,当我们在局部名称空间的时候是无法修改全局中的名字绑定关系,于是Python提供了一个方法去解决这个问题:

a = 10               # 定义一个全局语句

def test():
    global a        # 使用global语句,声明我是用的名字a全局名称空间中的那个a
    a = 5
    return a

print(a)            # 先打印输出一下没有调用函数前的a是什么
print(test())        # 输出一下函数中的结果
print(a)            # 看一下全局中的a是否发生了改变

10
5
5

所以我们可以知道,使用global语句后,我们使用的名字都将会是全局名称空间的

nonlocal语句

既然在局部可以修改全局名称空间中的名字绑定关系,那么在内部名称空间是否可以修改附属函数名称空间中的绑定关系呢?

答案显然是可以的,但是需要使用到nonlocal语句。

def test_1():
    def test_2():				# 在test_1里定义一个嵌套函数test_2
        a = 15

        def test_3():			# 俄罗斯套娃一波
            nonlocal a			# 使用nonlocal,声明接下来使用的a是附属名称空间的a,所以究竟是test_2中的还是test_1中的?
            a = 5
            print('调用test_3对a这个名字的绑定关系进行更改')

        test_3()
        print(f'输出附属函数名称空间中的a:{a}')

    a = 10
    test_2()
    print(f'输出最外部函数的a值:{a}')


test_1()

调用test_3对a这个名字的绑定关系进行更改
输出附属函数名称空间中的a:5
输出最外部函数的a值:10

由此我们可以知道,在调用nonlocal声明使用的a是函数test_2中的,而不是test_1中的

因此我们可以得出的结论是:

1)在使用nonlocal语句的过程中,仅仅只会向上寻找到对应名字修改一次

题目题目

还是老规矩,写一个题目对上述内容进行测试

def discount(price,rate):
    final_price = price * rate
    old_price = 6
    print('old_price的值',old_price)
    return final_price

old_price = float(input('请输入价格'))
rate = float(input('请输入折扣率'))
print(discount(old_price,rate))
print('old_price的值',old_price)

在上述代码中,假设我输入的old_price是100,rate是0.6
那么请问两个问题

print(discount(old_price,rate))中会输出的值是多少?
print('old_price的值',old_price)输出的值是多少?

小结

  1. 命名空间是一个名字(变量)和对象的映射表。
  2. 作用域是指命名空间的作用范围,或者说管辖区域。
  3. 变量的查找遵循 LEGB 原则,先从基层(最内层函数找),然后到市委(外层函数)…,再到省委(模块命名空间),最后到中央(builtin 命名空间)。
  4. 各个命名空间相互独立,创建时间和生命周期各不相同。
  5. global 用于在函数内创建和修改全局变量。
  6. nonlocal 用于在内层函数修改外层函数局部变量。
  7. 没有声明 global 和 nonlocal,尝试修改全局变量或外层函数局部变量,实际上只会在函数或者内层函数创建一个新的局部变量,同名的全局变量或者外层函数局部变量不会受影响。

能够看到这里的都是一条汉子!

总结

到此这篇关于Python基础教程之名称空间以及作用域的文章就介绍到这了,更多相关Python名称空间及作用域内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Python基础教程之名称空间以及作用域)