函数装饰器和闭包


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

装饰器基础知识

加入有个名为decorate的装饰器:

@decorate
def target():
    print('runing target()')

等同于

def target():
    print('runing target()')
target = decorate(target)

Python何时执行装饰器

函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。

registry = []  # 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()

把registration.py当作脚本运行得到的输出:
running register(<function f1 at 0x0000026393C97048>
running register(<function f2 at 0x0000026393C976A8>
running main()
registry-> [<function f1 at 0x0000026393C97048>, <function f2 at 0x0000026393C976A8>]
running f1()
running f2()
running f3()
如果导入registration.py模块(不作为脚本运行),输出:
running register(<function f1 at 0x0000026393C97048>
running register(<function f2 at 0x0000026393C976A8>

闭包

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
averager的闭包延伸到那个函数的作用域之外,包含自由变量series的绑定
averager的闭包延伸到那个函数的作用域之外,包含自由变量series的绑定;

nonlocal声明

前面实现make_averager函数的方法效率不高。在示例7-9中,我们把所有值存储在历史数列中,然后在每次调用averager时使用sum求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。

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

问题是,当count是数字或任何不可变类型时,count+=1语句的作用其实与count=count+1一样。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量。total变量也受这个问题影响。
前面那个没遇到这个问题,因为我们没有给series赋值,我们只是调用series.append,并把它传给sum和len。也就是说,我们利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count=count+1,其实会隐式创建局部变量count。这样,count就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。

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

a = make_average()
print(a(10)) # 10
print(a(15)) # 12.5
print(a(20)) # 15

实现一个简单的装饰器

import time
def clock(func):
    def clocked(*args):  # 定义内部函数clocked,它接受任意个定位参数
        t0 = time.perf_counter()
        result = func(*args) # 这行代码可用,是因为clocked的闭包中包含自由变量func
        elapsed = time.perf_counter() - 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 # 返回内部函数,取代被装饰的函数

使用clock装饰器

from Slot import clock
import time


@clock
def snooze(s):
    time.sleep(s)

@clock
def factorial(n):
    return 1 if n<2 else n*factorial(n-1)

if __name__ == '__main__':
    print('*'*40, 'calling snooze(.123)')
    snooze(0.123)
    print('*' * 40, 'calling factorial(6)')
    print('6!=',factorial(6))
输出:
**************************************** calling snooze(.123)
[0.12279980s]snooze(0.123)->None
**************************************** calling factorial(6)
[0.00000030s]factorial(1)->1
[0.00002310s]factorial(2)->2
[0.00004870s]factorial(3)->6
[0.00006080s]factorial(4)->24
[0.00006750s]factorial(5)->120
[0.00007470s]factorial(6)->720
6!= 720

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。

标准库中的装饰器

Python内置了三个用于装饰方法的函数:property、classmethod和staticmethod,这将在后面讨论。另一个常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器。标准库中最值得关注的两个装饰器是lru_cache和全新的singledispatch。

使用functools.lru_cache做备忘

functools.lru_cache是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉,在递归优化方面意义显著
生成第n 个斐波纳契数,递归方式非常耗时:

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

if __name__ == '__main__':
    print(fibonacci(6))
输出:
[0.00000080s]fibonacci(0)->0
[0.00000070s]fibonacci(1)->1
[0.00005310s]fibonacci(2)->1
[0.00000030s]fibonacci(1)->1
[0.00000040s]fibonacci(0)->0
[0.00000020s]fibonacci(1)->1
[0.00001200s]fibonacci(2)->1
[0.00002520s]fibonacci(3)->2
[0.00009450s]fibonacci(4)->3
[0.00000030s]fibonacci(1)->1
[0.00000040s]fibonacci(0)->0
[0.00000050s]fibonacci(1)->1
[0.00003210s]fibonacci(2)->1
[0.00005820s]fibonacci(3)->2
[0.00000050s]fibonacci(0)->0
[0.00000060s]fibonacci(1)->1
[0.00003290s]fibonacci(2)->1
[0.00000030s]fibonacci(1)->1
[0.00000050s]fibonacci(0)->0
[0.00000060s]fibonacci(1)->1
[0.00011000s]fibonacci(2)->1
[0.00015900s]fibonacci(3)->2
[0.00022510s]fibonacci(4)->3
[0.00031740s]fibonacci(5)->5
[0.00043200s]fibonacci(6)->8
8

浪费时间的地方很明显:fibonacci(1)调用了8次,fibonacci(2)调用了5次……但是,如果增加两行代码,使用lru_cache,性能会显著改善:

import functools
@ functools.lru_cache()  # 必须像常规函数那样调用lru_cache。这一行中有一对括号:@functools.lru_cache( )。这么做的原因是,lru_cache可以接受配置参数
@clock
def fibonacci(n):
    if n<2:
        return n
    return fibonacci(n-2)+fibonacci(n-1)

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

这样一来,执行时间减半了,而且n的每个值只调用一次函数:

输出:
[0.00000050s]fibonacci(0)->0
[0.00000030s]fibonacci(1)->1
[0.00003580s]fibonacci(2)->1
[0.00000070s]fibonacci(3)->2
[0.00004870s]fibonacci(4)->3
[0.00000040s]fibonacci(5)->5
[0.00006120s]fibonacci(6)->8
8

除了优化递归算法之外,lru_cache在从Web中获取信息的应用中也能发挥巨大作用。特别要注意,lru_cache可以使用两个可选的参数来配置。它的签名是:

functools.lru_cache(maxsize = 128,typed = False)

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

functools.singledispatch装饰器

functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。
singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。

叠放装饰器

@ d1
@ d2
def f():
    print('f')

等同于

def f():
    print('f')
f = d1(d2(f))

参数化装饰器

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

import time
if __name__ == '__main__':
    @clock()
    def snooze(s):
        time.sleep(s)
    for i in range(3):
        snooze(.123)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值