第7章 函数装饰器和闭包

本章的最终目标是介绍清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。

7.1 装饰器基础知识
装饰器就是函数,给另一个函数装饰的,会处理被装饰的函数,然后把它返回,返回的函数可能会被替换另外一个函数或对象。
它的两大特性:
  • 能把被装饰的函数替换成其他函数。
  • 装饰器在加载模块时立即执行。

7.2 Python何时执行装饰器
函数装饰器在导入模块时立即执行,而被装饰器函数只在调用时运行。

7.3 使用装饰器改进“策略”模式
#利用装饰器在加载模块时,立即执行的特性,将被装饰的函数添加到promos列表中
promos = []

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

@promotion              #被@promotion装饰的函数都会添加到promos列表中
def fidelity_promo(order):
    """5% discount for customers with 1000 or more fidelity points"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item_promo(order):
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount

@promotion
def large_order_promo(order):
    """7% discount for orders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0

def best_promo(order):
    """Select best discount available
    """
    return max(promo(order) for promo in promos)  
7.4 变量作用域规则
局部变量和全局变量

7.5 闭包
只有当嵌套函数中才会需要处理外部变量,且这个外部变量不是全局变量,这时才会用到闭包。

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定。自由变量,指未在本地作用域中绑定的变量。
#例子说明
#测试示例
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
7.6 nonlocal声明
前面实现make_averager函数的方法效率不高。在多次调用函数make_averager,将10、11、12都存放在列表中,再求平均值。更好的实现方式是,只存储目前的总值和元素个数,再计算两个数平均值。

#计算移动平均值的高阶函数,不保留所有历史值,但有缺陷
>>> def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  File "<pyshell#33>", line 1, in <module>
    avg(10)
  File "<pyshell#31>", line 5, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment
保存原因:当count是数字或任何不可变类型时,count += 1中的count会被赋值,是一个局部变量,这样就不能保留在闭包里。而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
7.7 实现一个简单的装饰器
#一个简单的装饰器,输出函数的运行时间
#clockdeco.py
import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8f] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked
#使用clock装饰器
# clockdeco_demo.py

import time
from clockdeco 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))
运行结果:
**************************************** Calling snooze(.123)
[0.12277146] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000342] factorial(1) -> 1
[0.00006899] factorial(2) -> 2
[0.00011832] factorial(3) -> 6
[0.00016308] factorial(4) -> 24
[0.00020299] factorial(5) -> 120
[0.00024718] factorial(6) -> 720
6! = 720
上述示例有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name__和__doc__属性,使用functools.wraps装饰器可以把相关属性从func复制到clocked中,还能处理关键字参数。
# clockdeco2.py

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - 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
7.8 标准库的装饰器
接下来,我们重点介绍functools模块中的两个装饰器:lru_cache和singledispath。

使用functools.lru_cache做备忘
特点:可以把耗时的函数结果保存起来,避免传入相同的参数时重复计算。
#使用缓存实现,速度更快
import functools 

from clockdeco import clock

@functools.lru_cache()    #lru_cache可以加参数,后面要加括号
@clock                    #这里放了叠加装饰器:@lru_cache()应用到@clock返回的函数上
def fibonacci(n):
     if n < 2 
          return n
     return fibonacci(n-2) + fibonacci(n-1)

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

单分派泛函数
假设我们再开发一个调试Web应用的工具,我们想生存HTML,显示不同类型的Python对象。但Python不支持重载方法或函数,所以我们会使用到functools.singledispatch装饰器,将普通函数变成泛函数。
#singledispatch创建一个自定义的htmlize.register装饰器,把多个函数绑在一起组成一个泛函数。
from functools import singledispatch
from collection import abc
import numbers
import html

@singledispatch              #@singledispatch标记出来object类型的基函数
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)       #各个专门函数使用@htmlize.register装饰
def _(text):                 #专门函数的名称无关紧要;_是个不错的选择
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)  #为每个需要特殊处理的类型注册一个函数,numbers.Integral是int的虚拟超类
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)     #可以叠放多个register装饰器,让同一个函数支持不同类型
@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>'
注意:singledispatch机制的一个显著特性,可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。

7.9 叠放装饰器
把@d1和@d2两个装饰器按顺序应用到f函数上,作用相当于f = d1(d2(f))。

7.10 参数化装饰器
#在clock装饰器的上,再添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出。
# clockdeco_param.py

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):         #clock是参数化装饰器工厂函数
    def decorate(func):             #decorate是真正的装饰器
        def clocked(*_args):        #clocked包装被装饰的函数
            t0 = time.time()
            _result = func(*_args)  #_result是被装饰的函数返回的真正结果
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  #_args是clocked的参数,args是用于显示的字符串
            result = repr(_result)         #result是_result的字符串表示形式,用于显示
            print(fmt.format(**locals()))  #这里使用**locals()是为了在fmt中引用clocked的局部变量
            return _result    #clocked会取代被装饰的函数,因此它应该返回被装饰的函数返回的值
        return clocked        #decorate返回clocked
    return decorate           #clock返回decorate

if __name__=='__main__':

    @clock()                  #不传入参数调用clock(),因此应用的装饰器使用默认的格式str
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)
运行结果:
[0.13200426s] snooze(0.123) -> None
[0.13200426s] snooze(0.123) -> None
[0.13100410s] snooze(0.123) -> None
受本书篇幅限制,我们队装饰器的探讨到此结束。装饰器最好通过实现__call__方法的类实现,不应该像本章的示例通过函数实现。这里是使用函数解说,更容易理解。

7.11 本章小结
***

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值