Python 的命名空间和作用域

引子

假如写一个求 x,y 最大值的函数:

1
2
3
4
5
6
In [1]: def my_max(x, y):
...: m = x if x > y else y
...: return m

In [2]: my_max(19, 29)
Out[2]: 29

这里为什么要把结果 return 呢?直接在程序中输出,行不行?

1
2
3
4
5
6
7
8
9
10
11
12
13
In [1]: def my_max(x, y):
...: m = x if x > y else y
...:

In [2]: my_max(19, 29)

In [3]: print(m)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-3-c384c0c7b6c7> in <module>
----> 1 print(m)

NameError: name 'm' is not defined

报错了!错误是 name 'm' is not defined,变量m 没有定义,明明 m 已经在函数中定义了啊?

是这样的,从 python 解释器开始执行之后,就在内存中开辟了一个空间
每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来。
但是 当遇到函数定义的时候解释器只是象征性的将函数名读入内存 ,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心。
等执行到函数调用的时候,python 解释器会 再开辟一块内存来存储这个函数里的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中。函数中的变量只能在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。

就把这个记录变量名和值之间的对应关系的东西叫做“命名空间”。
当代码刚开始运行时,创建的命名空间叫做 全局命名空间 ,在函数的运行时开辟的内存空间成为 局部命名空间

命名空间

命名空间的本质:存放变量名和变量值的对应关系,类似于之后学的字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [6]: import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

在 Python 之禅最后一句提到过:命名空间是一种绝妙的理念,让我们尽情地发挥吧!

命名空间分为三类:

  1. 全局命名空间
  2. 局部命名空间
  3. 内置命名空间

命名空间加载顺序:
内置命名空间(程序运行前加载)–> 全局命名空间(程序运行时,从上到下依次加载) –> 局部命名空间(函数调用时加载)

命名空间取值顺序
局部命名空间(在函数中)–> 全局命名空间 –> 内置命名空间。

作用域

作用域,即变量的有效作用范围。作用域分为 全局作用域 局部作用域,作用域和命名空间的对应关系如下:

  • 全局作用域:包括内置命名空间、全局命名空间,在程序执行过程中全局可调用。
  • 局部作用域:对应局部命名空间,只能在局部范围内生效

全局变量是在函数之外声明的变量,相反,局部变量就是在函数之内声明的变量。一般情况下,全局变量在程序结束后,被系统回收,局部变量则在函数调用结束后被注销掉。

global

在 Python 中可以轻松的访问全局变量,例如:

1
2
3
4
5
a = 1
def m():
return a

print(m() == a)

输出结果: True
可见,a是一个全局变量,在 m 函数中返回了全局变量 a 的值。
但是如果 m 函数中要试图修改 a 的值,例如:

1
2
3
4
5
6
a = 1
def m():
a = 2
return a

print(m()==a)

输出结果: False
从结果看出,m中的 a 是局部变量,与全局变量中的 a 并不是同一个对象。那么要修改全局变量中 a 的值便不能这么直接了当的来,必须使用 global 关键字声明我要更改的全局变量,上面的代码改成:

1
2
3
4
5
6
7
a = 1
def m():
global a # 不要写成 global a = 2
a = 2
return a

print(m()==a)

输出结果: True
可见,全局变量 a 更改成功。
这里出现一个陷阱,看下面的代码:

1
2
3
4
5
6
a = [1]
def m():
a[0] = 2
return a[0]

print(m()==a[0])

输出结果: True
看似应该报错的代码,却出现了 global 的效果。没有声明 global a,但是更改了全局变量a。所以,对于列表、字典等可变对象,不需要global 声明便可以直接修改里面的值。但是要彻底改变全局变量 a 的类型,如 listdict,就需要 global a 声明。

nonlocal

上面介绍的 global 关键字的用法,在函数中需要更改全局变量就需要用 global 声明。
如果需要更改的变量既不在当前函数内,也不在全局命名空间中,就需要使用 nonlocal 声明,例如:

1
2
3
4
5
6
7
8
9
10
11
a = 1                   # 定义全局变量 a
def outside():
a = 2 # 定义 outside 内部变量 a
def inside():
nonlocal a # 声明其他局部命名空间中的变量
a = 3 # 修改 a
return a
return inside() # 返回修改后 a 的值

print('nonlocal a:' + outside())
printf('global a:' + a)

当我们运行这段代码,输出结果如下:

1
2
nonlocal a: 3
global a: 1

可见,nonlocal声明的是 outside 中的 a,与全局a 无关。而且,需要注意的是,nonlocal声明并不是只能从上层函数中寻找局部变量,如果上层函数没有,将继续向外找直到最外层函数。
如果在最外层函数使用nonlocal,将会出现以下报错信息:

1
SyntaxError: no binding for nonlocal 'a' found

————–2018 年 11 月 28 日更新—————

到底哪个算是上层命名空间?

之前讨论了命名空间的问题,如果在局部命名空间空间中未曾找到变量,就会依次向上层命名空间寻找。这里的上层命名空间值得讨论一下,之前的例子大部分都是函数嵌套,上层命名空间非常容易辨别。且看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
a = "global"

def f1():
a = "local"
print(a)
f2()

def f2():
print(a)

f1()

从这段程序看,输出结果有两种可能:

  1. 输出两个“local”
  2. 输出一个“local”,一个“global”
    到底哪一个才是正确的,瞎猜不如实践一下,运行结果如下:
    1
    2
    local
    global

由此可见,上层命名空间指的并不是调用函数的地方,而是定义函数的地方,这一点需要注意,避免混淆,在别写其他语言的代码中也可能会遇到这个问题,例如golang

参考





root@kali ~# cat 重要声明
本博客所有原创文章,作者皆保留权利。转载必须包含本声明,保持文本完整,并以超链接形式注明出处【Techliu】。查看和编写文章评论都需翻墙,为了更方便地获取文章信息,可订阅RSS,如果您还没有一款喜爱的阅读器,不妨试试Inoreader.
root@kali ~# Thankyou!