python中的装饰器允许你动态地改变另一个函数的功能,而不改变它的代码。
什么,这有可能吗?
是的。
这包括:
1.什么是装饰器,如何创建一个?
2.装饰函数的更简单方法
3.类装饰器
4.装饰函数的 docstrings 问题以及如何解决。
什么是 Python 中的装饰器?
装饰器是一个函数,它把另一个函数作为参数,增加一些额外的功能,从而增强它,然后返回一个增强的函数。
所有这些都是在不改变原始函数的源代码的情况下发生的。
让我们来看看它的作用。
假设你有一个计算三角形斜边的函数:
# Compute Hypotenuse
def hypotenuse(a, b):
return round(float((a*a) + (b*b))**0.5, 2)
hypotenuse(1,2)
输出:
#> 2.24
用例
比方说,你碰巧在你的Python代码中定义了许多这样的函数,以一种精心设计的方式被执行。
为了保持跟踪,你想在实际运行之前打印出正在执行的函数,这样你就可以监控python代码中的逻辑流。
在这里,与此同时,你不想改变'Hypotenuse' 或任何其他函数的实际内容,因为很明显,由于管理较大的函数比较困难。
那么我们该怎么做呢?
当然是创建一个装饰器。
# Decorator that takes and print the name of a func.
def decorator_showname(myfunc):
def wrapper_func(*args, **kwargs):
print("I am going to execute: ", myfunc.__name__)
return myfunc(*args, **kwargs)
return wrapper_func
注意,wrapper_func 接收 (*args 和**kwargs)
# Decorate Hypotenuse
decorated_hyp = decorator_showname(hypotenuse)
decorated_hyp(1,2)
#> I am going to execute: hypotenuse
#> 2.24
很好。在执行hypotenuse() 之前,它显示了显示函数名称的自定义信息。
注意,hypotenuse 的内容本身没有改变。非常好!
最大的好消息是:它可以装饰任何函数,而不仅仅是'hypotenuse'。
因此,如果你想做同样的事情,例如计算circumference 的函数,你可以简单地像这样装饰它,它就可以正常工作了。
# Dummy example
decorated_circ = decorator_showname(circumference)
很好。
装饰函数的更简单的方法
但是,还有更简单的方法吗?有的。
只需在你想装饰的函数前加上@decorator_showname 。
# Method 1: Decorate WITH the @ syntax
@decorator_showname
def hypotenuse2(a, b):
return round(float((a*a) + (b*b))**0.5, 2)
hypotenuse2(1,2)
#> I am going to execute: hypotenuse2
#> 2.24
基本上你在这里做的是,装饰hypotenuse2 ,并将被装饰的函数重新分配到相同的名称(hypotenuse2 )。
# Method 2: Decorate WITHOUT the @ syntax.
def hypotenuse2(a, b):
return round(float((a*a) + (b*b))**0.5, 2)
hypotenuse2 = decorator_showname(hypotenuse2)
hypotenuse2(1,2)
#> I am going to execute: hypotenuse2
#> 2.24
这两种方法其实都是一样的。事实上,添加@decorator_func 包装器就能完成方法2的工作。
如何创建类装饰器?
虽然装饰器函数在实践中很常见。装饰器也可以作为类来创建,给它带来更多的结构。
让我们为同样的逻辑创建一个,但使用类:
class decorator_showname_class(object):
def __init__(self, myfunc):
self.myfunc = myfunc
def __call__(self, *args, **kwargs):
print("I am going to execute: ", self.myfunc.__name__)
return self.myfunc(*args, **kwargs)
为了使之工作,你需要确保:
__init__方法把要装饰的原始函数作为输入。这允许类接受一个输入。- 你在dunder
__call__()方法上定义了包装器,这样该类就成为可调用的,以便作为装饰器发挥作用。
@decorator_showname_class
def hypotenuse3(a, b):
return round(float((a*a) + (b*b))**0.5, 2)
hypotenuse3(1,2)
输出:
#> I am going to execute: hypotenuse3
#> 2.24
装饰器的问题:docstring的帮助消失了!?
当你装饰一个函数时,原来被装饰的函数的文档串就无法访问了。
为什么?
因为装饰器接收并返回一个增强的但不同的函数。记得吗?
# Before decoration
def hypotenuse2(a, b):
"""Compute the hypotenuse"""
return round(float((a*a) + (b*b))**0.5, 2)
help(hypotenuse2)
对main模块中的函数hypotenuse2的帮助。
hypotenuse2(a, b) 计算斜率
现在,让我们装饰一下,再试一次。
# Docstring becomes inaccesible
@decorator_showname
def hypotenuse2(a, b):
"""Compute the hypotenuse"""
return round(float((a*a) + (b*b))**0.5, 2)
help(hypotenuse2)
#> Help on function wrapper_func in module
帮助中没有显示文件串:(。
那么如何处理这个问题呢?
解决方案
@functools.wraps(func) 正是因为这个原因,每次有人写装饰器的时候,他们总是用另一个名为functools 包的装饰器来包裹这个包装函数。
它只是用原始函数的文档串来更新包装函数。
这是很容易使用的:
- 只要确保
functools.wraps装饰器返回的包装函数就可以了。 - 它接收要采用其文档的函数作为参数。
import functools
# Add functools docstring updation functionality
def decorator_showname(myfunc):
@functools.wraps(myfunc)
def wrapper_func(*args, **kwargs):
print("I am going to execute: ", myfunc.__name__)
return myfunc(*args, **kwargs)
return wrapper_func
现在试着装饰一下,文档串应该会显示出来:
# decorating will show docstring now.
@decorator_showname
def hypotenuse2(a, b):
"""Compute the hypotenuse"""
return round(float((a*a) + (b*b))**0.5, 2)
help(hypotenuse2)