Python中的装饰器--如何在不改变代码的情况下增强函数?

103 阅读4分钟

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)

为了使之工作,你需要确保:

  1. __init__ 方法把要装饰的原始函数作为输入。这允许类接受一个输入。
  2. 你在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 包的装饰器来包裹这个包装函数。

它只是用原始函数的文档串来更新包装函数。

这是很容易使用的:

  1. 只要确保functools.wraps 装饰器返回的包装函数就可以了。
  2. 它接收要采用其文档的函数作为参数。
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)