Python高阶装饰器

lru_cache和singledispatch

functools.wraps

functools.wraps 是Python标准库中拿来即用的装饰器之一。虽然这不是这篇文章的重点,但还是举个例子:

def clock(func):
    time0 = time.time()

    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[{:.8f}] {}({}) -> {}'.format(elapsed, name, arg_str, result))
        return result
    time1 = time.time()
    print('{:.5f}'.format(time1-time0))
    return clocked

在这里,@functools.wraps()里面传入的func就是被这个装饰器包装的函数,在使用时调用这个装饰器即可。

functools.lru_cache

functools.lru_cache是非常实用的装饰器,它实现了备忘功能。这是一项优化技术,它把耗时的函数结果保存起来,避免传入相同的参数时的重复计算。
LRU三个字母是“Least Recently Used”的缩写,表明缓存不会无限增长,一段时间不用的缓存条目会被扔掉。我们现在来实现一个斐波拉契数列:

@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
    print(fibonacci(6))

运行结果:

0.00002
[0.00000119] fibonacci(0) -> 0
[0.00000095] fibonacci(1) -> 1
[0.00004601] fibonacci(2) -> 1
[0.00000000] fibonacci(1) -> 1
[0.00000000] fibonacci(0) -> 0
[0.00000095] fibonacci(1) -> 1
[0.00002813] fibonacci(2) -> 1
[0.00005388] fibonacci(3) -> 2
[0.00013494] fibonacci(4) -> 3
[0.00000095] fibonacci(1) -> 1
[0.00000119] fibonacci(0) -> 0
[0.00000072] fibonacci(1) -> 1
[0.00002599] fibonacci(2) -> 1
[0.00005198] fibonacci(3) -> 2
[0.00000000] fibonacci(0) -> 0
[0.00000072] fibonacci(1) -> 1
[0.00006986] fibonacci(2) -> 1
[0.00000000] fibonacci(1) -> 1
[0.00000000] fibonacci(0) -> 0
[0.00000000] fibonacci(1) -> 1
[0.00002813] fibonacci(2) -> 1
[0.00005603] fibonacci(3) -> 2
[0.00015402] fibonacci(4) -> 3
[0.00022912] fibonacci(5) -> 5
[0.00039196] fibonacci(6) -> 8
8

我们可以看到在这行这个函数的时候大部分时间都是装饰器在执行重复的操作,如果我们把装饰器的操作缓存起来就会有更好的体验。

@functools.lru_cache() # 这里引入缓存,实现更高的执行速度
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))

执行结果:

0.00002
[0.00000000] fibonacci(0) -> 0
[0.00000000] fibonacci(1) -> 1
[0.00002813] fibonacci(2) -> 1
[0.00000095] fibonacci(3) -> 2
[0.00004625] fibonacci(4) -> 3
[0.00000095] fibonacci(5) -> 5
[0.00006390] fibonacci(6) -> 8
8

可以看到这个函数的执行过程大大减少了,虽然执行时间变化很小(这里只取了5位小数位,差别并不明显),那是因为这里只是生成了6次的斐波拉切数,如果是100次,
差别会是非常非常大的。

另外有几个注意的地方:

  1. functools.lru_cache()有两个参数,签名functools.lru_cache(
    maxsize=128, typed=False)
    maxsize指定存储多少个调用的结果,缓存满了之后,旧的结果会被扔掉,腾出显得空间。为了得到最佳的性能,maxsize应该设为2的幂,默认为128。如果设置为None的话就相当于是maxsize为正无穷。type参数设为True,会把不同类型的结果分开保存,即把通常认为想等的浮点数和整数类型(如1和1.0)区分。

  2. 因为lru_cache使用字典存储结果,而且键根据传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数必须是可散列的。

  3. lru_cache装饰的函数会有cache_clearcache_info两个方法,分别用于清除缓存和查看缓存信息

除了优化递归算法之外,lru_cache在从Web中获取信息的应用中也能发挥巨大作用。

functools.singledispatch

因为Python不支持方法的重载,在Pyhon3.4版本之后新增了functools.singledispatch装饰器,它可以把整体方案拆分成多个模块,甚至可以为你无法修改的
类提供专门的函数。使用@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同的方式执行相同操作的一组函数。
采用Python官方文档的例子:

from decimal import Decimal
from functools import singledispatch


@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg)


@fun.register(int) # 各个函数专门使用@«base_function».register(«type»)装饰
def _(arg, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)


@fun.register(list)
def _(arg, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem)


@fun.register(float)
@fun.register(Decimal)
def fun_num(arg, verbose=False):
    if verbose:
        print("Half of your number:", end=" ")
    print(arg / 2)


def nothing(arg, verbose=False):
    print("Nothing.")


fun.register(type(None), nothing) # 另一种注册的方式

if __name__ == '__main__':
    fun("hello,world")
    fun("test.", verbose=True)
    fun(42, verbose=True)
    fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
    fun(None)
    fun(1.23)

执行结果:

hello,world
Let me just say, test.
Strength in numbers, eh? 42
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
Nothing.
0.615

singledispatch是经过深思熟虑之后才添加到标准库中的,它提供的特性很多。它不是为了把Java中的重载带入Python,在一个类中为同一个方法定义多个重载
变体,比在一个函数中使用一长串if/elif/elif/else块要好。但是这两种方法都有一定的缺陷,因为他们让代码单元(类或者函数)承担的职责太多。@singledispatch的优点是支持模块化扩展;各个模块可以为他支持的各个类型注册一个专门的函数。

当然,除了上面的三种以外还有:
functools.property: 将方法装饰成属性;
functools.classmethod: 装饰成类方法;
functools.staticmethod : 装饰成静态方法.

参数化装饰器

解析源码的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他函数呢?答案是:创建一个装饰器工厂函数,把参数传给他,返回

一个装饰器,然后再把它应用到要装饰的函数上。

为了便于启动或禁用register执行的函数执行注册功能,我们为他提供一个可选的activate参数,设为False时,不注册被修饰的函数。从概念上看,这个新的register函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。

为了接受参数,新的register装饰器必须作为函数调用

registry = set()


def register(active=True): # 接受一个可选的关键字参数
    def decorate(func): # 这个内部函数是真正的装饰器,它的参数是一个函数
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func) # 如果集合中存在该元素则删除,else do nothing
        return func # 注意,这里没有带括号,不会执行这个函数
    return decorate # register是装饰器工厂函数,因此返回decorate


@register(active=False) # 工厂函数必须作为函数调用,并且传入所需的参数
def f1():
    print('running f1()')


@register() # 即使不传入参数,register也必须作为函数调用,即要返回真正的装饰器decorate
def f2():
    print('running f2()')


def f3():
    print('running f3()')

执行结果:

running register(active=False)->decorate(<function f1 at 0x109a9e158>)
running register(active=True)->decorate(<function f2 at 0x109a9e048>)
阅读更多
换一批

没有更多推荐了,返回首页