安全开发:说一说Python的作用域与名称空间

341 阅读6分钟
原文链接: zhuanlan.zhihu.com

0x00 前言

在这个系列的开始,介绍了Python文件操作相关的概念,这一节我会说一说对Python的作用域与名称空间的理解。

很多同学可能会觉得说的东西比较基础似乎和安全没有什么关系,其实不然。我是这么理解的,know it,then hack it.真正牛逼的漏洞都是对程序本身有深入的理解后挖出来的,『人肉扫描器』没啥意思,只有真正弄懂了基础,再去分析漏洞报告,做到举一反三,我相信0day也会随之而来。

同时,我更希望看到的是随着SDL的落地,安全意识的提高,从源头上“堵住”漏洞,走出『挖漏洞,补漏洞』的怪圈。

不罗嗦了,开始今天的主题。

0x01 Python的名称空间

什么是名称空间?

简单来说,名称空间就是存放名字与值绑定关系的地方。

这里说的名字与值,不单单指变量名与变量值之间的关系。函数名、类名等均为这里所指的名字。

名称空间是内存中一块隔离的空间。

名称空间还方便了大型项目的协作开发,Python之禅中也提到,“名称空间是一个伟大的发明,在日常开发中,我们要尽可能多的使用它。”

名称空间的分类:

名称空间共分为3类,

1、全局名称空间

2、局部名称空间

3、内置名称空间

注:变量名、函数名、类名等都是文中所指的名字。

内置名称空间,就是在Python解释器启动时生效,在Python解释器关闭时失效。像一些常见的函数,比如说print()、len()、max()等均存在于内置名称空间。

全局名称空间,定义的是文件级别的名字与值之间的绑定关系。举个例子,比如说我们在当前文件中定义变量x=1、函数def fuck:pass,等诸如此类,未在函数或类中定义的变量与值之间的绑定关系就是全局名称空间。这类名字与值之间的绑定关系,直接在Python文件的上下文中定义,未在函数或类的内部定义。

局部名称空间,定义的是函数内部名字与值之间的绑定关系。

在启动Python解释器时,内置名称空间随之生效。此时可以直接调用一些内置的函数,而不需要对该类函数进行定义。当Python解释器关闭时,内置名称空间随之失效。

执行Python文件时,Python解释器会将该Python文件级别的名字与值之间的绑定关系存放起来,存放该绑定关系的空间就是全局名称空间。当该Python文件执行完毕时,全局名称空间也随之失效。

在调用函数时,局部名称就会临时生效。函数调用结束,局部名称空间随之失效。

需要注意的是,类中定义的名字与值之间的绑定关系也可以理解为局部名称空间。与函数中的名称空间不同,类定义完,该名称空间就会产生。除非类被删除,否则这块名称空间不会被回收。

可见,名称空间的加载顺序如下:

1、先加载内置名称空间

2、再加载全局名称空间

3、最后有可能加载局部名称空间

反之,Python解释器查找名字的顺序为,先局部,再全局,最后内置。

0x02 作用域

作用域就是一个作用范围,简单理解,也跟找名字有关。

众所周知,Python的三块名称空间有:内置名称空间、全局名称空间、局部名称空间。

这几个内存空间的特点如下:

内置名称空间来说,

生效:在Python解释器启动时,内置名称空间生效。

失效:在Python解释器关闭时,内置名称空间失效。

全局名称空间呢?

生效:执行Python文件时,全局名称空间生效。

失效:当Python文件执行完毕时,全局名称空间失效。

局部名称空间:

生效:当函数被调用时,局部名称空间临时生效。

失效:函数调用结束,局部名称空间失效。

围绕Python的名称空间,其作用域一共分为两块:全局作用域、局部作用域。

全局作用域由内置名称空间、全局名称空间两部分的名字与值之间的绑定关系组成。

其特点为:全局存活,全局有效,伴随Python文件执行始终。

局部作用域由局部名称空间的名字与值之间的绑定关系构成。

其特点为:临时存活,局部有效。

总结一下,名字的查找顺寻为LEGB,即locals -> enclosing function -> globals -> builtins。

locals:代表函数内部的名字空间,包括函数内部的局部变量,和形式参数。

enclosing:外部函数嵌套的名字空间,这种形式常见于闭包中。

globals:全局变量,即文件级别定义的名字与值之间的绑定关系。函数定义所在模块的名称空间。

builtins:内置模块的名字空间。

查看作用域的两个内置函数:globals()、locals()。

globals(),用来查看全局名称空间的名字。

dir(globals()['__builtins__']),用来查看内置名称空间的名字。

在内置名称空间中,存在诸如print、max、len等名字,这就是为什么我们可以直接调用这些函数,而不需要进行定义的原因。

locals(),用来查看局部名称空间的名字。

在文件级别而不是在函数中调用locals()查看名字时,与globals()的返回值相同。

作用域关系,在函数定义时就已经确定,与调用位置无关。

x = 23333

def f1():
    def f2():
        print(x)
    return f2

f = f1()

print(f)
f()

def func():
    x = 123
    f()

func()

如上代码,由于作用域关系在函数定义时就已经确定了,即f()函数的作用域关系在定义时确定x = 23333(全局名称空间),与调用位置x=123(局部名称空间)无关,所以最后的输出结果仍为23333而不是123。

0x03 关键字global和nonlocal的引入

x = 1
def foo():
    x = 10
foo()
print(x)

该代码的执行结果为:1

x = 1
def foo():
    global x
    x = 10
foo()
print(x)

该代码的执行结果为:10

在函数内部(局部名称空间),要想修改全局名称空间内变量的值需要使用关键字global声明。

x = 1
def foo():
    x = 2
    def foo2():
        nonlocal x
        x = 13213321
    foo2()
    print('foo:'+str(x))
foo()
print(x)

该代码的执行结果为:

foo:13213321
1

在嵌套定义的函数中,要想修改上一级函数中(上一级局部名称空间)变量的值,需要使用关键字nonlocal进行声明。若上一级不存在该变量,则继续向上一级寻找,直到找到为止(仅限在函数内部)。找不到则报错。

0x04 参考链接

9. Classes - Python 3.6.4 documentation