人海茫茫
相识真好

决战Python之巅(十二)

前言

从这一章开始我们就要开始学习进阶的函数知识。
函数中比较重要的就是装饰器、迭代器、生成器这三样,我将分开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()):
决战Python之巅(十二)
即现在变量f就是func2的内存地址,也就是说 f = func2,那我们执行f(),那是不是就相当于执行了func2()?
这里有的同学可能会问:func2()不是嵌套在func1()里的嘛,外面怎么可以调用?
真的是这样的嘛?我们试一下…
决战Python之巅(十二)
可以看到,我们不仅成功的“执行”了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)即可。
决战Python之巅(十二)
这样看起来可以,但你想一想,万一哪天你写了几百万条代码,里面有几百万个地方调用了这个print_msg(),那你是不是得将这几百万个修改为login(print_msg)。不太现实吧?
你有可能会这么想,那我直接这样print_msg = login(print_msg),这样不就不需要改了吗?还是print_msg(),但功能已经扩展了…
没错,但这样又有一个新问题:
决战Python之巅(十二)
我还没有执行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里面的代码。
决战Python之巅(十二)
这时候,再结合上面的知识,我们知道现在print_msg = login(print_msg)等号左边的print_msg其实就相当于login()里面的inner,我们执行print_msg()就相当于执行inner()。我们可以看一下效果:
决战Python之巅(十二)
还可以简单一点,将print_msg = login(print_msg)这句话换成@login加在def print_msg的上面:
决战Python之巅(十二)
这个@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
决战Python之巅(十二)
没错吧~今天搞了一天才明白的- -不容易啊…
还是不懂的童鞋,可以敲一敲代码,然后每句代码前都打个断点,一步一步看执行顺序。

赞(0) 打赏
未经允许不得转载:老康的学习空间 » 决战Python之巅(十二)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏