python装饰器抽丝拨茧

从前,有一个函数add

def add(a,b):
    s = a + b
    print("add done")
    return s
add(1,2)

执行结果:
add done
3

后来需要在函数add执行前后加上一些代码:

  • 执行前:打印“执行前”
  • 执行后:打印“执行后”
  • 且不能改变add函数结构

要满足这个要求,可以定义一个新的函数A,在A函数中加上需要执行的代码,在A函数中调用add函数。

def add(a,b):
    s = a + b
    print("add done")
    return s

def A(a,b):
    print("执行前")
    add(a,b)
    print("执行后")

A(1,2)

执行结果如下:
执行前
add done
执行后

上面的方案虽然没有改变add函数的结构,但现在有了新的要求:

  • 不能改变add函数的调用方式
  • 有很多类似add的函数需要进行类似改造
  • 要可以接收到add函数的返回值
    按照上述改造方法,首先add函数的调用方式改变了,不能再通过add函数名进行调用,且不能接收到add函数执行的返回值了,同时难以对大量的类似函数进行快捷方便的改造。
def add(a,b):
    s = a + b
    print("add done")
    return s

def A(a,b):
    print("执行前")
    add(a,b)
    print("执行后")

add(1,2)

执行结果:
add done
3

要解决上述问题:

  • 首先要想办法让A函数可以接收参数,这个参数可以告诉A函数其内部要调用的函数是谁,这样就可以只定义一个A函数,然后对很多类似add这样需要改造的函数进行加工。
def A(fun,a,b):    
    print("执行前")
    fun(a,b)
    print("执行后")

def add(a,b):
    s = a + b
    print("add done")
    return s

A(add,1,2)

执行结果:
执行前
add done
执行后
  • 其次还要想办法保持函数的调用方式不改变,上述方案虽然解决了将需要改造的函数名作为参数传递给A,但是调用方式和以前的add(1,2)相比已经改变了。现在还必须让A函数包裹着的内部函数的命名空间传出到外部来,解决调用方式不变的问题。于是闭包的方案呼之欲出了。
def A(fun):
    def decorator(a,b):
        print("执行前")
        fun(a,b)
        print("执行后")
    return decorator

def add(a,b):
    s = a + b
    print("add done")
    return s

add = A(add)
add(1,2)

执行结果:
执行前
add done
执行后
  • 现在我们来解决add函数返回值的接收问题:
def A(fun):
    def decorator(a,b):
        print("执行前")
        ret = fun(a,b)
        print("执行后")
        return ret
    return decorator

def add(a,b):
    s = a + b
    print("add done")
    return s

add = A(add)
s = add(1,2)
print(s)

执行结果:
执行前
add done
执行后
3

好了,咱们已经完成任务了。

不过好像还有一些问题,我们要使用A函数去改造大量的函数,这些函数接收的参数并不一定像add函数这样只有2个。假设一个函数cube需要接接收3个参数,这时如果我们还用上面的A函数去改造,就会报错,例如:

def A(fun):
    def decorator(a,b):
        print("执行前")
        ret = fun(a,b)
        print("执行后")
        return ret
    return decorator

def cube(a,b,c):
    s = a * b * c
    print("cube done")
    return s

cube = A(cube)
s = cube(1,2,3)
print(s)
执行结果:
TypeError                                 Traceback (most recent call last)
<ipython-input-7-8426cee39f2f> in <module>
     13 
     14 cube = A(cube)
---> 15 s = cube(1,2,3)
     16 print(s)

TypeError: decorator() takes 2 positional arguments but 3 were given

怎么办?怎样才能让A函数具有广泛的适应能力?

  • 我们想到了python的*,**方式传参,有了这两个武器,就可以让A中的decorator函数可以接收任意数量的参数。棒棒滴
def A(fun):
    def decorator(*args,**kwargs):
        print("执行前")
        ret = fun(*args,**kwargs)
        print("执行后")
        return ret
    return decorator

def cube(a,b,c):
    s = a * b * c
    print("cube done")
    return s

def add(a,b):
    s = a + b
    print("add done")
    return s

cube = A(cube)
s = cube(1,2,3)
print(s)
print("--------------华丽分割线------------------")

add = A(add)
s = add(1,2)
print(s)

执行结果:
执行前
cube done
执行后
6
--------------华丽分割线------------------
执行前
add done
执行后
3

舒服啊,这下只需要用 fun = A(fun)的形式,就可以让A函数去改造任何函数了,而且从外部调用方来看,根本没有改变fun()这样的调用方式。到这里就大功告成了。

  • 可是,在简洁的道路是,pythonic怎么会满足于此呢。于是@符号就来了
def A(fun):
    def decorator(*args,**kwargs):
        print("执行前")
        ret = fun(*args,**kwargs)
        print("执行后")
        return ret
    return decorator

@A
def cube(a,b,c):
    s = a * b * c
    print("cube done")
    return s

@A
def add(a,b):
    s = a + b
    print("add done")
    return s

s = cube(1,2,3)
print(s)
print("--------------华丽分割线------------------")

s = add(1,2)
print(s)

执行结果:
执行前
cube done
执行后
6
--------------华丽分割线------------------
执行前
add done
执行后
3

神奇不神奇?

原理剖析:

-我们把函数add、cube作为一个对象传给A,使用一个函数decorator将其包装起来,然后定制我们想要的功能,然后把decorator返回,这样我们就得到了一个被我们包装(装饰)过的函数,这就是装饰器的实现原理。

结束了吗?

  • 让我们站在一个外部用户的角度来看看,还有什么不完美的地方
def A(fun):
    def decorator(*args,**kwargs):
        """这是decorator函数,用来改造函数们"""
        print("执行前")
        ret = fun(*args,**kwargs)
        print("执行后")
        return ret
    return decorator

@A
def cube(a,b,c):
    """这是cube函数,用来算体积"""
    s = a * b * c
    print("cube done")
    return s

def add(a,b):
    """这是add函数,用来做加法"""
    s = a + b
    print("add done")
    return s

s = cube(1,2,3)
print(s)
help(cube)
print("--------------华丽分割线------------------")

s = add(1,2)
print(s)
help(add)

执行结果:
执行前
cube done
执行后
6
Help on function decorator in module __main__:

decorator(*args, **kwargs)
    这是decorator函数,用来改造函数们

--------------华丽分割线------------------
add done
3
Help on function add in module __main__:

add(a, b)
    这是add函数,用来做加法
  • 看到上面add和cube的help文档的区别没有,用A装饰过的函数cube的帮助文档变成了decorator的帮助文档,而没有经过A装饰的函数add还保留着自己的帮助文档

对于python这么一个追求完美的语言,这是难以容忍的,盘它:

def A(fun):
    def decorator(*args,**kwargs):
        """这是decorator函数,用来改造函数们"""
        print("执行前")
        ret = fun(*args,**kwargs)
        decorator.__name__ = fun.__name__
        decorator.__doc__ = fun.__doc__
        print("执行后")
        return ret
    return decorator

@A
def cube(a,b,c):
    """这是cube函数,用来算体积"""
    s = a * b * c
    print("cube done")
    return s

@A
def add(a,b):
    """这是add函数,用来做加法"""
    s = a + b
    print("add done")
    return s

s = cube(1,2,3)
print(s)
help(cube)
print("--------------华丽分割线------------------")

s = add(1,2)
print(s)
help(add)

执行结果:
执行前
cube done
执行后
6
Help on function cube in module __main__:

cube(*args, **kwargs)
    这是cube函数,用来算体积

--------------华丽分割线------------------
执行前
add done
执行后
3
Help on function add in module __main__:

add(*args, **kwargs)
    这是add函数,用来做加法

优化到现在,算是比较完美了。其实python已经内置了解决方案。

  • 那就是functools.wraps,让我们继续在简化的道路上越陷越深。
  • 先把上面的那个方案中的decorator.name = fun.__name__和decorator.doc = fun.__doc__删掉。
from functools import wraps
def A(fun):
    @wraps(fun)
    def decorator(*args,**kwargs):
        """这是decorator函数,用来改造函数们"""
        print("执行前")
        ret = fun(*args,**kwargs)
        decorator.__name__ = fun.__name__
        decorator.__doc__ = fun.__doc__
        print("执行后")
        return ret
    return decorator

@A
def cube(a,b,c):
    """这是cube函数,用来算体积"""
    s = a * b * c
    print("cube done")
    return s

@A
def add(a,b):
    """这是add函数,用来做加法"""
    s = a + b
    print("add done")
    return s

s = cube(1,2,3)
print(s)
help(cube)
print("--------------华丽分割线------------------")

s = add(1,2)
print(s)
help(add)

执行结果:
执行前
cube done
执行后
6
Help on function cube in module __main__:

cube(a, b, c)
    这是cube函数,用来算体积

--------------华丽分割线------------------
执行前
add done
执行后
3
Help on function add in module __main__:

add(a, b)
    这是add函数,用来做加法

真的是很棒啊。

  • 让我们继续研究一下@wraps(fun)的原理。装饰器带参数?怎么回事?
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页