Python的装饰器

blog 145

装饰器简介

为什么要有装饰器

软件的开发应该遵循开放和关闭原则的,即对可扩展是开放的,而对修改是封闭的。对扩展开放意味着有新的需求或者变化的时候,可以在现有代码的基础上进行扩展,以适应新的需求。对修改封闭意味着对象一旦设计完成,就可以独立的完成工作,而不要对其进行修改。

而软件的调用都应该避免直接修改其源代码,否则一旦报错,则极有可能产生连锁反应,最终导致程序的崩溃,而对于上线后的APP,新需求或者变化层出不穷,我们必须为程序提供可靠的可扩展性。从而就需要使用到了装饰器。

什么是装饰器

“装饰”指的是为某个事物提供新的东西,我们这里指的是为装饰的对象提供新的功能。

而”器“则指的是工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。

装饰器的实现

函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

无参装饰器的实现

定义一个函数bar()

import time  # 导入时间模块

def bar():
	time.sleep(2) # 模拟程序运行需要2s,所以我们这里延时2秒
	print('welcome to bar page')
bar() # 调用函数

如果想在某个函数的基础上,添加该函数的执行时间的功能,那么修改之后为

import time 

def bar():
	start = time.time() # 函数执行的开始时间
	time.sleep(2) 
	print('welcome to bar page')
	stop = time.time() # 函数执行的结束时间
	print('该函数的运行时间为: ',stop - start)
bar() # 调用函数

time.time()返回的是1970年1月1日00:00:00到至今的秒数,我们称之为时间戳。

Python的装饰器

那么的确是实现了需求,打印函数执行需要的时间。但是遵循装饰器的规则,我们不能修改装饰器对象源代码的,也就是不能修改函数内部。那么我们想到的解决方法可能是如下这样的:

修改函数外部的代码

import time 

def bar():
	time.sleep(2)
	print('welcome to bar page')

# 使用函数调用之前的时间 - 调用结束之后的时间 = 函数执行的时间
start = time.time()
bar()
stop = time.time()
print('函数执行需要的时间为:',stop - start)

但是我们又考虑到当前功能可能还会使用到其他函数身上,于是我们将其定义为函数。

import time 

def bar():
	time.sleep(2)
	print('welcome to bar page')

# 将功能封成函数
def wrapper():
	start = time.time()
	bar()
	stop = time.time()
	print('函数执行需要的时间为:',stop - start)

wrapper() # 调用功能函数

考虑到程序的复用性,于是我们将里面的bar()修改为可以控制的值。

import time 

def bar():
	time.sleep(2)
	print('welcome to bar page')

# 将功能封成函数
def wrapper(func): # 接受func参数 func ==> bar函数的内存地址
	start = time.time()
	func()
	stop = time.time()
	print('函数执行需要的时间为:',stop - start)

wrapper(bar) # 调用功能函数 wrapper(bar函数的内存地址)
wrapper(其他函数)

但是我们这里修改了被装饰对象bar()函数的调用规则,于是我们就换成了为函数体传值的方式。即将值包给函数

import time 

def bar():
	time.sleep(2)
	print('welcome to bar page')

def timer(func):	
	def wrapper(): # 接受func参数 func ==> bar函数的内存地址
		start = time.time()
		func()
		stop = time.time()
		print('函数执行需要的时间为:',stop - start)
	return wrapper # 将wrapper函数的内存地址作为timer函数的返回值

demo = timer(bar) # 将wrapper的内存地址赋值给demo变量
demo() # 调用demo()函数就相当与调用wrapper()

到此我们就完成了一个无参装饰器,可以在不修改被装饰对象bar()的源代码和其调用方式的前提下去扩展新的功能,但是我们忽略了若被装饰对象是一个有参函数,则会报错。

import time

def timer(func):
    def wrapper(): # 引用外部作用域的变量func
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

def home(name):
	time.sleep(2)
	print('my name is %s'%name)

home = timer(home)
home('x1ong') # TypeError: wrapper() takes 0 positional arguments but 1 was given

报错的原因是我们调用home('x1ong'),其实就相当于调用wrapper('x1ong'),而wrapper()是一个无参函数,所以会报错。那么我们修改wrapper()为wrapper(*args,**kwargs),为其加上可变长度的任意参数。而调用我们的func()也要加上该参数成为func(*args,**kwargs),那么wrapper()函数接受任意长度的参数了,但是参数的个数要参照func那个函数的参数个数,否则会报错。

import time

def bar():
	time.sleep(2)
	print('from bar')

def home(name):
	time.sleep(2)
	print('my name is %s'%name)

def timer(func):
	# func = 函数的内存地址
	def wrapper(*args,**kwargs):
		start = time.time()
		func(*args,**kwargs)
		stop = time.time()
		print('函数的执行时间为',stop - start)
	return wrapper

wrapper = timer(home)
wrapper('x1ong')

此时我们的wrapper()可以带位置参数或者关键字参数,也可以不带参数。那么问题来了,假设我们home()或者bar()有返回值呢,我们的wrapper()可以正确返回吗?

import time
def home(name):
	time.sleep(2)
	print('my name is %s'%name)
	return "from home" # 设置该函数返回值为 from home

def timer(func):
	# func = 函数的内存地址
	def wrapper(*args,**kwargs):
		start = time.time()
		func(*args,**kwargs)
		stop = time.time()
		print('函数的执行时间为',stop - start)
	return wrapper

wrapper = timer(home)
print(wrapper('x1ong')) # None 返回None

可以看到wrapper()返回为None,为什么返回为None呢,是因为wrapper()函数本身没有设置返回值,所以默认为None

我们这边尝试将wrapper()函数的返回值设置为from wrapper

def home(name):
	time.sleep(2)
	print('my name is %s'%name)
	return "from home"

def timer(func):
	def wrapper(*args,**kwargs):
		start = time.time()
		func(*args,**kwargs)
		stop = time.time()
		print('函数的执行时间为',stop - start)
		return 'from wrapper' # 设置wrapper()函数的返回值
	return wrapper

wrapper = timer(home) 
print(wrapper('x1ong')) # from wrapper

可以看到返回值为from wrapper,那么就可以得知,我们调用wrapper()实则类似于在调用home(),但是返回值确实wrapper()函数本身的,所以我们要将func(*args,**kwargs)的结果(也就是返回值),赋值给变量res,之后将res返回给wrapper()即可。

import time

def bar():
	time.sleep(2)
	print('from bar')
	return "from bar"

def home(name):
	time.sleep(2)
	print('my name is %s'%name)
	return "from home"

def timer(func):
	def wrapper(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs) # 将func(*args,**kwargs)的返回值返回给res变量
		stop = time.time()
		print('函数的执行时间为',stop - start)
		return res # 将函数的返回值作为wrapper()函数的返回值返回
	return wrapper

wrapper = timer(home)
print(wrapper('x1ong'))

那么这样的话,就基本实现了装饰器,之后我们就可以在全局调用wrapper()了,其实Python还为我们提供了更简洁的语法,那就是语法糖的形式。它取代了wrapper = timer(home)的形式。

语法糖的形式非常简单,只需要在被装饰对象的正上方单独一行添加@timer即可。其实就会调用timer()函数,假设我们在home函数的正上方添加了@timer(),那么就类似于wrapper = timer(home),之后我们直接通过home()就可以调用啦。

import time

def timer(func): # 定义
	def wrapper(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs) # 将func(*args,**kwargs)的返回值返回给res变量
		stop = time.time()
		print('函数的执行时间为',stop - start)
		return res # 将函数的返回值作为wrapper()函数的返回值返回
	return wrapper

@timer # 类似于 home = timer(home) 使用
def home(name):
	time.sleep(2)
	print('my name is %s'%name)
	return "from home"

# 直接调用
home('egon') # my name is egon 函数的执行时间为 2.0065903663635254

注意:@timer一定要在定义之后再使用!!!

其实函数本身有很多自带的属性,比如__name____doc____name__可以获取某个函数的函数名,而__doc__可以获取某个函数里面的注释信息。


def bar():
	'''
		这是bar()函数的描述信息
	'''
	print('hello world')

	# 假装有很多代码...

print(bar.__name__) # bar
print(bar.__doc__) # 显示bar()函数内的多行注释

知道了__name____doc__的作用之后,我们再打印被装饰对象(index)的__name____doc__属性返回的值,

import time 
def timer(func):
	# func = 内存地址
	def wrapper(*args,**kwargs):
		'''
			这是wrapper()函数的描述信息
		'''
		start = time.time()
		res = func(*args,**kwargs)
		stop = time.time()
		print('程序的运行时间为',stop-start)
		return res
	return wrapper

@timer  # index=timer(index)
def index(name):
	time.sleep(2)
	print('my name is %s'%name)
	return 'index的返回值'
print(index.__name__)  # 返回的是wrapper
print(index.__doc__) # 返回的是wrapper()函数的多行注释

可以发现index.__name__返回的值实际上是wrapper.__name__的返回值,而__doc__也是。那么我们如何伪装的更像一点呢?答案是为__name____doc__重新赋值。

import time
def timer(func):
	def wrapper(*args,**kwargs):
		'''
			这是wrapper()函数的多行注释
		'''
		start = time.time()
		res = func(*args,**kwargs)
		stop = time.time()
		wrapper.__name__ = func.__name__ # 为其重新赋值
		wrapper.__doc__ = func.__doc__
		print('程序的运行时间为',stop-start) # 为其重新赋值
		return res
	return wrapper

@timer  # index=timer(index)
def index(name):
	'''
		我是index()函数的多行注释
	'''
	time.sleep(2)
	print('my name is %s'%name)
	return 'index的返回值'
index('x1ong') # 调用函数 
print(index.__name__)  # 返回的是index
print(index.__doc__) # 返回的是index()函数的多行注释

但是这个方法比较土,况且函数有很多属性的,我们总不能一一赋值吧,Python为我们提供了functools模块。可以很好的解决了这个问题。

Python的装饰器

下面我们使用模块的方式为其重新赋值。

import time
from functools import wraps # 导入必要的模块
def timer(func):
	@wraps(func) # 加上该语法之后,wrapper()就获得了被装饰对象一样的属性返回值
	def wrapper(*args,**kwargs):
		'''
			这是wrapper()函数的多行注释
		'''
		start = time.time()
		res = func(*args,**kwargs)
		stop = time.time()
		print('程序的运行时间为',stop-start)
		return res
	return wrapper

@timer  # index=timer(index)
def index(name):
	'''
		我是index()函数的多行注释
	'''
	time.sleep(2)
	print('my name is %s'%name)
	return 'index的返回值'
index('x1ong') # 调用函数 
print(index.__name__)
print(index.__doc__)

只需要在wrapper的头顶上加上@wraps(func) ,那么wrapper就获得了被装饰对象一样的属性返回值。比如被装饰对象为index,那么就为@wraps(index),而此时wrapper函数就有了和index函数一样的属性返回值

有参装饰的实现

学习了无参装饰器之后,我们来学习有参装饰器,用其模拟一个登录认证功能。

大概写法就是

def deco(func):
    def wrapper(*args,**kwargs):
        编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
    return wrapper

如果想使用多种不同的认证方式,我们可以从wrapper()函数的里面去改写:

def deco(func):
	def wrapper(*args,**kwargs):
		in_user = input('your user>> ')
		in_passwd = input('your passwd>> ')
		if auth_type == 'file':
			if in_user == 'x1ong' and in_passwd == '123':
				print('来自于file认证')
				res = func(*args,**kwargs)
				return res
			else:
				print('username or password error')
		elif auth_type == 'mysql':
			print('来自于mysql认证')
			res = func(*args,**kwargs)
			return res
		elif auth_type == 'ldap':
			print("来自于ldap认证")
			res = func(*args,**kwargs)
			return res
		else:
			print('无效的auth_type')
	return wrapper

接着就有了如下代码:

def deco(func):
	def wrapper(*args,**kwargs):
		in_user = input('your user>> ')
		in_passwd = input('your passwd>> ')
		if auth_type == 'file':
			if in_user == 'x1ong' and in_passwd == '123':
				print('来自于file认证')
				res = func(*args,**kwargs)
				return res
			else:
				print('username or password error')
		elif auth_type == 'mysql':
			print('来自于mysql认证')
			res = func(*args,**kwargs)
			return res
		elif auth_type == 'ldap':
			print("来自于ldap认证")
			res = func(*args,**kwargs)
			return res
		else:
			print('无效的auth_type')
	return wrapper


# 认证来源于文件
def root(x,y):
	print('from root ————> %s%s'%(x,y))

# 认证来源于mysql
def home(a,b):
	print('from home ————> %s%s'%(a,b))

# 认证来源于ldap
def transfer():
	print('transfer')

# 认证来源于文件调用 root()
# root = deco(root)
# root('test','x1ong')

# 认证来源于mysql调用 home()

# home = deco(home,auth_type='mysql')
# home('test','waaa')

# 认证来源于ldap
transfer = deco(transfer,auth_type='ldap')
transfer()

但是这样不能使用语法糖的方式去使用装饰器。因为我们没办法传入auth_type参数对应的值。假设我们目前使用语法糖去传值

# 认证来源于文件
@auth
def root(x,y):
	print('from root ————> %s%s'%(x,y))
root('test','x1ong') # 调用函数  NameError: name 'auth_type' is not defined

可以发现,缺少auth_type参数。有的伙伴可能想到为auth()函数在定义的时候加上默认参数,但是我们要控制auth_type的参数即认证方式,就不能这么干了。

def auth(func,auth_type='file'):

所以我们还需要在deco()函数外部嵌套另外一个函数用来接收auth_type的值

def auth(auth_type):
    def deco(func):
        def wrapper(*args,**kwargs):
            ...
        return wrapper
    return deco

之后经过演变之后得到如下代码:

def auth(auth_type): # 接受auth_type参数
	def doce(func):
	# auth_type = 内存地址
		def wrapper(*args,**kwargs):
			in_user = input('your user>> ')
			in_passwd = input('your passwd>> ')
			if auth_type == 'file':
				if in_user == 'x1ong' and in_passwd == '123':
					print('来自于file认证')
					res = func(*args,**kwargs)
					return res
				else:
					print('username or password error')
			elif auth_type == 'mysql':
				print('来自于mysql认证')
				res = func(*args,**kwargs)
				return res
			elif auth_type == 'ldap':
				print("来自于ldap认证")
				res = func(*args,**kwargs)
				return res
			else:
				print('无效的auth_type')
		return wrapper
	return doce


# 认证来源于文件
@auth(auth_type='mysql') # 传入auth_type的值
def root(x,y): 
	print('from root ————> %s%s'%(x,y))
root('test','x1ong')
# 认证来源于mysql
def home(a,b):
	print('from home ————> %s%s'%(a,b))

# 认证来源于ldap
def transfer():
	print('transfer')

叠加多个装饰器分析

def deco1(func1):    # func = wrapper2的内存地址
    def wrapper1(*args,**kwargs):
        print('from wrapper1>>>')
        res1 = func1(*args,**kwargs)
        return res1
    return wrapper1


def deco2(func2):  # func2 = wrapper3的内存地址
    def wrapper2(*args,**kwargs):
        print('from wrapper2>>>')
        res2 = func2(*args,**kwargs)
        return res2
    return wrapper2

def deco3(z):
    def outter3(func3): # func3 = 被装饰对象index的内存地址
        def wrapper3(*args,**kwargs): 
            print('from wrapper3>>>')
            res3 = func3(*args,**kwargs)
            return res3
        return wrapper3
    return outter3

# 加载顺序自下而上
@deco1 # index = deco1(wrapper2的内存地址) ====>  index = wrapper1的内存地址
@deco2 # index = deco2(wrapper3的内存地址) =====> index = wrapper2的内存地址 
@deco3(1) # @outter ===> index = ouuter(index的内存地址) =====>  index = wrapper3的内存地址
def index(x,y):
    print('from index %s %s' %(x,y))

# 执行顺序 自上而下 wrapper1 > wrapper2 > wrapper 3 
index(1,2) 

'''
运行结果:

from wrapper1>>>
from wrapper2>>>
from wrapper3>>>
from index 1 2
'''
Python的装饰器

分享