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. 装饰器在加载模块时立即执行

1310818-20180413133156076-626627464.png

可以看出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中存历史值。

1310818-20180413193530947-1645748130.png

总结:

  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('\n', '<br>\n')
    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>\n</li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</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程序中使用函数装饰器实现装饰器模式,但是实现装饰器模式更好使用类表示装饰器和要包装的组件。

转载于:https://www.cnblogs.com/allen2333/p/8848010.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值