函数进阶-(命名空间、作用域、函数嵌套、作用域链、函数名本质、闭包)
函数的进阶
-
由来
-
命名空间
-
函数作用域
-
函数嵌套
-
作用域链
-
函数名本质(函数对象)
-
闭包函数
一、由来
首先我们先来看一个代码:
>>> def zjk_max(): ... a = 1 ... b = 2 ... print(a,b) ... >>> zjk_max() 1 2 >>> print(a,b) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'a' is not defined
什么鬼?竟然报错了,错误是变量a没有被定义,可是我明明定义了a量等于1啊,这是咋回事呢。
那么这里我们要捋一下python的代码在运行中遇到函数时是怎么做的。
#1.当python解释器开始执行之后,就在内存中开辟了一个空间; #2.每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来; #3.但是,当遇到函数定义的时候,解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心; #4.等执行到函数调用时,python解释器会再开辟一块内存来存储这个函数里的内容,这个时候才关注函数里有哪些变量,而函数中的变量会存储在新开辟的内存中,函数中的变量只能在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有的内容也会被清空;
我们给这个“存放名字和值的关系的空间起了一个名称”--------叫做命名空间;
代码在运行伊始,创建的存储“变量名与值的关系”的空间叫做全局命名空间,在函数的运行中开辟的临时的空间叫做局部命名空间
二、命名空间
1.定义:
命名空间就是存在着名字和值绑定关系的地方;
python之禅中说过:命名空间是一种绝妙的理念,让我们尽情的使用发挥吧;
2.种类(命名空间有三种):
a.内置命名空间 -----> (python解释器)
就是python解释器一启动就可以使用的名字存储在内置命名空间中;
内置的名字在启动解释器的时候被加载进内存里,比如:input、print、list、tuple等等;
b.全局命名空间 ------> (就是我们写的代码但不是函数中的代码)
是在程序从上到下被执行的过程中依次加载进内存的;
放置了我们设置的所有变量名和函数名
c.局部命名空间 -------> (函数)
就是函数内部定义的名字;
当调用函数的时候,才会产生这个名称空间,随着函数执行的结束,这个命名空间就又消失了;
说明:
#1.在局部:可以使用全局、内置命名空间中的名字; #2.在全局:可以使用内置命名空间中的名字,但是不能使用局部中的名字;因为局部的代码随之函数执行完毕后就被清除了; #3.在内置,不能使用局部和全局的名字;启动的时候就加载了,当然不会使用局部和全局了,那时的代码都还没有加载呢,何谈调用
注意:
#1.在正常的情况下,直接使用内置的名字; #2.当我们在全局定义了和内置名字空间中同名的名字时,会使用全局的名字,这是因为,当我这层空间有了我想要的变量时,我就不会再向上级去寻要了,如果我自己当前这个空间没有,那么我就要找我上一层空间去要,上一层没有的话去上上层要,如果内置空间里名字都不存在,就会报错; #3.多个函数拥有多个独立的局部名字空间,不相互共享;
3.三种命名空间之间的加载与取值顺序:
加载顺序(从小到大):内置命名空间(程序运行前加载) --------> 全局命名空间(程序运行中:从上到下加载) --------> 局部命名空间(程序运行中:调用时才加载)
取值:
在局部调用:先检查局部内有没有所需要的,如果局部没有则去上一级(全局命名空间)查找,如果上一级还没有则去内置命名空间查找;
在全局调用:首先在全局命名空间查找,如果没有则去内置命名空间查找;
4.这三个种类的图解:
三、函数作用域
1.定义:
作用域就是作用范围,按照生效范围可以分为全局作用域和局部作用域;
全局作用域:包含内置命名空间、全局命名空间,在整个文件的任意位置都能被引用、全局有效;
局部作用域:局部命名空间,只能在局部范围内生效;
2.查看全局作用域和局部作用域的两个方法:
globals和locals:
globals:查看全局的作用域,以字典的形式将全局变量显示出来;
locals:查看当前的作用域,以字典的形式将局部变量显示出来;
例如:
#-----------------在局部命名空间查看 globas 和 locals ----------------- >>> name = "python" >>> def cat(): ... a = 1 ... b = 2 ... c = "zjk" ... print(locals()) ... print(globals()) ... >>> cat() {'a': 1, 'b': 2, 'c': 'zjk'} {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'cat': <function cat at 0x7ff74ba79268>, 'name': 'python'} #------------------在全局命名空间查看 globas 和 locals ----------------- >>> a,b,c = "ABC" >>> print(globals());print(locals()) {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 'A', 'b': 'B', 'c': 'C'} {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 'A', 'b': 'B', 'c': 'C'} #我们会发现在全局命名空间中,它的globs 和locals是一样的,这点大家都可以想清楚吧。 #总结: #globals 永远打印全局的名字; #locals 根据locals所在的位置进行输出;
还有一点需要大家注意:
对于变量为不可变数据类型来说, 在局部是可以查看全局作用域中的不可变类型的变量,但是不能直接修改;如果想要修改,需要在程序的一开始添加global声明;如果在一个局部(函数)内声明了一个global变量,那么这个变量在局部的所有操作将对全局的变量有效。
为什么局部不能直接修改全局中的变量?
答:如果内部函数有引用外部函数的同名变量或者全局变量,并且对这个变量有修改.那么python会认为它是一个局部变量,又因为内部函数_func中没有x的定义,所以报错。
例如:
#------------- 我们尝试在局部变量中对全局命名空间的一个不可变类型的a变量进行修改 ------------------------------- >>> a = "zjk" >>> def func(): ... a+="_china" ... >>> func() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in func UnboundLocalError: local variable 'a' referenced before assignment >>> print(a) zjk #可以发现在运行函数时直接报错了,并且全局命名空间的a变量并没有被修改; #------------- 我们再尝试在局部变量中对全局命名空间的一个可变类型的b变量进行修改 ------------------------------- >>> b = ["A","B","C"] >>> def func1(): ... b.append("D") ... >>> func1() >>> print(b) ['A', 'B', 'C', 'D'] #可以发现,对于可变的全局命名空间里的变量,在局部命名空间中是可以被修改的; #那么如果在局部命名空间对去全局命名空间中的不可变类型的变量进行修改呢?我们可以使用global关键字来修改; #------------使用 global关键字修改不可变类型的全局命名空间变量 -------------- >>> a = "zjk" >>> def func2(): ... global a ... a+="_china" ... >>> func2() >>> print(a) zjk_china #可以发现我们成功的对全局命名空间的不可变类型的变量进行了修改,但是这种在局部变量中通过global进行修改全局变量,这样的代码是不安全的,以后我们会说一下其他方法;
3.global关键字、nonlocal关键字:
global:
a.声明一个全局变量;
b.在局部作用域想要对全局作用域的全局变量进行修改时,需要用到global(限于字符串、数字,也就是不可变类型);
def func(): global a a = 3 func() print(a) count = 1 def search(): global count count = 2 search() print(count) global关键字举例
注意:对可变数据类型(list,dict,set)可以直接引用不用通过global;这点我们在上面也强调过了,在此不再举例;
nonlocal:
a.不能修改全局变量;
b.在局部作用域中,对父级作用域(或者更外层作用域非全局作用域)的变量进行引用和修改,并且引用的哪层,从那层及以下此变量全部发生改变;
def add_b(): b = 42 def do_global(): b = 10 print(b) def dd_nonlocal(): nonlocal b b = b + 20 print(b) dd_nonlocal() print(b) do_global() print(b) add_b() ----------------------显示结果------------------------------------- 10 30 30 42
四、函数嵌套
1.函数的嵌套定义、作用:
函数嵌套就是在函数里再调用或添加一个或多个函数;
函数嵌套可以将一个大的功能细化成各种小的函数功能并调用;
def max2(x,y): m = x if x>y else y return m def max4(a,b,c,d): res1 = max2(a,b) res2 = max2(res1,c) res3 = max2(res2,d) return res3 # max4(23,-7,31,11) 函数的嵌套调用
内部函数可以使用外部函数变量;
def f1(): print("in f1") def f2(): print("in f2") f2() f1()
def f1(): def f2(): def f3(): print("in f3") print("in f2") f3() print("in f1") f2() f1()
五、函数作用域链
def f1(): a = 1 def f2(): print(a) f2() f1()
def f1(): a = 1 def f2(): def f3(): print(a) f3() f2() f1()
def f1(): a = 1 def f2(): a = 2 f2() print('a in f1 : ',a) f1()
六、函数名本质(函数对象)
函数被称为第一类对象,函数可以被当做数据传递,函数名本质上就是函数的内存地址;
1.函数可以被赋值:
直接输出函数名的值,就是函数在内存中的地址;
>>> def func(): ... print("func") ... >>> print(func) <function func at 0x7f4202e9a578>
函数可以被赋值:将函数名代码的值赋值给变量;
>>> def func(): ... print("zjk") ... >>> f = func >>> print(func,f);f() <function func at 0x7f36009551e0> <function func at 0x7f36009551e0> zjk
2.函数可以作为参数传递:
函数可以作为参数传递
>>> def foo(): ... print("foo") ... >>> def bar(func): ... print(func) ... func() ... >>> bar(foo) <function foo at 0x7f36008561e0> foo
3.函数可以作为返回值:
函数可以作为函数的返回值
>>> def foo(): ... print("zjk") ... >>> def bar(func): ... print(func) ... func() ... return func ... >>> f = bar(foo);print(f);f() <function foo at 0x7f3600856400> zjk <function foo at 0x7f3600856400> zjk
4.函数可以作为容器类型的元素:
函数作为字典的键的值:
def add(): print("=======>function add") def search(): print("=======>function search") def delete(): print("=======>function delete") def change(): print("=======>function change") def tell_msg(): msg=''' search:查询 add:添加 delete:删除 change:修改 create:新建 ''' print(msg) def create(): print('=======>function create') cmd_dic={ 'search':search, 'add':add, 'delete':delete, 'change':change, 'create':create } while True: tell_msg() choice=input("please input your choice:") cmd_dic[choice]() ------------------------- 输出打印 ------------------------------------- E:\python\venv\Scripts\python3.exe E:/python/s15/day10.py search:查询 add:添加 delete:删除 change:修改 create:新建 please input your choice:search =======>function search search:查询 add:添加 delete:删除 change:修改 create:新建 please input your choice:add =======>function add ..............
七、闭包函数
1.闭包函数的定义:
简单说闭包函数满足两个条件就可以称为闭包函数;
条件一:必须是嵌套函数;
条件二:内部函数调用上层内部函数的变量
正规定义:内部函数包含对外部作用域而非全局作用域名字的引用,该内部函数称为闭包函数;
举个例子:
>>> def outer(): ... a = 1 ... def inner(): ... print(a) ... inner() ... #inner就是一个闭包函数,它满足两个条件,首先它属于嵌套函数;再一个就是它调用了上层的内部函数的变量; #怎么判断一个函数是不是闭包函数,有没有什么方法? #通过内置方法来判断一个函数是否是闭包函数; >>> def outer(): ... a = 1 ... def inner(): ... print(a) ... inner() ... print(inner.__closure__) ... >>> outer() 1 (<cell at 0x7fa062f85c78: int object at 0x7fa062e6a540>,) #如果打印一个函数的__closure__方法,显示的是cell开头的话,说明这个函数是一个闭包函数;
2.闭包的基本形式:
在函数F1中,定义F2,F2只能引用F1定义的变量,之后F1函数返回F2的函数名;
def F1(): a = 1 def F2(): print(a) return F2
3.闭包的意义、作用:
返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域,自带状态即变量,可以不用传参就用,方便。
作用:a.可以保护我们的变量不受侵害;b.可以让一个变量常驻内存(在爬取数据时就体现出闭包变量永驻的好处了);
4.一个简答的闭包例子:
>>> def F1(): ... x = 1 ... def F2(): ... print(x) ... return F2 ... >>> F = F1() >>> print(F) <function F1.<locals>.F2 at 0x7f124ceb2268> >>> F() 1
5.闭包的__closure__变量
闭包都有__closure__属性;
__closure__对象会返回闭包应用外围作用域的变量信息。f.__closure__保存外围作用域的变量内存地址,f.__closure__[0].cell_contents存放的是外围作用域的变量的值。
对于那些不是闭包的函数对象来说,__closure__ 属性值为 None;
6.闭包的应用:
from urllib.request import urlopen def index(): url = "http://www.xiaohua100.cn/index.html" def get(): return urlopen(url).read() return get xiaohua = index() content = xiaohua() print(content)
7.如何正确使用闭包
def outer(): a = 10 def inner(): print(a) return inner outer()() outer()() #这种方法不行,因为每次执行inner函数时都要先执行outer函数,这不是闭包要的效果; ret = outer() ret() ret() #这种方法可以,只有第一次执行时需要执行outer函数,然后会获取到inner函数的地址并赋值给ret,下次执行ret时直接就是执行的inner函数,不再执行outer函数了,这才是真正意义上的闭包;