深入理解Python中命名空间的查找规则
命名空间,英文名字:namespaces。
什么是命名空间?由于Python一切皆对象,而对象很多时候直接使用并不是很方便,所以要给它取一个名字。打个比方,我们常常使用的手机,我们买手机的时候经常会给销售说:我看一下华为夏日胡杨色Mate40Pro手机",这就是手机的对象。但是我们平时在提及的时候,总是说:“华为夏日胡杨色Mate40Pro手机",是不是太麻烦了?于是我们人类就为这个世界上的各种对象命名,比如将"华为夏日胡杨色Mate40Pro手机”称之为"手机"。在编程中也是如此,为了更好的表示变量与引用对象的关系,我们常常通过变量赋值完成它们之间的映射。比如:上面这个代码,5就是一个计算机内存中存在的对象,使用id()内置函数可以查看对象在内存中的地址。a就是为这个对象赋予的别名,如前面所讲,它是与内存中一个编号为的对象关联,这个对象就是5。所以命名空间是从所定义的命名到对象的映射。因此大部分的命名空间都是通过Python字典来实现的,字典内保存了变量名称与对象之间的映射关系不同的命名空间,可以同时存在,但彼此相互独立互不干扰。一般有三种命名空间:
内置名称(built-innames),Python语言内置的名称,比如函数名max、id和异常名称BaseException、Exception等等。全局名称(globalnames),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。局部名称(localnames),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)它们的包含关系如下:
如果我们要搜索一个变量a,查找顺序为:局部的命名空间-全局命名空间-内置命名空间。从内圈向外圈查找。
如何来查看局部和全局的变量有哪些呢?我们通常使用Python内置的函数:globals()和locals()
比如:
number=10#声明func函数deffunc():n=10m=5total=0foriinrange(n):total=i+mprint(locals())#定义类classPerson:def__init__(self,name):self.name=namedef__str__(self):returnself.nameprint(globals())#调用函数func()
运行结果:
LEGB那LEGB规则与命名空间有什么关联呢?
前面讲到,Python的命名空间是一个字典,字典内保存了变量名称与对象之间的映射关系,因此,查找变量名就是在命名空间字典中查找键-值对。Python有多个命名空间,因此,需要有规则来保证命名空间的查找顺序,而LEGB就是用来规定命名空间查找顺序的规则。LEGB是指:什么是内建作用域呢?内建作用域是通过一个名为builtin的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。在Python3.0及以上版本中,可以使用以下的代码来查看到底预定义了哪些变量:importbuiltinsdir(builtins)
我们通过一个案例来看一下查找规则:
number=10#声明func函数deffunc():number=definner_func():number=0print(number)#调用inner_func()函数inner_func()#调用外部函数func()
查找顺序:
以inner_func()内部的print(number)为例:
1.如果inner_func()函数内部有number这个变量,那么number这个变量就是局部的;
2.如果inner_func()函数内,没有这个变量,那么就会去找该函数外层的函数func()中有没有number这个变量,如果有,那么这个number就是闭包的;
3.如果外层函数func()中没有变量number,那么就会去最外层的全局变量找;
4.如果全局变量没有number,就去内置中找。
但是需要注意的是:
Python中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问。a=10b=8ifab:c=5print(a+b+c)else:print(a+b)print(a,b,c)
结果:
23
可以发现没有报错,在if中声明了新的变量c,但是在if..else的外层也可以使用此变量c。
在函数的内部不可以直接修改全局变量的值,需要使用关键字global声明num=1deffun1():globalnum#需要使用global关键字声明print(num)num+=5#此处对全局变量进行修改了print(num)fun1()print(num)
同样在内层函数中也不能直接修改外层函数的变量需要加nonlocal声明。
num=1deffun():print(num)a=10definner_func():nonlocalaa+=5print(a)#调用inner_func函数inner_func()print(a+num)#调用funcfunc()
结果:
1
15
16
为什么是这个结果呢?上面如果分别去掉global或者nonlocal会有什么报错呢?在第一个案例中如果num不是整型变量1,而换成一个列表还用使用global关键字声明吗?
闭包闭包是跟上面的案例有关的,为什么?因为上面案例中我们在一个函数中定义了另一个函数,外部的称作外层函数,里面的函数称作内层函数。
那怎样就构成闭包呢?需要在上面的基础上再加上一些条件闭包条件在一个外函数中定义了一个内函数。内函数里运用了外函数的临时变量。并且外函数的返回值是内函数的引用满足了这三个条件我们就把函数称作闭包函数了。为什么要这样写呢?
一般情况下,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。
看一个案例:
#闭包函数的实例#outer是外部函数a和b都是外函数的临时变量defouter(a):b=10#inner是内函数definner():#在内函数中用到了外函数的临时变量print(a+b)#外函数的返回值是内函数的引用returninnerif__name__==__main__:#在这里我们调用外函数传入参数5#此时外函数两个临时变量a是5b是10,并创建了内函数,然后把内函数的引用返回给了demo变量#外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数demo=outer(5)#我们调用内部函数,看一看内部函数是不是能使用外部函数的临时变量#demo存了外函数的返回值,也就是inner函数的引用,这里执行demo()就相当于执行inner()函数demo()#15那我们就说outer就是一个闭包函数。在闭包内函数中,可以随意使用外函数绑定来的临时变量,但是如果想修改外函数临时变量数值的时候就需要添加nonlocal(前提这个临时变量是不可变的,如果是可变类型的就无需添加nonlocal了)还有一点需要注意:使用闭包的过程中,一旦外函数被调用一次返回了内函数的引用,虽然每次调用内函数,是开启一个函数执行过后消亡,但是闭包变量实际上只有一份,每次开启内函数都在使用同一份闭包变量即:
defouter(a):b=10#inner是内函数definner(c):#在内函数中用到了外函数的临时变量print(a+b+c)#外函数的返回值是内函数的引用returninnerif__name__==__main__:demo=outer(5)#下面是相同的demo函数,同一份闭包变量demo(5)demo(6)
闭包用途
装饰器!装饰器是做什么的?其中一个应用就是,我们工作中写了一个登录功能,我们想统计这个功能执行花了多长时间,我们可以用装饰器装饰这个登录模块,装饰器帮我们完成登录函数执行之前和之后取时间。
面向对象!经历了上面的分析,我们发现外函数的临时变量送给了内函数。对象有好多类似的属性和方法,所以我们创建类,用类创建出来的对象都具有相同的属性方法。
实现单例模式!这也是装饰器的应用。
装饰器上面我们提到了闭包,那装饰器和闭包又有什么关系呢?
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。而实际上,装饰器就是一个闭包,把一个函数当做参数然后返回一个替代版函数。通俗一点的理解:现在的妹子喜欢化妆,化妆后的妹子堪比整容,但是人是没有变化的,只不过加了更多的修饰而已。那原来的你就是化妆前,漂亮的你就是装饰后。
装饰器常可以用于:插入日志、性能测试、事务处理、缓存、权限校验等场景。
先来看一个简单例子,有一个函数是用来求和的,现在想记录一下函数的调用情况通过日志的方式。
defadd(x,y):result=x+yprint(求和结果是:+str(result))
于是可以在该函数里面使用loggging模块的info记录函数,代码如下:
importloggingdefadd(x,y):result=x+yprint(求和结果是:+str(result))logging.info("addisrunning")
但是假设还有其他的函数比如:func1(),func2(),....都需要添加日志记录,这时我们可能会在每个函数中添加类似的一句话:
logging.info(addisrunning)
注意:logging模块提供的日志记录函数所使用的日志器设置的日志级别是WARNING,因此只有WARNING级别的日志记录以及大于它的ERROR和CRITICAL级别的日志记录被输出了,而小于它的DEBUG和INFO级别的日志记录被丢弃了。
但是这样就造成大量雷同的代码,为了减少重复写代码,我们可以重新定义一个函数:专门处理日志,日志处理完之后再执行真正的业务代码。
有人给出这样的解决方案:
importloggingdefrecord_log(func,*args,**kwargs):logging.warning("%sisrunning"%func.__name__)func(*args,**kwargs)defadd(x,y):result=x+yprint(求和结果是:+str(result))deffunc1():print(我是func1函数)#调用add函数record_log(add,1,2)#调用func1函数record_log(func1)
逻辑上应该不难理解,但是这样的话,我们每次都要将一个函数作为参数传递给record_log函数,最主要的是之前执行业务逻辑时,执行运行add()或者func1()的地方,但是现在不得不改成record_log(参数)的调用了。但是这样会破坏原来的代码,如果你的代码量很多很多的话,修改起来则有是灾难。当然我们还有更好的解决发方式就是:装饰器。
来看一个简单的装饰器,实现模拟:日志打印器
日志就是你调用了哪些方法的一个记录,就类似我们每天做的事情要通过日记记录下来一样
deflogger(func):defwrapper(*args,**kwargs):print(准备调用函数:+func.__name__)#执行funcfunc(*args,**kwargs)print(哈哈哈,我调用了+func.__name__+很棒吧!点赞!)returnwrapper#有一个求和的函数,给其添加装饰器
loggerdefadd(x,y):result=x+yprint(求和结果是:+str(result))add(5,3)结果:
准备调用函数:add求和结果是:8哈哈哈,我调用了add很棒吧!点赞!
其中
符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作。我们来看一下使用装饰器的原理是什么?
#Mark1为托尼的装甲defMark1(func):#变身deftransform(*args,**kwargs):print(我变成钢铁侠了)returnfunc(*args,**kwargs)returntransformdefTony():我是大名鼎鼎的托尼print(我是斯塔克工业的CEO)new_Tony=Mark1(Tony)#有一套装甲函数,将Tony作为参数传入,返回一个新的引用new_Tony()#调用新的Tony函数
结果:
我变成钢铁侠了我是斯塔克工业的CEO
然后这段代码,写起来有点麻烦,Python官方出了一个快捷代码,也就是语法糖
,用了语法糖就变成了下面这样#Mark1为托尼的装甲defMark1(func):#变身deftransform(*args,**kwargs):print(我变成钢铁侠了)returnfunc(*args,**kwargs)returntransform
Mark1defTony():我是大名鼎鼎的托尼print(我是斯塔克工业的CEO)Tony()这样对于函数调用本身是没有变化的,因为调用Tony()的函数名没有变化。看下面的代码输出结果是什么?#Mark1为托尼的装甲defMark1(func):print(我是一副帅气的铠甲...)#变身deftransform(*args,**kwargs):print(我要变身喽!)print(我变成钢铁侠了)returnfunc(*args,**kwargs)print(装甲穿好了!!)returntransform
Mark1defTony():我是大名鼎鼎的托尼print(我是斯塔克工业的CEO)Tony()结果:
我是一副帅气的铠甲...装甲穿好了!!我要变身喽!我变成钢铁侠了我是斯塔克工业的CEO
为什么是这个结果呢?那就是使用语法糖装饰Tony之后,代码执行顺序就变成了:
加载Mark1和Tony
Mark1装饰Tony,即调用函数Mark1并将Tony作为参数传到函数中
首先打印:我是一副帅气的铠甲...
然后加载:transform()函数
接下来打印:装甲穿好了!!
最后将transform函数的引用返出去,并将返回值赋值给Tony即:Tony=transform
调用Tony(注意:此时调用的就是transform函数)
打印:
我要变身喽!我变成钢铁侠了我是斯塔克工业的CEO
这三句话.
当然还有有参数的装饰器,类装饰器等.我们就不给大家展开介绍了。最后再补充一点:#上面代码的基础上我们打印查看一下print(Tony.__name__)print(Tony.__doc__)
结果:
transform
None
如果不加装饰器看到的结果是:
Tony
我是大名鼎鼎的托尼
大家发现加上装饰器之后原函数的元信息不见了,比如名字改变,文档改变等。如何解决这个问题呢?
使用functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,这使得装饰器函数也有和原函数一样的元信息了。
在哪里加呢?
fromfunctoolsimportwraps#Mark1为托尼的装甲defMark1(func):print(我是一副帅气的铠甲...)#变身
wrapper(func)deftransform(*args,**kwargs):print(我要变身喽!)print(我变成钢铁侠了)returnfunc(*args,**kwargs)print(装甲穿好了!!)returntransformMark1defTony():我是大名鼎鼎的托尼print(我是斯塔克工业的CEO)Tony()print(Tony.__name__)print(Tony.__doc__)再次打印结果就会是原有的Tony函数的结果了。Tips:但是不是必须加的,如果元信息没用,可以不添加的,根据自己的开发需求了。系列文章第1天:Python环境搭建指南
第2天:PyCharm的安装与使用、变量与数据类型
第3天:Python类型转换、运算符、分支结构
第4天:if语句的多种形式和while循环
第5天:for循环与基础语法综合案例
第6天:一文清晰掌握Python字符串
第7天:Python基础:英雄联盟案例学习List
第8天:史上最全的Python数据类型:元组和集合总结
第9天:Python字典详解
第10天:全网最全Python函数入门使用(多段代码举例)
第11天:Python函数的基本特征详解
-END-Python专栏关于Python都在这里预览时标签不可点收录于话题#个上一篇下一篇转载请注明:http://www.sonphie.com/jbby/14154.html