前言
从这一章开始我们就要开始学习进阶的函数知识。
函数中比较重要的就是装饰器、迭代器、生成器这三样,我将分开3篇介绍。
知识回顾
装饰器
在讲装饰器之前,我们先讲一点补充知识。
命名空间
名称到对象的映射。命名空间是一个字典的实现,键为变量名,值是变量对应的值。 各个命名空间是独立没有关系的,一个命名空间中不能有重名,但是不同的命名空间可以重名而没有任何影响。
在我们定义了一个变量并且赋值之后,x = 1
,我们知道,1是存在内存地址的,那么x和x与1的映射关系存在哪呢?诶,就是存在命名空间的。在命名空间中,是以字典的形式存放变量名与它的值,即{x:1234}
(假设1的内存地址是1234)。还记得我们之前讲的作用域嘛?局部变量只有在当前定义它的函数的作用域内可以被调用,在外部是不能调用的,限制这个的因素就是因为 局部变量只存放在当前函数的命名空间内,而在外面的函数或者主程序的命名空间中是没有的。…大致了解一下可以了,与作用域差不多感觉。
作用域的查找顺序
之前我们也讲过,作用域的查找顺序是从最里面一层开始找,一层一层往外面找,最后找全局的作用域。那么,准确的,或者学术一点解释就是:遵循LEGB的顺序。L:local,即局部作用域;E:enclosing,即上一层作用域;G:global,即全局作用域;B:builtins,即内置模块的作用域。
闭包
函数中我们学过高阶函数,高阶函数必须具备的条件是以函数作为参数传入,或者返回一个函数。这里我们主要看后面一个条件:返回一个函数:
def func1(msg): def func2(): print(msg) return func2 f = func1('helloworld') print(f)
如果我们这里执行func1,那么得到的就将是func2的内存地址(因为func1()返回的是func2,而不是func2()):
即现在变量f就是func2的内存地址,也就是说 f = func2,那我们执行f(),那是不是就相当于执行了func2()?
这里有的同学可能会问:func2()不是嵌套在func1()里的嘛,外面怎么可以调用?
真的是这样的嘛?我们试一下…
可以看到,我们不仅成功的“执行”了func2(),甚至还用了func1()里的变量值。
这就叫做闭包,官方解释是:
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。—— 维基百科
也就是说,这里的func2夹带了func1的变量。你可以理解为,func1中的变量包装在func2外面,若果你想访问func2就先得访问外面那层包装。
装饰器
前面讲了那么多,那装饰器到底是什么呢?
这里我们先提供一个函数:
def print_msg(): print('Hello world')
如果有一天你想扩展这个函数,或者说给这个函数加点新功能,你会怎么办?
有的同学会说:直接改源代码啊。
没错这是一个方法,但是根据开发的“扩展开放,修改关闭”原则,只允许你扩展而不能修改,怎么办呢?
或许你会想到高阶函数,将这个函数作为一个参数传到我需要新增功能的函数里就行:
def login(func): NAME = 'Kris' PWD = '123' name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func() else: print('用户名或密码错误')
这样你只需要执行 login(print_msg)即可。
这样看起来可以,但你想一想,万一哪天你写了几百万条代码,里面有几百万个地方调用了这个print_msg(),那你是不是得将这几百万个修改为login(print_msg)
。不太现实吧?
你有可能会这么想,那我直接这样print_msg = login(print_msg)
,这样不就不需要改了吗?还是print_msg(),但功能已经扩展了…
没错,但这样又有一个新问题:
我还没有执行print_msg()这个函数,它就自己执行login()了。Why?因为赋值操作是从右往左的,就是会先将右边的结果算出来,再传给左边。这样也不行
那怎么办?结合上面讲的闭包,我们可以这样做:
def login(func): def inner(): NAME = 'Kris' PWD = '123' name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func() else: print('用户名或密码错误') return inner
这时候,当我们运行print_msg = login(print_msg)时,左边的print_msg得到的仅仅是inner的内存地址,但并没有执行inner里面的代码。
这时候,再结合上面的知识,我们知道现在print_msg = login(print_msg)
等号左边的print_msg
其实就相当于login()里面的inner
,我们执行print_msg()
就相当于执行inner()
。我们可以看一下效果:
还可以简单一点,将print_msg = login(print_msg)
这句话换成@login加在def print_msg的上面:
这个@login就是一个装饰器。
2018.12.13补充
带参数的装饰器
上面我们只是简单的实现了一个装饰器。在实际开发中,我们开发的代码往往都是带有参数的,那么这种函数怎么加装饰器呢?假如我现在有下面这段代码:
def print_msg(msg): print(msg)
我现在的需求还是想加一个验证登录的功能,怎么办?
原装饰器代码: def login(func): def inner(): NAME = 'Kris' PWD = '123' name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func() else: print('用户名或密码错误') return inner
我们来重新分析下,当我们在函数定义前加上@login,是不是就相当于执行了xx= login(xx)
(xx是@login下面定义的函数名),而这句代码最终返回的是inner函数
,即xx = inner
,之前的操作是接下来直接调用inner函数inner()
,也就是xx()
,即xx()= inner()
。但是现在,我让xx=print_msg,那是否就意味着print_msg = inner
,进而print_msg(msg)= inner(msg)
,所以我们只需要在定义inner的时候,给inner加一个变量即可:
新装饰器代码: def login(func): def inner(arg): # 新增一个arg变量 NAME = 'Kris' PWD = '123' name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func(arg) else: print('用户名或密码错误') return inner
这样就能解决需要装饰的函数带参数的问题。…然而,事情没那么简单,不知你是否发现,现在的这个装饰器只能装饰一种类型的函数即只有一个参数的函数,但实际生活中我们往往需要它可以装饰不限量的参数的函数,这可怎么办呢?
不知你是否还记得,我们将函数参数的时候讲过一种特殊的参数,非固定参数,*args,**kwargs,这里我们就可以用到它俩:
改进后装饰器代码: def login(func): def inner(*arg,**kwargs): # 新增一个arg变量 NAME = 'Kris' PWD = '123' name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func(*arg,**kwargs) #注意这里也需要使用非固定参数,否则会变成位置参数 else: print('用户名或密码错误') return inner
这样你装饰不带参数的函数或者多个参数的函数就都没问题~
可以思考下为何func(*args,**kwargs)必须要使用非固定参数~
在作死的道路上继续前行…
来来来,都别走,接下来还有。我们已经实现了带参数的装饰器,那么我现在又有一个新的需求(别打我- -要打别打脸,毕竟靠脸吃饭),我现在需要增加login的功能,就是可以让用户选择登录模式,比如QQ/WeiXin/WeiBo这三种,平常写这种可能很容易实现:
def login_with_type(type): if type == 'QQ': #QQ登录代码 elif type == 'WeiXin': #WeiXin登录代码 else: #WeiBo登录代码
那么现在我需要加在装饰器里,怎么操作呢?
有的童鞋可能说:我在写个装饰器呗,再加一层装饰呗…这个我们日后再说。我现在想直接改login的代码,怎么实现呢?
定义login再加一个参数?def login(type,func)
?不行,因为这样定义就无法使用@语句,
@login(‘QQ’,print_msg)这是个错误的语法,Python 3里根本就没有~那可怎么办呢?我们又不能直接给login传type,type传进去是个字符串,到最后执行字符串()肯定会报错。
好了…我直接说吧,这里再加一层,看代码:
二改后装饰器代码: def login(type): def outer(func): def inner(*arg,**kwargs): if type == 'QQ': #QQ登录代码 elif type == 'WeiXin': #WeiXin登录代码 else: #WeiBo登录代码 name = input('输入用户名') pwd = input('输入密码') if name == NAME and pwd == PWD: func(*arg,**kwargs) else: print('用户名或密码错误') return inner return outer
现在我们再装饰函数的时候,可以直接在函数定义的上面写:@login(‘QQ’)。
我来分析下具体过程:@login(‘QQ’)首先执行的是login(‘QQ’),返回的是outer函数,注意不是调用,即login('QQ') = outer
,这时候再执行@语法,也就是@outer,很熟悉吧,这样又回到了最开始最简单的装饰器,此时outer自动把下面定义的函数名作为参数传进去,开始后面的事情。
永攀作死的高峰…
现在我们来讨论下,如果,一个函数被多个装饰器装饰,是什么执行顺序呢?
def dec_one(func): print('1111') def inner_one(): print('aaaa') func() #三 return inner_one def dec_two(func): print('2222') def inner_two(): print('bbbb') func() #四 return inner_two @dec_one @dec_two def print_msg(): #二 print('test') print_msg() #一
…最终打印的顺序是啥?
首先我们要将一个原则,就是自下而上的原则,即当被多个装饰器装饰的时候,离它最近的先上,然后由近到远依次装饰。
那么,这里一处的print_msg
函数,就相当于dec_one(dec_two(print_msg)) #这里的print_msg是二处的
,执行dec_one(dec_two(print_msg))()
,首先执行dec_two(print_msg)
,进入dec_two
,先打印2222
,然后返回inner_two
函数,即dec_two(print_msg) = inner_two
,然后执行dec_one(inner_two)
,先打印1111
,然后返回inner_one
,这里注意dec_one
在最外层,所以到这里dec_one(dec_two(print_msg))()
就变成inner_one()
,而inner_one()
内三处的func
变成了inner_two
,所以,执行inner_one()
,先打印aaaa
,然后执行inner_two()
,打印bbbb
,四处的func是定义的print_msg
,最后打印一句test
。
所以最终结果是:2222 1111 aaaa bbbb test
没错吧~今天搞了一天才明白的- -不容易啊…
还是不懂的童鞋,可以敲一敲代码,然后每句代码前都打个断点,一步一步看执行顺序。