美文网首页
Fluent Python笔记--装饰器与闭包

Fluent Python笔记--装饰器与闭包

作者: MontyOak | 来源:发表于2018-01-13 19:33 被阅读17次

装饰器的本质很简单,它本质是一个函数,入参是另一个函数:

@decorate
def target():
    pass

# 上面的写法等价于
def target():
    pass

target = decorate(target)

被装饰过的函数不一定是原先的函数,有可能是被替换过之后的函数,但更为常见的场景是在原有函数的基础上完成了一些额外的工作。
另外需要注意的是,装饰器的执行时间,它的执行时间和其他定义的函数不同。装饰器在被装饰函数定义之后立即执行,通常可以认为是导入时执行

registry = [] 

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() 

执行上面代码:

>>> python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

许多Python web框架(例如flask)通常会使用装饰器来完成路由注册的工作,除了将URL和函数绑定起来之外,装饰器在这里并不对函数做替换。
之前提高过的策略模式,在best_promo函数那里,有时会因为没有注册新的策略导致最佳策略结果有误,而注册管理这种事情和打折策略本身又没有关系,属于额外的工作,非常适合使用装饰器来解决:

promos = [] 

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

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

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

@promotion
def large_order(order):
    """订单中的不同商品达到10个或以上时提供7%折扣"""
    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)

这里的装饰器起到了注册策略的作用,如果想要禁用某个策略,简单的把那个函数的装饰器去掉就可以了。

接下来简单说一下闭包的概念。闭包本质是对函数作用域的延展,它包含函数定义体中引用、但并不在定义体中定义的非全局变量。
简单来说就是,在两个嵌套函数的场景下,内层函数可以访问定义在内外层函数之间的变量。
比如下面的例子:

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

# 使用方法
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5

在上面的averager函数中,series是自由变量,它是指在本地作用域中绑定的变量。

averager函数的闭包图示
那么,怎么知道函数有哪些自由变量呢?
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

所以,闭包实质是函数,它保留定义函数时的自由变量的绑定,调用函数时,虽然定义作用域不可用,但是还有绑定可以使用。
在Python3中,nonlocal用于声明自由变量:

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

在常见初级python面试中,关于python装饰器常常会要求实现一个装饰器,打印被装饰方法的执行耗时:

def clock(func):
    import time
    def clocked(*args): 
        t0 = time.time()
        result = func(*args) 
        t1 = time.time() 
        elapsed = t1 - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked 

改进一下上面的装饰器,让它支持关键字参数:

def clock(func):
    import time
    import functools
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        t1 = time.time() 
        elapsed = t1 - 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.lru_cache做缓存
    functools.lru_cache实现了备忘的功能,LRU(Least Recently Used)表明备忘的数量不会无限增长,过一段时间或者超过一定数量就会被清除。
    下面是斐波那契数列的例子:
from clockdeco import clock

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

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

# 运行上面代码
>>> python3 fibo_demo.py
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00007892s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00003815s] fibonacci(2) -> 1
[0.00007391s] fibonacci(3) -> 2
[0.00018883s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00004911s] fibonacci(2) -> 1
[0.00009704s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00002694s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00005102s] fibonacci(2) -> 1
[0.00008917s] fibonacci(3) -> 2
[0.00015593s] fibonacci(4) -> 3
[0.00029993s] fibonacci(5) -> 5
[0.00052810s] fibonacci(6) -> 8
8

不难看出,这里出现了大量的重复计算。这也是递归的一个特点:递归基会被多次重复调用。
接下来看一下lru_cache的优化版本:

import functools
from clockdeco import clock

@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))

# 运行上面代码
>>> python3 fibo_demo_lru.py
[0.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8

可以看到,lru_cache方法减少了重复运算,从而带来了运行时间的减少。

接下来,简单说一下singledispatch这个装饰器(之前有篇文章介绍过)。
简单来说,functools.singledispatch主要用于将整体方案拆分成多个模块,用于应对多种情况,减少长串的if...elif...elif......的代码块。被它装饰的函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的同一组函数。

下面展示带参数的装饰器。

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) 
        return func 
    return decorate 

@register(active=False) 
def f1():
    print('running f1()')

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

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

# 使用上面代码
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)
>>> registration_param.registry
{<function f2 at 0x10063c268>}

相关文章

  • Fluent Python笔记--装饰器与闭包

    装饰器的本质很简单,它本质是一个函数,入参是另一个函数: 被装饰过的函数不一定是原先的函数,有可能是被替换过之后的...

  • Fluent Python 笔记 —— 装饰器和闭包

    装饰器 函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。它是一种以另一个函数(被装饰的函数)为参数的...

  • Python—闭包与装饰器

    将之前学习Python的笔记整理记录下来。 闭包 装饰器

  • Python的自定义超时机制——装饰器的妙用

    装饰器 关于装饰器的入门,可以参考这篇文章:12步轻松搞定python装饰器简单来说,装饰器其实就是一个闭包(闭包...

  • 2020-012 python闭包与装饰器

    python闭包与装饰器 闭包 函数和对其周围状态(lexical environment,词法环境)的引用捆绑在...

  • Python装饰器-专题笔记

    学会装饰器,Python更进阶 函数作用域到闭包到装饰器讲解,及闭包和装饰器的运用。 [√] 慕课网Meshare...

  • [python] 装饰器学习

    很多python的代码都带有装饰器=。=现在不学以后也要学学一下装饰器 闭包 在学装饰器之前先看看python的闭...

  • Python 2 - 高级用法 - 装饰器

    Python 2 - 高级用法 - 装饰器 一谈到 装饰器,就离不开闭包 闭包 闭包就是能够读取其他函数内部变量的...

  • Python装饰器——初识

    上次讲了Python的闭包,今天来讲一下闭包的应用——装饰器 1. 装饰器是什么 什么叫装饰器?顾名思义,它是一个...

  • python装饰器

    装饰器简述 要理解装饰器需要知道Python高阶函数和python闭包,Python高阶函数可以接受函数作为参数,...

网友评论

      本文标题:Fluent Python笔记--装饰器与闭包

      本文链接:https://www.haomeiwen.com/subject/egncoxtx.html