装饰器
一、装饰器的引入
有一个需求,一个加法函数,想要增强它的功能,能够输出被调用过以及调用的参数信息,这种需求应该如何解决呢?
1 # 加法函数 add 的定义 2 def add(x, y): 3 return x+y 4 # 增强它的功能,输出被调用过以及调用的参数信息 5 def add(x, y): 6 print("call add, x = {}, y = {}".format(x, y)) 7 return x+y
思考:功能是增强了,但是这种直接在原来只计算值的函数中增加属于非业务功能的代码,这种方式好吗?肯定是不好的,缺点如下:
-
首先打印语句的耦合性太高了
-
加法函数属于业务的逻辑,输出信息属于非业务功能的代码,不应该直接放在该函数中。
代码改进,重新定义一个函数,在这个函数中来做输出信息的功能:
1 def add(x, y): 2 return x+y 3 4 def message_show(fn): 5 print("begin...call add") 6 result = fn(4, 5) 7 print("end...") 8 return result 9 10 print(message_show(add))
思考:这种方式做到了业务功能的分离,但是 add 函数的传参是一个问题,这种方式很不灵活,继续改进。
代码改进,解决参数的问题:
1 def add(x, y): 2 return x+y 3 4 def message_show(fn, *args): 5 print(*args) 6 print("begin...call add") 7 result = fn(*args) 8 print("end...{} + {}".format(*args)) 9 return result 10 11 print(message_show(add, 4, 5))
至此,成功做到了新增业务功能的分离,可以看到并没有在原来的 add 函数中加入任何的代码,从而增强了它显示信息的功能。
二、装饰器的改造
(一)柯里化
要写出装饰器,首先要对函数进行柯里化。柯里化的过程如下代码:
1 def add(x, y): 2 return x+y 3 4 def message_show(fn): 5 def wrapper(*args): 6 print(*args) 7 print("begin...call add") 8 result = fn(*args) 9 print("end...{} + {}".format(*args)) 10 return result 11 return wrapper 12 13 print(message_show(add)(4, 5))
思考:message_show(add)(4, 5)
这种调用方式能不能改造成add(4, 5)
,这种调用方法是不是就看不出调用过 message_show
函数了,而是直接调用的 add 函数,继续改造。
(二)装饰器定义
代码改造,柯里化之后,就可以使用装饰器的语法糖:
1 def message_show(fn): 2 def wrapper(*args): 3 print(*args) 4 print("begin...call add") 5 result = fn(*args) 6 print("end...{} + {}".format(*args)) 7 return result 8 return wrapper 9 10 @message_show # 等效于 add = message_show(add) 11 def add(x, y): 12 return x+y 13 14 print(add(4, 5))
注意:装饰函数的代码,必须要定义在被装饰函数的代码之前。
至此,一个简单的装饰器就出来了。
三、无参装饰器
(一)无参装饰器的理解
-
装饰器本身就是一个函数,而且是一个高阶函数
-
它的形参是一个函数,ps:要被装饰的函数
-
它的返回值是一个函数
-
可以使用 @function_name 的方式调用
(二)装饰器的例子
1 def fn_strengthen(fn): 2 def wrapper(*args, **kwargs): 3 print("========start=======") 4 print("args = {}, kwargs = {}".format(args, kwargs)) 5 result = fn(*args, **kwargs) 6 print(result) 7 print("========end=========") 8 return result 9 return wrapper 10 11 @fn_strengthen # add = fn_strengthen(add) 12 def add(x, y): 13 print("==========call add=============") 14 return x + y 15 16 add(4, y=5)
(三)装饰器的副作用
1 def fn_strengthen(fn): 2 def wrapper(*args, **kwargs): 3 print("========start=======") 4 print("args = {}, kwargs = {}".format(args, kwargs)) 5 result = fn(*args, **kwargs) 6 print(result) 7 8 print("========end=========") 9 return result 10 return wrapper 11 12 @fn_strengthen # add = fn_strengthen(add) 13 def add(x, y): 14 """This is a function to add""" 15 print("==========call add=============") 16 return x + y 17 18 print("name = {}, doc = {}".format(add.__name__, add.__doc__)) # name = wrapper, doc = None
注意:可以看出被装饰函数的属性都被替换了,这种问题应该如何解决?
代码改进,再定义一个函数,用来记录被装饰函数的属性:
1 def fn_strengthen(fn): 2 def wrapper(*args, **kwargs): 3 print("========start=======") 4 print("args = {}, kwargs = {}".format(args, kwargs)) 5 result = fn(*args, **kwargs) 6 print(result) 7 8 print("========end=========") 9 return result 10 add_property(fn, wrapper) 11 return wrapper 12 13 def add_property(fn, wrapper): 14 wrapper.__name__ = fn.__name__ 15 wrapper.__doc__ = fn.__doc__ 16 17 @fn_strengthen # add = fn_strengthen(add) 18 def add(x, y): 19 """This is a function to add""" 20 print("==========call add=============") 21 return x + y 22 23 print("name = {}, doc = {}".format(add.__name__, add.__doc__))
上面的方法解决了属性被覆盖的问题,考虑到add_property
这个函数的通用性,可以将这个复制属性的函数也改造成装饰器函数,只不过是带参的装饰器,代码改造如下:
1 def add_property(fn): 2 def _copy(wrapper): 3 wrapper.__name__ = fn.__name__ 4 wrapper.__doc__ = fn.__doc__ 5 return wrapper 6 return _copy 7 8 def fn_strengthen(fn): 9 @add_property(fn) # wrapper = add_property(fn)(wrapper) 10 def wrapper(*args, **kwargs): 11 print("========start=======") 12 print("args = {}, kwargs = {}".format(args, kwargs)) 13 result = fn(*args, **kwargs) 14 print(result) 15 16 print("========end=========") 17 return result 18 return wrapper 19 20 @fn_strengthen # add = fn_strengthen(add) 21 def add(x, y): 22 """This is a function to add""" 23 print("==========call add=============") 24 return x + y 25 26 print("name = {}, doc = {}".format(add.__name__, add.__doc__))
四、带参装饰器
(一)带参装饰器的引入
有一个需求,获取函数的执行时长,对时长超过阈值的函数记录一下:
1 import datetime, time 2 3 def add_property(fn): 4 def _copy(wrapper): 5 wrapper.__name__ = fn.__name__ 6 wrapper.__doc__ = fn.__doc__ 7 return wrapper 8 return _copy 9 10 def time_detection(timeout): 11 def fn_strengthen(fn): 12 @add_property(fn) # wrapper = add_property(fn)(wrapper) 13 def wrapper(*args, **kwargs): 14 print("========start=======") 15 start = datetime.datetime.now() 16 result = fn(*args, **kwargs) 17 print(result) 18 delta = (datetime.datetime.now() - start).total_seconds() 19 print("so slow") if delta > timeout else print("so fast") 20 print("========end=========") 21 return result 22 return wrapper 23 return fn_strengthen 24 25 @time_detection(5) # add = time_detection(5)(add) 26 def add(x, y): 27 """This is a function to add""" 28 print("==========call add=============") 29 time.sleep(6) 30 return x + y 31 32 add(4, 5)
(二)带参装饰器的理解
-
带参装饰器本身是一个函数,而且是一个高阶函数
-
它的形参可以是一个函数,返回值是一个不带参数的装饰器函数
-
使用 @function_name(参数) 方式调用
-
可以看做是在装饰器的外层又加了一层函数
(三)带参装饰器的例子
将上一个例子中的记录时长的功能提取出来,能不能定义成一个参数来灵活的控制输出,代码改造如下:
1 import datetime, time 2 3 def add_property(fn): 4 def _copy(wrapper): 5 wrapper.__name__ = fn.__name__ 6 wrapper.__doc__ = fn.__doc__ 7 return wrapper 8 return _copy 9 10 def time_detection(timeout, func=lambda name, delta:print("{} spend {}s".format(name, delta))): 11 def fn_strengthen(fn): 12 @add_property(fn) # wrapper = add_property(fn)(wrapper) 13 def wrapper(*args, **kwargs): 14 print("========start=======") 15 start = datetime.datetime.now() 16 result = fn(*args, **kwargs) 17 print(result) 18 delta = (datetime.datetime.now() - start).total_seconds() 19 if delta > timeout: 20 func(fn.__name__, delta) 21 print("========end=========") 22 return result 23 return wrapper 24 return fn_strengthen 25 26 @time_detection(5) # add = time_detection(5)(add) 27 def add(x, y): 28 """This is a function to add""" 29 print("==========call add=============") 30 time.sleep(6) 31 return x + y 32 33 add(4, 5)
五、functools模块
这个模块就省了自己重新写复制属性的那个函数,functools.update_wrapper(包装函数, 原函数)
使用这个模块来改造前面获取原函数属性的函数:
1 import datetime, time, functools 2 3 def time_detection(timeout, func=lambda name, delta:print("{} spend {}s".format(name, delta))): 4 def fn_strengthen(fn): 5 def wrapper(*args, **kwargs): 6 print("========start=======") 7 start = datetime.datetime.now() 8 result = fn(*args, **kwargs) 9 print(result) 10 delta = (datetime.datetime.now() - start).total_seconds() 11 if delta > timeout: 12 func(fn.__name__, delta) 13 print("========end=========") 14 return result 15 return functools.update_wrapper(wrapper, fn) 16 return fn_strengthen 17 18 @time_detection(5) # add = time_detection(5)(add) 19 def add(x, y): 20 """This is a function to add""" 21 print("==========call add=============") 22 time.sleep(6) 23 return x + y 24 25 add(5, 6), add.__name__, add.__doc__
还可以使用这个模块的装饰器的方法来复制属性,@functools.wraps(原函数)
:
1 import datetime, time, functools 2 3 def time_detection(timeout, func=lambda name, delta:print("{} spend {}s".format(name, delta))): 4 def fn_strengthen(fn): 5 @functools.wraps(fn) 6 def wrapper(*args, **kwargs): 7 print("========start=======") 8 start = datetime.datetime.now() 9 result = fn(*args, **kwargs) 10 print(result) 11 delta = (datetime.datetime.now() - start).total_seconds() 12 if delta > timeout: 13 func(fn.__name__, delta) 14 print("========end=========") 15 return result 16 return wrapper 17 return fn_strengthen 18 19 @time_detection(5) # add = time_detection(5)(add) 20 def add(x, y): 21 """This is a function to add""" 22 print("==========call add=============") 23 time.sleep(6) 24 return x + y 25 26 print(add(5, 6), add.__name__, add.__doc__)