python装饰器-给你的代码加个buff

python装饰器-给你的代码加个buff

面对威严高耸的需求城堡,攻城狮们严阵以待,只听队长一声令下:弟兄们,给俺冲!大队人马即将一跃而上,这时法师李狗蛋举手示意:等一哈,容我先起个buff!队长大声呵斥:这关键时刻你搞毛线啊!李狗蛋不顾队长的呵斥,默默在那结印施法,时间一分一秒的过去了,众人呆呆的看着他,突然间腾的一声,李狗蛋变的又高又壮。众人惊喜,思锅矣~队长问:“这是啥buff”,“装饰器红buff”,李狗蛋答道,队长暗喜:这要是每人都加个装饰器,那不分分钟拿下城堡了。果然之后的战斗很快就以胜利告终了~到底这装饰器是何方神器,为何如此强大,且听我慢慢道来。

python中将一个可调用对象A作为参数传给另一个可调用对象B,返回经过了特殊处理或是增加了额外功能的伪A,这时将可调用对象B称为一个装饰器,可以是装饰器函数或装饰器类。之所以把返回的称为伪A,是因为返回的其实是一个包装函数,包裹着真正的A和一些其他特殊操作,后边通过各种示例就可以看出来。装饰器有两种使用形式:利用语法糖@和原始的无糖型方式。

一、利用语法糖@将装饰器绑定到目标上

因为其便利性以及可以提高代码的可读性,装饰器的大部分使用场景都是基于这种方式。一旦通过这种方式使用装饰器,那被装饰的函数就不再是源函数了,而是伪A,就像文章开头所说的那样,通过例子来看下
#例子1-1
def simple_dec(func):
    def wrapper(*args, **kwds):
        print("Decorated function:{}".format(func.__name__))
        return func(*args, **kwds)
    return wrapper

@simple_dec
def simple_func():
    pass

print(simple_func)
simple_func()

输出:
<function simple_dec.<locals>.wrapper at 0x000001AF9272D6A8>
Decorated function:simple_func

可以看到被装饰过的simple_func已经被悄然替换为包装函数wrapper,当调用simple_func时,其实是执行的wrapper(),wrapper中包裹着特殊的操作以及对源函数simple_func的调用。熟悉python的人应该都知道可以通过functools.wraps来保留源函数的信息,如下所示:

#例子1-2
from functools import wraps

def simple_dec(func):
    @wraps(func)
    def wrapper(*args, **kwds):
        print("Decorated function:{}".format(func.__name__))
        return func(*args, **kwds)
    return wrapper

@simple_dec
def simple_func():
    pass

print("I am actually wrapper:", simple_func)
print("I am real simple_func:", simple_func.__wrapped__)
simple_func()

输出:
I am actually wrapper: <function simple_func at 0x000002015E42E6A8>
I am real simple_func: <function simple_func at 0x000002015E2BFA60>
Decorated function:simple_func

通过是用wraps后,可以看到打印simple_func输出的是<function simple_func at 0x000002015E42E6A8>,但实际上还是wrapper。其实wraps装饰器就是进行了一些复制操作,把被装饰的源函数的某些属性复制给wrapper函数,特殊属性包括:’__module__’, ‘__name__’, ‘__qualname__’, ‘__doc__’,’__annotations__’,’__dict__’,并给wrapper添加了个特殊属性__wrapped__,赋值为func函数,算是一个特殊接口用来访问源函数,就像例子中所示那样,而且通过输出可以看到,两次输出的内存地址不相同,说明不是同一个函数。

上边的例子展示了比较标准的装饰器,只是单纯的将源函数作为装饰器的参数,其实装饰器也是可以有自定义参数
#例子1-3
def parameter_dec(**kwds):
    def decorator(func):
        for name, value in kwds.items():
            setattr(func, name, value)
        @wraps(func)
        def wrapper(*args):
            return func(*args)
        return wrapper
    return decorator

@parameter_dec(hello="world")
def parameter_func(n):
    print("I am parameter_func")

print(parameter_func.__wrapped__.hello)

输出:
world

从例子中可以看到只是又多了一层调用,这里利用闭包原理,真正的装饰器decorator可以访问最外层parameter_dec传入的参数,进而给被装饰的源函数添加额外的属性。

关于该装饰器使用方式还有一点要说明的就是,当多个装饰器作用于同一个目标函数时的顺序问题,通过一个简单的例子来说明:

#例子1-4
def dec1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('I am dec1')
        return func(4)
    return wrapper

def dec2(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('I am dec2')
        return func(3)
    return wrapper

@dec2
@dec1
def func(num):
    print('I am func, my arg:', num)

func(999)

输出:
I am dec2
I am dec1
I am func, my arg: 4

通过输出可以看到,最外层的装饰器dec2先执行,最靠近源函数的dec1最后执行。

二、无糖的执行方式

所谓无糖的执行方式,其实就是装饰器的本质执行原理。我们以例子1-1来说明这种执行方式,当调用被装饰的函数:simple_func(),其实就相当于simple_dec(simple_func)(),很明显分为两步调用,先以simple_func作为参数调用simple_dec,返回函数wrapper然后再次调用。再回看例子1-3,调用被装饰的函数parameter_func就相当于parameter_dec(hello=”world”)(func)(n);例子1-4就相当于dec2(dec1(func))(999),这其实就是装饰器的本来面目,比较丑陋晦涩难懂。而语法糖@正是为了解决这种丑陋的调用方式而产生的,让代码变得更加整洁易懂。那既然无糖的方式这么low,干脆直接抛弃掉,只用语法糖的方式不得了。no~no~no~,low有low的好处,糖分低不容易得糖尿病呀。举个例子,比如有1000个接口函数,需要判断返回是否正常,通过装饰器可以避免在每个接口中编写样板代码。此时如果使用语法糖@方式,那就要给每个接口函数都添加@decorator形式的标注,显而易见这么做很是麻烦,但是通过原始方式调用的话就方便多了,通过伪代码来看下:

for method in methods:
    decorator(method)(args)

通过一个简单的循环就解决了,并且通过这种无糖的调用方式不会改变接口函数的原始性,因为是在运行时才会执行的。不会像语法糖@那样在定义时就已经绑定给源函数,也就是在编译期就已经改变了源函数,这算是两种执行方式之间比较突出的区别了。其实如果觉得无糖调用的方式麻烦,可以赋值给一个变量deced_func=dec2(dec1(func)),之后直接调用deced_func即可。当然了,不同的需求通过不同的方式去解决,要学会灵活运用。

接下来会列举一些实践性较强的例子,来展示装饰器的强大用途:

1、计时装饰器,用来统计函数的执行时间

from random import shuffle 
from time import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwds):
        start = time()
        result = func(*args, **kwds)
        elapsed = int((time() - start)*1000)
        print("{} elapsed: {}ms".format(func.__name__, elapsed))
        return result
    return wrapper

def quick_sort(seq):
    if len(seq) < 2:
        return seq
    small_list = []
    big_list = []
    #median为基准值
    median_index = int(len(seq)/2)
    median = seq[median_index]
    front = seq[:median_index]
    end = seq[median_index+1:]
    for num in chain(front, end):
        if num > median:
            big_list.append(num)
        else:
            small_list.append(num)
    return quick_sort(small_list) + [median] + quick_sort(big_list)

l = list(range(100000))
shuffle(l)
timer(quick_sort)(l)

输出:
quick_sort elapsed: 283ms

该例子中使用了无糖的调用方式,因为快速排序函数中有递归调用,如果用语法糖@方式调用的话,结果你懂得,该示例引自笔者的上一篇文章这些耳熟能详的算法你真的懂么,有兴趣的可以去瞅一眼。

2、熟悉python的人都应该知道property装饰器,是基于C实现的。下边的例子提供了python版的实现,笔者做了点小改进,增加了类型校验,比较糙,主要看原理。

class myproperty:
    def __init__(self, func):
        self._getter = func
    
    def __get__(self, obj, cls):
        if not obj:
            return self
        result = self._getter(obj)
        return result
    
    def __set__(self, obj, value):
        if value.__class__ != self._cls:
            raise TypeError("The type of value must be {}".format(self._cls))
        self._setter(obj, value)
  
    def setter(self, cls):
        self._cls = cls
        def wrapper(func):
            if func.__name__ != self._getter.__name__:
                raise NameError("The name of setter must be same with getter's")
            self._setter = func
            return self
        return wrapper
        
class Test:
    def __init__(self):
        self._value = None

    @myproperty
    def value(self):
        return self._value

    @value.setter(int)
    def value(self, value):
        self._value = value

t = Test()
t.value = 666
print(t._value)
t.value = "hello world"

输出:
666
Traceback (most recent call last):
  File "d:\mycode\Python\tests\dec.py", line 128, in <module>
    t.value = "hello world"
  File "d:\mycode\Python\tests\dec.py", line 100, in __set__
    raise TypeError("The type of value must be {}".format(self._cls))
TypeError: The type of value must be <class 'int'>

例子中的myproperty是一个描述器,同时也是一个装饰类,原理跟装饰函数一样,只不过是把源函数作为参数传给装饰类的构造方法而已,其实就相当于这个表达式:t.value = myproperty(value),也就是将一个描述器实例赋值给t.value,这样在对t.value访问或是赋值时会先访问描述器的__get__和__set__特殊方法,从而改变了该属性的访问与赋值方式。如果不了解描述器相关知识,可以先参考笔者的另一篇文章PYTHON描述器知多少,相信会让你有所顿悟。这个例子的核心技术还是描述器居多,但是相比于直接将描述器实例赋值给属性的方式,通过装饰器让描述器的使用方法变得更加优雅,这也是装饰器的一大优点。

3、利用装饰器实现函数重载

from functools import singledispatch

@singledispatch
def fun(arg, verbose=True):
    print(arg)

@fun.register(int)
def _(arg):
    print("int arg:", arg)

@fun.register(list)
def _(arg):
    print("list arg:", arg)

fun(123)
fun([1,2,3,4])

输出:
int arg: 123
list arg: [1, 2, 3, 4]

熟悉java的人应该都知道,java中的方法重载很方便,因为它是强类型语言,相同的方法名不同的参数类型该方法的签名就不同。python是弱类型语言,通过对参数类型判断也可以实现重载,但是远不如通过装饰器来的优雅,代码风格上有点类似java,不同类型的参数单独定义其方法,使代码更加清晰易懂。

4、实现缓存功能,避免重复计算

from functools import lru_cache

@lru_cache(maxsize=1024)
def fib1(n, f=1, s=1):
    if n == 1:
        return f
    if n == 2:
        return s
    if n > 2:
        return fib1(n-2) + fib1(n-1)

def fib2(n, f=1, s=1):
    if n == 1:
        return f
    if n == 2:
        return s
    if n > 2:
        return fib2(n-2) + fib2(n-1)

timer(fib1)(100)
timer(fib2)(40)

输出:
fib1 elapsed: 0ms
fib2 elapsed: 24177ms

通过输出可以看到增加了缓存的fib1的性能要远远优于无缓存的fib2。因为python在通常的cpython解释器中是解释执行的,所以其执行效率是比较低下的,但是通过增加缓存功能,以空间换时间,可以极大的提高执行性能,减少cpu占用。笔者在实际工作中通过增加计算缓存,把cpu占用从原来的80%降低到30%左右,可谓是收效颇丰。但是否应该使用缓存还得看实际的场景,并不是每种情况都适合使用缓存的。

5、单例类装饰器,相比于其他实现单例的方式比如元类,要优雅的多

def signleton(cls):
    cache = None
    def wrapper(*args, **kwds):
        nonlocal cache
        if cache is None:
            cache = cls(*args, **kwds)
        return cache
    return wrapper

@signleton
class Test:
    pass

t = Test()
a = Test()
print("t is a:", t is a)

输出:
t is a: True
通过上面的例子可以看到,装饰器可以给被装饰对象增加额外功能,使之成为增强版,就像游戏中给角色增加了buff一样,瞬间使角色变的强大起来。这里再顺便提下另一个与装饰器有关的概念:AOP面向切面编程。关于AOP编程网上有很多专业的介绍,个人理解其本质就是在不修改核心代码的前提下为该核心代码增加特定功能的特殊编程方法,python中的装饰器其实只是AOP面向切面编程的一种实现方式。在很多框架中比如Django和spring,都提供了基于AOP的功能,使开发更加简单并且极大提高开发效率,比如请求缓存,通用异常处理,自定义权限验证,日志划分处理等。
Comments are closed.