Python入门篇-装饰器

时间:2023-03-09 08:51:41
Python入门篇-装饰器

              Python入门篇-装饰器

                                      作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.装饰器概述

装饰器(无参)
  它是一个函数
  函数作为它的形参
  返回值也是一个函数
  可以使用@functionname方式,简化调用 装饰器和高阶函数
  装饰器是高阶函数,但装饰器是对传入函数的功能的装饰(功能增强) 带参装饰器
  它是一个函数
  函数作为它的形参
  返回值是一个不带参的装饰器函数
  使用@functionname(参数列表)方式调用
  可以看做在装饰器外层又加了一层函数

二.为什么要用装饰器

1>.在不是用装饰器的情况下,给某个函数添加功能

在解释为什么使用装饰器之前,完美来看一个需求:
  一个加法函数,想增强它的功能,能够输出被调用过以及调用的参数信息
    def add(x, y):
      return x + y
  增加信息输出功能:
    def add(x, y):
      print("call add, x + y") # 日志输出到控制台
      return x + y
  上面的加法函数是完成了需求,但是有以下的缺点
    打印语句的耦合太高,换句话说,我们不推荐去修改初始的add函数原始代码。
    加法函数属于业务功能,而输出信息的功能,属于非业务功能代码,不该放在业务函数加法中

2>.使用高阶函数给某个函数添加功能

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com def add(x,y):
return x + y def logger(func):
print('begin') # 增强的输出
f = func(4,5)
print('end') # 增强的功能
return f print(logger(add)) #以上代码输出结果如下:
begin
end
9

3>.解决了传参的问题,进一步改变

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com def add(x,y):
return x + y def logger(func,*args,**kwargs):
print('begin') # 增强的输出
f = func(*args,**kwargs)
print('end') # 增强的功能
return f print(logger(add,5,y=60)) #以上代码输出结果如下:
begin
end
65

4>.柯里化实现add函数功能增强

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com def add(x,y):
return x + y def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper # print(logger(add)(5,y=50)) #海航代码等价于下面两行代码,只是换了一种写法而已
add = logger(add)
print(add(x=5, y=10)) #以上代码输出结果如下:
begin
end
15

5>.装饰器语法糖

#!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com """
定义一个装饰器
"""
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper @logger # 等价于add = logger(add),这就是装饰器语法
def add(x,y):
return x + y print(add(45,40)) #以上代码输出结果如下:
begin
end
85

三.帮助文档之文档字符串

1>.定义python的文档字符串

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com """
Python的文档
Python是文档字符串Documentation Strings
在函数语句块的第一行,且习惯是多行的文本,所以多使用三引号
惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
可以使用特殊属性__doc__访问这个文档
""" def add(x,y):
"""This is a function of addition"""
a = x+y
return x + y print("name = {}\ndoc = {}".format(add.__name__, add.__doc__)) print(help(add)) #以上代码执行结果如下:
name = add
doc = This is a function of addition
Help on function add in module __main__: add(x, y)
This is a function of addition None

2>.装饰器的副作用

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper @logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y print("name = {}\ndoc= {}".format(add.__name__, add.__doc__)) #原函数对象的属性都被替换了,而使用装饰器,我们的需求是查看被封装函数的属性,如何解决? #以上代码执行结果如下:
name = wrapper
doc= I am wrapper

3>.提供一个函数,被封装函数属性==copy==> 包装函数属性

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com """
通过copy_properties函数将被包装函数的属性覆盖掉包装函数
凡是被装饰的函数都需要复制这些属性,这个函数很通用
可以将复制属性的函数构建成装饰器函数,带参装饰器
"""
def copy_properties(src, dst): # 可以改造成装饰器
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__ def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
copy_properties(fn, wrapper)
return wrapper @logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y print("name = {}\ndoc = {}".format(add.__name__, add.__doc__)) #以上代码执行结果如下:
name = add
doc = This is a function for add

4>.提供一个函数,被封装函数属性==copy==> 包装函数属性,改造成带参装饰器

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com def copy_properties(src): # 柯里化
def _copy(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy def logger(fn):
@copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper @logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y print("name = {}\ndoc = {}".format(add.__name__, add.__doc__)) #以上代码执行结果如下:
name = add
doc = This is a function for add

5>.使用Python提供的wrap装饰器修改被装饰的doc信息 

 #!/usr/bin/env python
#_*_conding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie from functools import wraps def logger(fn): @wraps(fn) #其实查看wraps源码是利用update_wrapper实现的(需要有偏函数知识),但是实际开发中我们推荐使用wraps装饰去。
def wrapper(*args,**kwargs):
'''This is a function for wrapper'''
ret = fn(*args,**kwargs)
return ret
return wrapper @logger
def add(x,y):
'''This is a function for add'''
return x + y print("name = {}\ndoc = {}".format(add.__name__, add.__doc__)) #以上代码执行结果如下:
name = add
doc = This is a function for add

四.装饰器案例

1>.无参装饰器

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com import datetime
import time """
定义一个装饰器
"""
def logger(fn):
def wrap(*args, **kwargs):
# before 功能增强
print("args={}, kwargs={}".format(args,kwargs))
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
# after 功能增强
duration = datetime.datetime.now() - start
print("function {} took {}s.".format(fn.__name__, duration.total_seconds()))
return ret
return wrap @logger # 相当于add = logger(add),调用装饰器
def add(x, y):
print("===call add===========")
time.sleep(2)
return x + y print(add(4, y=7)) #以上代码输出结果如下:
args=(4,), kwargs={'y': 7}
===call add===========
function add took 2.000114s.
11

2>.有参装饰器

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com import datetime,time def copy_properties(src): # 柯里化
def _copy(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy """
定义装饰器:
获取函数的执行时长,对时长超过阈值的函数记录一下
"""
def logger(duration):
def _logger(fn):
@copy_properties(fn) # wrapper = wrapper(fn)(wrapper)
def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
print('so slow') if delta > duration else print('so fast')
return ret
return wrapper
return _logger @logger(5) # add = logger(5)(add)
def add(x,y):
time.sleep(3)
return x + y print(add(5, 6)) #以上代码执行结果如下:
so fast
11
 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com import datetime,time def copy_properties(src): # 柯里化
def _copy(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy """
定义装饰器:
获取函数的执行时长,对时长超过阈值的函数记录一下
"""
def logger(duration, func=lambda name, duration: print('{} took {}s'.format(name, duration))):
def _logger(fn):
@copy_properties(fn) # wrapper = wrapper(fn)(wrapper)
def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
if delta > duration:
func(fn.__name__, duration)
return ret
return wrapper
return _logger @logger(5) # add = logger(5)(add)
def add(x,y):
time.sleep(3)
return x + y print(add(5, 6)) #以上代码输出结果如下:
11

将记录的功能提取出来,这样就可以通过外部提供的函数来灵活的控制输出

五.functools模块

1>.functools概述

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)  
  类似copy_properties功能
  wrapper 包装函数、被更新者,wrapped 被包装函数、数据源
  元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性'__module__', '__name__', '__qualname__', '__doc__', '__annotations__'模块名、名称、限定名、文档、参数注解
  元组WRAPPER_UPDATES中是要被更新的属性,__dict__属性字典
  增加一个__wrapped__属性,保留着wrapped函数

2>.functools模块案例

 #!/usr/bin/env python
#_*_coding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie/tag/python%E8%87%AA%E5%8A%A8%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B9%8B%E8%B7%AF/
#EMAIL:y1053419035@qq.com import datetime, time, functools def logger(duration, func=lambda name, duration: print('{} took {}s'.format(name, duration))):
def _logger(fn):
@functools.wraps(fn)
def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
if delta > duration:
func(fn.__name__, duration)
return ret
return wrapper
return _logger @logger(5) # add = logger(5)(add)
def add(x,y):
time.sleep(1)
return x + y print(add(5, 6), add.__name__, add.__wrapped__, add.__dict__, sep='\n') #以上代码执行结果如下:
11
add
<function add at 0x0000000002A0F378>
{'__wrapped__': <function add at 0x0000000002A0F378>}

六.小试牛刀

1>.实现Base64编码(要求自己实现算法,不用库)

 #!/usr/bin/env python#_*_conding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie from string import ascii_lowercase, ascii_uppercase, digits
import base64 bytesBase64 = (ascii_uppercase + ascii_lowercase + digits + "+/").encode("utf-8") #注意,我们这里拿到的是一个字节序列哟~ def base64encode(src:str,code_type="utf-8") -> bytes:
res = bytearray() if isinstance(src,str):
_src = src.encode(code_type)
elif isinstance(src,bytes):
_src = src
else:
raise TypeError length = len(_src) for offset in range(0,length,3):
triple = _src[offset:offset+3] #切片可以越界 r = 3 - len(triple) if r:
triple += b'\x00' * r #便于计算先补零,即传入的字符串转换成字节后不足3个字节就补零 """
bytes和bytearray都是按照字节操作的,需要转换为整数才能进行位运算,将3个字节看成一个整体转成字节bytes,
使用大端模式,如"abc => 0x616263"
"""
b = int.from_bytes(triple,'big') for i in range(18,-1,-6):
index = b >> i if i == 18 else b >> i & 0x3F #注意,十六进制0x3F使用而二进制表示为: "11 1111"
res.append(bytesBase64[index]) if r:
res[-r:] = b'=' * r #替换等号,从索引-r到末尾使用右边的多个元素依次替换 return bytes(res) if __name__ == '__main__':
testList = ["a", "`", "ab", "abc", "jason", "yinzhengjie", "尹正杰2019"]
for item in testList:
print(item)
print("自定义的base64编码:{}".format(base64encode(item)))
print("使用base64标准库编码:{}".format(base64.b64encode(item.encode())))
print("*" * 50)

参考案例

2>.实现一个cache装饰器,实现可过期被清楚的功能

缓存的应用场景:
  有数据需要频繁使用。
  获取数据代价高,即每次获取都需要大量或者较长等待时间。
使用缓存来提高查询速度,用内存空间换取查询,加载时间。cache的应用极广,比如硬件CPU的一级,二级缓存,硬盘自带的缓存空间,软件的redies,varnish集群缓存软件等等。
 #!/usr/bin/env python
#_*_conding:utf-8_*_
#@author :yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie from functools import wraps
import time,inspect,datetime """
缓存装饰器实现
"""
def jason_cache(duration):
def _cache(fn):
local_cache = {} # 对不同函数名是不同的cache
@wraps(fn)
def wrapper(*args, **kwargs):
# 使用缓存时才清除过期的key
expire_keys = []
for k, (_, stamp) in local_cache.items():
now = datetime.datetime.now().timestamp()
if now - stamp > duration:
expire_keys.append(k) for k in expire_keys:
local_cache.pop(k) sig = inspect.signature(fn)
params = sig.parameters # 参数处理,构建key,获取一个只读的有序字典
target = {} # 目标参数字典 """
param_name = [key for key in params.keys()]
#位置参数
for i,v in enumerate(args):
k = param_name[i]
target[k] = v
target.update(zip(params.keys(),args)) #关键词参数
for k,v in kwargs.items():
target[k] = v
target.update(kwargs)
"""
# 位置参数,关键字参数二合一处理
target.update(zip(params.keys(), args), **kwargs) # 缺省值处理
for k in (params.keys() - target.keys()):
target[k] = params[k].default """
target.update(((k,params[k].default) for k in (params.keys() - target.keys()))) for k,v in params.items():
if k not in target.keys():
target[k] = v.default
"""
key = tuple(sorted(target.items())) #待补充,判断是否需要缓存
if key not in local_cache.keys():
local_cache[key] = fn(*args, **kwargs),datetime.datetime.now().timestamp()
return key, local_cache[key]
return wrapper
return _cache """
装饰查看函数执行时间
"""
def logger(fn): def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
print(fn.__name__,delta)
return ret
return wrapper """
使用多个装饰器,需要注意调用过程和执行过程
调用过程:
生长洋葱一样,遵循就近原则,即离得近的装饰器先装饰,从内向外。
执行过程:
剥洋葱一样,从外向里执行
"""
@logger
@jason_cache(10)
def add(x,y,z=30):
time.sleep(3)
return x + y + z if __name__ == '__main__':
result = []
result.append(add(10,20))
result.append(add(10,y=20))
result.append(add(10, 20, 30))
result.append(add(10,z=30,y=20))
result.append(add(x=10,y=20,z=30)) for item in result:
print(item)

参考案例

七.装饰器的用途

  装饰器是AOP面向切面编程 Aspect Oriented Programming的思想的体现。

  面向对象往往需要通过继承或者组合依赖等方式调用一些功能,这些功能的代码往往可能再多个类中出现,例如logger功能代码。这样造成代码的重复,增加了耦合。loggger的改变影响所有其它的类或方法。

  而AOP再许哟啊的类或者方法上切下,前后的切入点可以加入增强的功能。让调用者和被调用者解耦,这是一种不修改原来的业务代码,给程序员动态添加功能的技术。例如logger函数就是对业务函数增加日志的功能,而业务函数中应该把业务无关的日志功能剥离干净。

八.装饰器应用场景

日志,监控,权限,审计,参数检查,路由等处理。

这些功能与业务功能无关,是很多都需要的公有的功能,所有适合独立出来,需要的时候,对目标对象进行增强。

简单讲:缺什么,补什么。