《Fluent Python》- 07 函数装饰器和闭包

这里的装饰器并非是四人帮(我们老师巨喜欢这么叫,我第一次听到的时候是一脸懵逼的)那本书中的装饰器模式,别搞混了。函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

装饰器基础

装饰器是可调用对象,其参数是另一个函数。装饰器可能会处理被装饰的函数,然后把它返回,或者将替换为另一个函数或可调用对象。

假如有个名为decorate的装饰器:

# @decorate  # 假设有个decorate的装饰器
# def target():
#     print("running target()")

def target():   # 与上面的效果相同
    print("running target()")

target = decorate(target)

这两种写法最终的结果是一致的。最后得到的这个target并不是原来的那个target函数,而是decorate(target)

做个示范:

def deco(func):
    def inner():
        print('running inner()')
    return inner  # deco 函数返回的是inner函数对象

@deco
def target():
    print('running target()')  

target()  # running inner()
print(target) # <function deco.<locals>.inner at 0x000001C47ABBB940> 其实是inner对象的引用

我们现在知道装饰器的一大特性是能把被装饰的函数替换成其他函数,第二个马上说,就是加载模块时立即执行。

registry = [] # 保存被@register装饰 引用的函数

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func  # 与之前不同,这里返回的是引用的方法自身

@register
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

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

def main():
    print('running main()')
    print('registry->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main()

## 输出
# running register(<function f1 at 0x00000128BEAEC940>)
# running register(<function f2 at 0x00000128BEAFFAF0>)
# running main()
# registry-> [<function f1 at 0x00000128BEAEC940>, <function f2 at 0x00000128BEAFFAF0>]
# running f1()
# running f2()
# running f3()

注意,register在模块中其他函数之前运行(两次)。调用register时,传给它的参数是被装饰的函数。加载模块后,registry中有两个被装饰函数的引用:f1和f2。这两个函数,以及f3,只在main明确调用它们时才执行。

使用装饰器改进“策略”模式

# 用装饰器修改之前的策略
promos = []

def promotion(promo_func):
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity_promo(order):
    """为积分1000或以上的顾客提供5%"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item_promo(order):
    """单个商品为20个或以上时提供10%"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 0:
            discount += item.total() * .1
    return discount

@promotion
def large_order_promo(order): # 第三个
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0

def best_promo(order):
    return max(promo(order) for promo in promos)

这里我们也不用注意命名规范,不需要以_promo结尾。其次,@promotion,体现了装饰器的函数作用,还便于禁用策略,只需要去掉注释即可。最后,促销折扣可以在其他模块中定义,任何地方都可以(推荐你不要这样,否则你可能会出现找不到的情况)

变量作用域

b = 1
def f1(a):
    # 以b为例,如果函数体内第二次出现了对b的赋值之类的操作,就会报错,简单说,函数体的这个b会与之外的b冲突
    # 如果想用的是全局的一个b,需要用global声明
    global b
    # print(a)
    # print(b)
    b = 3  # 如果没有global声明会报错
f1(1)

闭包

闭包不等于匿名函数,别混淆了。

闭包是延伸了作用域,其中包含函数定义体中引用,但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

有些抽象,我们看示例吧:

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

我们写了一个简单求平均的方法,它保存了过去的数据,接着我们换一种写法:

def make_averager():
    series = []
    num = 0
    def averager(new_value):
        # nonlocal num  #  使用了nonlocal,下面不会创建新对象,而是用外面那个
        num = 5  # 如果没有nonlocal,这里把外面的num盖掉了,之后的num都是这个对象,而非原来的那个
                 # 与java有点类似,因为python这里其实并不是对外面的num重新赋值,而是重新创建了一个对象
        series.append(new_value)
        total = sum(series)

        print("var:", num)
        return total / len(series)
    averager(5)
    print("global:",num)
    return averager

avg = make_averager()
print(avg(1))

这两个示例有共通之处(不要在意nonlocal部分,那个是为了方便我们做统计用的,简化averager时候用的,思路很简单,我们只保存两个值,total,和count就可以了,total/count就是average):调用Avergaer()或者make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前均值。

实现一个简单的装饰器

def clock(func):
    @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('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

functools.wraps,它的作用是协助构建行为良好的装饰器。

我们来试一下斐波那契数列:

@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

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

很简单和标准的推导斐波那契的第n个数,这个方法有多耗时,相信大家一定很清楚了。我们简单的来优化一下(这里不采用动态规划的思想,主要是为了说明Python中的一个注解)。我们都知道这样计算斐波那契数列很耗时,而耗时就在于它重复计算上。我们按照这个代码去跑,会出现上千行结果

 

 观察这些内容不难发现,很多内容会被重复计算。熟悉算法的朋友,应该很容易想到一种叫做记忆化搜索(记忆化搜索,简单来说就是一定程度的缓存结果,通常是可以和动态规划相互转换的)的方式。这里就采用这种思想,在Python中有个注解就可以缓存函数结果:

@functools.lru_cache()  # 有两个参数,maxsize,typed   maxsize 整个lru的长度,lru:会踢出最近最少使用,为了保证性能,maxsize保证为2的幂比较合适
                        # typed如果为true,会把不同类型结果分开存

我们加上注解再来跑一遍:

 这次只输出了18行结果(17行fib(x)和一行结果),我这里并不是要告诉大家怎么去优化斐波那契数列(计算fib最快的方式还是动态规划),而且想告诉大家functools.lru_cache()的作用。它可以帮助我们缓存函数结果,如果有大量重复使用的情况下,采用这个方式会大幅提高性能。

单分派泛函数

import html
from functools import singledispatch
import numbers
from collections import abc

@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item for item in seq))
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

因为我不是搞前端的,对于html,js,css我也只停留在能简单看懂,编写简单代码层面,这个原理是什么我没法给你解释。我把代码贴在这里,感兴趣的同学可以好好学习一下。我主要是说明一下这里的注解。

Python是不支持重载的。所以我们需要采用单分派的想法变相支持。Python提供了一个@singledispatch装饰器注解,可以把整体方案拆分成多个模块,使用@singledispatch装饰过的普通方法会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。还有要注意的地方,我们注册的专门函数,应该去处理一些抽象基类,而不是具体实现类。这样代码支持的范围就会更广。

装饰器也是可以叠加的:

# @d1
# @d2
# def f():
#     print('f')
# 
# 等价于
# 
# def f():
#     print('f')
# f = d1(d2(f))

 

杂谈:

这部分还存在一个参数化装饰器,用起来颇为复杂,简单来说就是创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把他应用到要装饰的函数上。就说到这里,具体如何使用,还是去查看其他资料,书上的内容是很少的。而且要完全弄懂这个,你先得明白工厂这个东西,我就不多说了。

上周把git相关内容过了一遍,顺带学习了一些linux基础,我果然还是没办法一边学习一边写blog,git部分之后看情况吧,现在坑太多填不过来。最近又快开学了,烦的一批,实在不想去学校,去了就要被隔离。我毕设也弄完了,去了也没啥事,表示不想被隔离。

posted @ 2020-04-27 21:16  C`Moriarty  阅读(228)  评论(0编辑  收藏  举报
/* 鼠标点击求赞文字特效 */ /*鼠标跟随效果*/