• Fluent_Python_Part3函数即对象,07-closure-decoration,闭包与装饰器


    第7章 函数装饰器和闭包

    装饰器用于在源码中“标记”函数,动态地增强函数的行为。
    了解装饰器前提是理解闭包。
    闭包除了在装饰器中有用以外,还是回调式编程和函数式编程风格的基础。

    1. 装饰器基础知识

    1. 装饰器是callable对象,其参数是被装饰的函数。
    2. 装饰器将被装饰的函数处理后返回,或者将被装饰的函数替换成另一个函数或可调用对象。
    3. Python也支持类装饰器,参见第21章。
      4. 第一大特性:装饰器能把被装饰的函数替换成其他函数
      5. 第二大特性:装饰器在加载模块时立即执行。看例子3,和下一小节详细说明
      6. 严格来说,装饰器只是语法糖。

    例子1. 效果一样的写法:

    #假设有个名为decorate的装饰器
    @decorate
    def target():
        pass
    
    def target():
        pass
    
    target = decorate(target)
    

    例子2. 装饰器通常把函数替换成另一个函数

    #deco函数返回inner函数对象
    def deco(func):
        def inner():
            print('running inner()')
        return inner
    
    #使用deco装饰器装饰target
    @deco
    def target():
        print('running target()')   
    
    #调用被装饰的target()会运行inner()
    target()
    #查看target地址,其实是inner()的引用
    target
    

    例子3. 装饰器在加载模块时立即执行

    可以看出I am deco和running traget()立即输出。

    2. Python何时执行装饰器

    1. 装饰器的第二大特性:它们在被装饰的函数定义之后立即运行(联想到Flask框架中的URL映射和绑定)。通常是在导入时(即Python加载模块时)。
    2. 大多数装饰器会在内部定义一个函数,然后将其返回。但有些装饰器返回被装饰的函数,很多Python Web框架使用这样的装时期把函数添加到注册处,例如把URL模式映射到生成HTTP响应的函数上的注册处。这种注册装饰器可能会也可能不会修改被装饰的函数。

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

    1. 改进第6章的5.1。
    2. 原本的hardcode,当有新的策略(新的promotion)的时候,要手动添加。现在用装饰器装饰新的策略,自动添加进列表。
    3. 优点1:促销策略函数无需使用特殊的名称(例如不同_promo结尾)
    4. 优点2: 临时禁用某个促销策略,只需要把装饰器注释掉。
    5. 优点3:促销折扣策略可以在其他模块中定义,模块化。
    promos = []
    def promotion(promo_func):
        promos.append(promo_func)
        return promo_func
    
    @promotion
    def pro1():
        pass
    
    @promotion
    def pro2():
        pass
    
    #类也可以被装饰
    @promotion
    class pro3():
        pass
    
    #注意,方法和类存入列表的形式不同
    #选择最佳策略的函数不变
    def best_promo():
        """选择可用的最佳折扣"""
        return max(promo(order) for promo in promos)
    

    4. 变量作用域

    1. 为理解闭包,先了解Python中的变量作用域。

    例子1. b是局部变量,因为在函数的定义体中给它赋值了

    b = 6
    #到print(b)时会报错
    def f2(a):
            print(a)
            print(b)
            b = 9
    

    原因:
    在函数中给b赋值了,Python认为它是局部变量。当获取并打印局部变量a后,尝试获取局部变量b的值时,发现b没有绑定值。

    1. 这不是缺陷,这是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。
    2. 这比JavaScript的行为好多了,JavaScript也不要求声明变量(在函数中使用var关键字进行显式申明的变量是作为局部变量,而没有用var关键字,使用直接赋值方式声明的是全局变量),但是如果忘记把变量声明为局部变量(使用var),可能在不知情的情况下获取全局变量。

    例子2. 在函数中,如果让解释器把b当成全局变量,要使用global声明:

    b = 6
    def f(a):
        global b
        print(a)
        print(b)
        b = 9
    
    f(3)  # 3 9
    
    b     # 9
    

    例子3. 用dis.dis查看例子1和2的字节码的不同

    #反汇编模块
    from dis import dis
    dis(s)
    
    #LOAD_FAST是读本地变量进栈,LOAD_GLOBAL读取全局变量, 区别在b处的操作不同。
    

    5. 闭包(closure)

    1. 闭包是指延伸了作用域的函数,其中包括函数定义体中的引用、定义体之外的非全局变量。
    2. 闭包和匿名函数:在内部里定义函数,只有涉及嵌套函数才有闭包问题。内部函数是不是匿名没有关系。
    3. 闭包关键是能访问定义体之外定义的非全局变量(自由变量)。

    问题:自定义avg函数,计算不断增加的系列值的均值,关键是如何保存历史值

    方案1: 用类实现计算平均值

    class Average():
        def __init__(self):
            #实例初始化时初始化一个列表
            self.series = []
        
        def __call__(self, new_value):
            self.series.append(new_value)
            #归约函数sum()
            total = sum(self.series)
            return total/len(self.series)
    
    avg = Average()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    

    例子2. 函数式实现,计算平均值的高阶函数

    def make_average():
        #series是make_average的局部变量,因为在这个函数定义体中初始化了series
        series = []
        
        def average(new_value):
            series.append(new_value)
            total = sum(series)
            return total/len(series)
        
        return average
    

    例子1在self.series实例属性存储历史值。例子2在自由变量(free variable)series中存历史值。

    总结:

    1. 闭包是引用了自由变量的函数。
    2. In computer programming, the term free variable refers to variables used in a function that are not local variables nor parameters of that function
    3. free variable: variables that are used locally, but defined in an enclosing scope

    6. nonlocal声明

    例子1. 有缺陷的计算平均值的高阶函数

    def make_average():
        count = 0
        total = 0
    
        def average(new_value):
            count += 1
            total += new_value
            return total/count
    
        return average
    
    avg = make_average()
    #报错,当count是不可变类型时,因为count += 1 相当于 count = count + 1。我们在average的定义体中为count赋值,Python当成它是局部变量,total也受到这个问题影响。
    #series.append没遇到这个问题,因为利用了列表是可变对象这一事实,没有给series赋值。
    avg(1)
    

    对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1, 会隐式创建局部变量count,这样count再不是自由变量了,也不存在闭包了。
    为了解决这个问题,Python3引入了nonlocal声明。它的作用是把变量标记为自由变量。

    def make_average():
        count = 0
        total = 0
    
        def average():
            nonlocal count, total
            count += 1
            total += new_value
            return total / count
    
        return average
    

    7. 实现一个简单的装饰器

    例子1. 把经过的时间、传入的参数、调用的结果打印出来

    #b.py
    import time
    
    def clock(func):
        
        #定义内部函数clocked, 它接受任意个位置参数
        def clocked(*args):
            start = time.perf_counter()
            #这行代码可用,因为clocked的闭包中包含自由变量func
            result = func(*args)
            elapsed = time.perf_counter() - start
            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
    
    
    #a.py
    import time
    from b import clock
    
    @clock
    def snooze(seconds):
        time.sleep(seconds)
    
    @clock
    def factorial(n):
        return 1 if n < 2 else n * factorial(n-1)
    
    if __name__ == '__main__':
        print('*' * 40, 'Calling snooze(.123)')
        snooze(.123)
        print('*' * 40, 'Calling factorial(6)')
        print('6! =', factorial(6))
    

    此处clock函数的缺点:

    1. 不支持关键字参数
    2. 掩盖了被装饰函数的__name__和__doc__属性

    由这个例子看出
    这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,通常返回被装饰函数本该返回的值,同时还做些额外的操作

    例子2. 解决上个例子的两个缺点

    1. 用functools.wraps装饰器把相关的属性从func复制到clocked中
    2. 此外,这个版本还能正确处理关键字参数
    import time
    import functools
    
    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 = ['[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result]
            return result
        
        return clocked
    

    8. 标准库中的装饰器

    1. Python内置了三个装饰器property(在19.2节讨论)、classmethod(在9.4节讨论)、staticmethod(在9.4节讨论)。
      2. 另一个常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器,如上面的例子。
    2. 标准库中最值得关注的是functools.lru_cache和functools.singledispatch装饰器。

    8.1 functools.lru_cache()

    例子1. 生成第n个斐波那契数
    递归方式非常耗时,中文电子书P323

    #clock为上面例子中的装饰器
    from clockdeco import clock
    def fibonacci(n):
        if n < 2:
            return n
        return fibonacci(n-2) + fibonacci(n-1)
    
    if __name__ == '__main__':
        print(fibonacci(6))
    

    例子2. 优化例子1

    1. functools.lru_cache实现了备忘录(memoization)功能的装饰器。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同参数时重复计算。LRU代表“Least Recently Used”,表明缓存不会无限制增长,一段时间不用的条目会被扔掉。
    2. functools.lru_cache适合优化例子1这种慢速递归函数。
    3. functools.lru_cache在Web中获取信息的应用中也能发挥巨大作用。
    4. functools.lru_cache有两个可选的配置参数(maxsize=128, typed=False)
    5. functools.lru_cahe用字典存储结果,并且key根据调用时传入的位置参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。
    import functools
    from clockdeco import clock
    
    #lru_cache加()的原因是,lru_cache可以接受配置参数。
    @functools.lrucache()
    #这里叠放了装饰器:@lru_cache()应用到@clock返回的函数上。
    @clock
    def fibonacci(n):
        if n < 2:
            return n
        return fibonacci(n-2) + fibonacci(n-1)
    
    if __name__ == '__main__':
        print(fibonacci(6))
    
    

    8.2 functools.singledispatch装饰器, 单分派泛函数

    1. 中文电子书P326,根据不同的Python对象输出不同格式的HTML
    2. 问题:因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体。
    3. 笨拙的解决方法:把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int等。但这样不利于模块拓展,htmlize会变得很大,而且它与各个专门函数之间的耦合也很紧密。
      解决方法2. Python3.4新增的(PyPi中的包可以向后兼容)functools.singledispatch装饰器可以把整体方案拆成多个模块。使用@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型(一个参数为单分派,多个参数为多分派),以不同的方式执行相同操作的一组函数。
    1. Java的重载和if/elif/elif定义的分派函数的缺点是代码单元(类或函数)承担的职责过重。singledispatch的优点是支持模块化拓展,各个模块可以为它支持的各种类型注册专门函数。
    from functools import singledispatch
    from collections import abc
    import numbers
    import html
    
    # 1.singledispatch标记处理object类型的基函数。htmlize变成了泛函数
    @singledispatch
    def htmlize(obj):
        content = html.escape(repr(obj))
        return '<pre>{}</pre>'.format(content)
    
    # 2.各个专门函数用@<<base_function>>.register(<<type>>)装饰
    @htmlize.register(str)
    # 3.专门函数的名称无关紧要
    def _(text):
        content = html.escape(text).replace('
    ', '<br>
    ')
        return '<p>{0}</p>'.format(content)
    
    # 4.numbers.Integral是int的虚拟超类
    @htmlize.register(numbers.Integral)
    def _(n):
        return '<pre>{0} (0x{0:x})</pre>'.format(n)
    
    # 5.叠放多个register装饰器,让一个函数支持不同的类型。
    @htmlize.register(tuple)
    @htmlize.register(abc.MutableSequence)
    def _(seq):
        inner = '<li>
    </li>'.join(htmlize(item) for item in seq)
        return '<ul>
    <li>' + inner + '</li>
    </ul>'
    
    

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

    使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类。在第11章讨论。

    singledispatch提供的特性很多,查看PEP443"Single-dispatch generic functions"

    9.叠加装饰器

    @d1和@d2按顺序应用到f函数上,相当于f = d1(d2(f))

    @d1
    @d2
    def f():
        pass
    #等同于
    def f():
        pass
    f = d1(d2(f))
    

    10. 参数化装饰器

    1. 解析源码中的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数。问题:怎么让装饰器接受其他参数?
    2. 解决方案:创建一个装饰器的工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

    例子1. 给出registration.py模块的删减版,便于讲解

    registry = []
    
    def register(func):
        print('running register(%s)' % func)
        registry.append(func)
        return func
    
    #载入模块时就运行装饰器
    @register
    def f1():
        print('running f1()')
    
    print('running main()')
    print('registry ->', registry)
    print(f1)
    

    例子2.为例子1的register添加一个可选的active参数,便于启用或禁用register执行的函数注册功能。
    新的register函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。

    # 1.set对象添加和删除速度更快
    registry = set()
    # 2.register是一个装饰器工厂函数,返回一个装饰器,接受一个可选的关键字参数
    def register(active=True):
        # 3.decorate这个内部函数才是真正的装饰器;它的参数是被装饰的函数
        def decorate(func):
            print('running register(active=%s) -> decorate(%s)' % (active, func))
        # 4.active是自由变量,从decorate的闭包中获取。只有True时才注册func
            if active:
                registry.add(func)
            else:
                # 5.如果active不为真,而且func在registry中,那么把它删除
                registry.discard(func)
            # 6.decorate是装饰器,返回一个函数
            return func
        # 7. register是工厂函数,返回decorate装饰器
        return decorate
    
    # 8. @register工厂函数必须作为函数调用,并且传入可选参数
    @register(active=False)
    def f1():
        print('running f1()')
    
    @register(active=True)
    def f2():
        print('running f2()')
    
    def f3():
        print('running f3()')
    
    # print(registry)
    # print(f1())
    # print(f2())
    # print(f3())
    

    这里的关键是,register()是一个装饰器工厂函数,返回decorate装饰器,把它应用到被装饰的函数上。(思考:Flask中的URL映射也是这样?装饰器的.index()是工厂函数。一般情况下,装饰器作为函数调用,即有括号(),就是工厂函数?)

    11. 参数化clock装饰器

    1. 中文电子书P334

    总结:

    1. 大部分工业级的装饰器比上述所有例子都要复杂。参数化装饰器至少涉及两层嵌套函数。
    2. Graham Dumpleton和Lennart Regebro认为,装饰器最好通过实现__call__方法的类实现。作者同意使用它们建议的方式实现非平凡的装饰器, 使用函数解说这个语言特性的基本思想更容易理解。
    3. 真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的nonlocal(重新绑定既不在本地作用域中也不在全局作用域中的名称)声明。
    4. 就拓展功能而言,装饰器模式比子类化更灵活。
    5. 在实现层面,Python装饰器与装饰器涉及模式不同,但有相似之处。在特定情况下,Python程序中使用函数装饰器实现装饰器模式,但是实现装饰器模式更好使用类表示装饰器和要包装的组件。
  • 相关阅读:
    八大排序算法
    大数据系列文章汇总
    HBase原理、设计与优化实践
    hbase分页应用场景及分页思路与代码实现
    【HBase】zookeeper在HBase中的应用
    HBase什么时候作minor major compact
    Namenode HA原理详解(脑裂)
    【版本特性】sql server2005版本特性
    Navicat破解版下载安装
    MongoDB(2.2)MongoDB的安装与基本使用
  • 原文地址:https://www.cnblogs.com/allen2333/p/8848010.html
Copyright © 2020-2023  润新知