流畅的python:函数装饰器-Part2

上一节我们讲到了装饰器的基础知识,并且讲到了functools.wraps内置装饰器,由于接下来的内容比较复杂,所以分开进行说明。好了,让我们更深入挖掘装饰器吧!

4、标准库中的装饰器

常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器,我们已经说过了,剩余标准库中最值得关注的两个装饰器是lru_cache和全新的singledispatch,赶紧来看看吧。

4.1 使用functools.lru_cache做备忘

functools.lru_cache是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。我们以生成斐波那契数列为例:

import time
import functools

# 装饰器函数
def clock(func):
    name = func.__name__
    @functools.wraps(func)
    def clocked(*arg,**kwargs):
        n = 1
        if arg:
            n = int(arg[0])
        start = time.perf_counter()
        result = func(n)
        cost_time = time.perf_counter() - start
        print("[{0:.8f}s] {1:s}({2:d})->{3:d}".format(cost_time, name, n,
                                                      result))
        return result
	return clocked

# 装饰器定义
@clock
def fb(n):
    '''生成斐波那契数列'''
    return 1 if n < 3 else fb(n - 1) + fb(n - 2)


>>>fb(6)
# 返回
[0.00000040s] fb(2)->1
[0.00000030s] fb(1)->1
[0.00007020s] fb(3)->2
[0.00000030s] fb(2)->1
[0.00033980s] fb(4)->3
[0.00000030s] fb(2)->1
[0.00000020s] fb(1)->1
[0.00003310s] fb(3)->2
[0.00042010s] fb(5)->5
[0.00000030s] fb(2)->1
[0.00000020s] fb(1)->1
[0.00003300s] fb(3)->2
[0.00000030s] fb(2)->1
[0.00009420s] fb(4)->3
[0.00055190s] fb(6)->8
8

看到了没,采用这种慢速递归函数尽管只计算到fb(8),但是出现了大量的重复运算,效率低下。现在我们用lru_cache来实现一下看看有什么变化:

@functools.lru_cache() # 注意调用方式
@clock
def fb(n):
    return 1 if n < 3 else fb(n - 1) + fb(n - 2)

>>>fb(6)
# 返回
[0.00000040s] fb(2)->1
[0.00000050s] fb(1)->1
[0.00023260s] fb(3)->2
[0.00027590s] fb(4)->3
[0.00030300s] fb(5)->5
[0.00032790s] fb(6)->8
8

效果是立竿见影的。除此之外,lru_cache还有两个可选参数:

functools.lru_cache(maxsize=128, typed=False)
  • maxsize参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize应该设为2的幂。
  • typed参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1和1.0)区分开。
  • 顺便说一下,因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。

4.2 单分派泛函数

我们以一个简单的例子来说明,比如说我们输入一个对象,想把它直接打印出来,但是对于字符串或者整数,需要加上汉字的前缀,你准备怎么办?我一开始就想,直接用if/elif判断一下输入类型不就好了,确实是一个思路,但是这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数(if)会变得很大,而且每一个if都必须构建一个专门函数。

这时候functools.singledispatch就是一个不错的选择,作为装饰器来使用可以把整体方案拆分成多个小的模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数

from functools import singledispatch
import numbers

# 用@singledispatch 标记基函数
@singledispatch 
def print_type(obj):
    print(repr(obj))

# 各个专门函数使用@«base_function».register(«type»)装饰。
@print_type.register(str)
def _(text): # 专门函数的名称无关紧要;_是个不错的选择,简单明了
    print('字符串:', text)

# 可以叠放多个register装饰器,让同一个函数支持不同类型。
@print_type.register(numbers.Integral)
def _(n):
    print('整数:', n)
>>>print_type(1)
整数: 1
>>>print_type("我祝你们开心")
字符串: 我祝你们开心

# 未标注的类型仍按最初的repr(obj)打印
>>>print_type(abs)
<built-in function abs>

只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral和abc.MutableSequence),不要处理具体实现(如int和list)。这样,代码支持的兼容类型更广泛。

5、叠放装饰器

叠放装饰器就是在定义的时候使用多个装饰函数,前面我们已经见过了,在4.1中我们使用lru_cache以及clock两个装饰函数对fb进行装饰。

def d1(func):
    print('a')
    return func

def d2(func):
    print('b')
    return func

@d1
@d2
def f():
    print('c')
# 装饰器定义的时候就被执行,没忘吧
a
b

事实上等价于:f=d1(d2(f))

6、参数化装饰器

前面我们已经学过,Python把被装饰的函数作为第一个参数传给装饰器函数,而被装饰函数的参数在装饰器函数内构造一个内部函数进行接收。如果没有印象,可以参考上一篇文章装饰器(上)中的实现一个简单的装饰器函数。今天我们讨论一个更复杂的问题:装饰器本身如果也要独立外部参数呢?应该传递给谁?

我们以一个简单的例子进行说明,被装饰函数对于被一个进门的警官,都会说一声Hello,装饰器会说出Yes,但是会需要一个额外的性别参数确定确定前缀是MR还是MS:

def factory(sex='man'): # 装饰器工厂函数,接受装饰器所需独立参数
    def decorat(func):  # 真实的装饰器
        def yes(name):  # 内部函数,接收被装饰函数的参数
            func(name)
            if sex == 'man':
                print('Yes, Mr.', name)
            else:
                print('Yes, Ms.', name)
        return yes
    return decorat

# 带参数的装饰器定义
@factory('woman')
def come(name='None'):
    print(name, ' is comming!')

我们一定要区分的参数的不同:

  • 一个是装饰器的参数,这个参数在你定义@的时候就进行传参,传递给装饰器工厂函数factory。调用它会返回真正的装饰器decorat,这才是应用到目标函数上的装饰器。
  • 一个是被装饰函数的参数,通过在装饰器内定义一个内部函数进行传参,比如yes内部函数

如果你不想使用@语法,要像常规函数那样传递上面的两个参数,那么装饰器应该定义为:

def come(name='None'):
    print(name, ' is comming!')
come=factory(sex='woman')(come)

感觉这样的三层嵌套很复杂是吧,好吧,我也觉得很复杂,所以装饰器到这里就结束了吧,不再深入地讨论了。

若想真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的nonlocal声明。掌握闭包和nonlocal不仅对构建装饰器有帮助,还能协助你在构建GUI程序时面向事件编程,或者使用回调处理异步I/O。

另外最后说一点,不是很推荐使用函数作为装饰器,尽管我们为了便于讲述是这么用的,但是事实上更推荐按最好通过__call__方法的类实现。两者的区别不是很大,因为前面我们讲过的,任何实现了__call__方法的对象其行为模式都表现地像个函数。比如上面的例子,使用类实现如下所示:

class factory():
    # 装饰器独立参数通过__init__传递
    def __init__(self, sex='man'):
        self.sex = sex

    # 真实的装饰器
    def __call__(self, func):
    # 被装饰函数的参数同样需要构造内部函数进行传递
        def yes(name):
            func(name)
            if self.sex == 'man':
                print('Yes, Mr.', name)
            else:
                print('Yes, Ms.', name)

        return yes

# 以下语法一样
@factory(sex='man')
def come(name='None'):
    print(name, ' is comming!')

——本章完——
欢迎关注我的微信公众号
扫码关注公众号

你可能感兴趣的:(流畅的python)