流畅的python笔记(七)函数装饰器和闭包

目录

前言

一、装饰器基础知识

二、python何时执行装饰器

三、使用装饰器改进策略模式

四、变量作用域规则

五、闭包

六、nonlocal声明

七、实现一个简单的装饰器

八、标准库中的装饰器

使用functools.lru_cache做备忘

单分派泛函数(functools.singledispatch实现)

九、叠放装饰器

十、接受参数的装饰器

一个参数化的注册装饰器(装饰器工厂函数实现)

参数化clock装饰器(函数金字塔)


前言

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。要掌握装饰器,必须了解闭包,必须知道nonlocal关键字。

一、装饰器基础知识

装饰器是可调用的对象,其参数是被装饰的函数,因此装饰器可以认为是一种特殊的高阶函数。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

        假如有个名为decorate的装饰器:

@decorate
def target():
    print("running target()")

上述代码如下述代码效果一样:

def target():
    print("running target()")

target = decorate(target)

即用一个高阶函数decorate处理了target,最后返回的target不一定是最开始的target了。如下例子我们定义的装饰器返回了一个跟参数不一样的函数,因此调用target之后返回结果改变了。可以看到最后得到的target实际上是decorate函数的引用。


def decorate(func):
    def deco():
            print("running decorate()")
    return deco


@decorate
def target():
    print("running target()")

target()
print(target)

         装饰器只是语法糖,其实就是一个可调用的高阶函数。装饰器有两大特性,第一是能把被装饰的函数替换成其他函数,二是在加载模块时立即执行。

二、python何时执行装饰器

python在导入模块时运行装饰器,即只要.py模块被加载进内存,装饰器函数立即运行。

如上例子可以看到,当把registration.py当作脚本运行的时候,刚把模块加载进内存还没运行main函数之前,装饰器就已经运行了。

        若只是在其他模块中导入registration模块,则main函数不会运行,但是装饰器依然运行了。 

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

        以上只是为了演示而做的例子,真实装饰器使用一般采用以下两种方式:

  • 装饰器在一个单独的模块中定义,然后应用到其它模块的函数上。
  • 装饰器一般在内部定义一个新的函数然后返回,而不是直接返回原函数。

三、使用装饰器改进策略模式

第六章中有一个问题是新增策略函数后可能忘记将其添加到promos列表中,因此求最佳策略的时候可能忽略新策略。我们可以将promos列表中的值用promotion装饰器填充。

以上例子中,我们每添加一个新策略,都会将其用装饰器装饰,这样当我们加载模块时会自动运行装饰器,然后就将左右策略函数都加入了promos列表。

四、变量作用域规则

例1:一个函数,读取一个局部变量和一个全局变量

可以看到打印b的时候出错了,因为全局变量b并不存在

例2:先给全局变量b赋值,再调用函数,可以看到,调用成功了,打印出了a和b的值。

例3:在函数体内给变量b赋值

可以看到,虽然我们实现给全局变量b赋值了,但是依然没有成功打印出b。这是因为我们在函数体内给b赋值了,python假定在函数体内赋值的变量是局部变量。因此在打印b的时候把b当成了局部变量,而局部变量b的赋值语句在打印语句下边,因此打印时找不到局部变量b。

        若既想在函数体中赋值,又想让解释器把b当成全局变量,则需要使用global声明:

 

五、闭包

首先明确定义:闭包是指延伸了作用域的函数。

实现闭包有三个必要条件:

  1. 必须嵌套函数
  2. 内部函数要引用外部变量,这个外部是相对外部,即在外部函数的函数体中定义,在内部函数的函数体中引用
  3. 外部函数的返回值是内部函数

此时,这个内部函数就被称为闭包。

如上例中,外部函数是make_averager,内部函数是averager,内部函数averager引用的外部变量就是series列表,这个变量也被称为自由变量。

虽然调用完make_averager函数返回以后,其本地作用域已经不存在了, 但是由于内部函数绑定了series变量,于是这个变量的值被保存了下来。这个列表具体保存再哪里呢?

        在返回的函数对象avg中,其__code__属性中的co_varnames成员和co_freevars成员中分别保存有函数的局部变量和自由变量的名称。

如上如所示,可以看到函数的自由变量名称中有series, 但这里存储的只是名字,真正的列表在avg.__closure__属性中。avg.__closure__中的元素是cell对象,有个cell_contents属性,保存着真正的自由变量值。即自由变量保存在闭包中

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用闭包函数时,即使自由变量的定义作用域不可用了,但是仍能使用那些绑定。

六、nonlocal声明

上一小节中求平均值我们是存储了一个列表然后求均值,但是更好的做法是只保存当前的总值和元素个数,然后即可求均值。

上边例子错误原因是,count是不可变的类型,因此当我们在内部用 count += 1时并不是一个原地操作,相当于count = count + 1,这个时候相当于构造了一个新的变量count,而这个新的count是内部函数中的局部变量了,而不再是自由变量。

        上一小节中没有遇到这个问题,是因为我们没有给series赋值,我们只是调用series.append,并把它传给sum和冷,也就是说,我们利用了列表是可变的对象这一事实。但是对于数字、字符串、元组等不可变类型来说,如果尝试重新绑定,比如 count = count + 1,就会隐式创建局部变量count,这样count不再是自由变量,因此不会保存在闭包中。为了解决这个问题,python3引入了nonlocal声明,其作用是把变量标记为自由变量,如果为nonlocal声明的变量赋予新值,闭包中保存的绑定也会更新

七、实现一个简单的装饰器

# 一个简单的装饰器,输出函数的运行时间

import time

def clock(func):
    def clocked(*args): # 装饰器函数的内部函数,应该是个闭包,闭包的参数即被装饰函数的参数
        t0 = time.perf_counter()
        result = func(*args) # 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 clocked

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 n < 2: 
        return 1
    else:
        # print(n - 1)
        return n * factorial(n-1)

if __name__ == '__main__':
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6 != ', factorial(6))

当我们用装饰器装饰一个函数,即相当于我们调用装饰器修改了这个函数。如果我们查看factorial的__name__属性,则会看到已经变成了clocked:

所以,factorial保存的是clocked函数的引用,自此之后,每次调用factorial(n),执行的都是clocked(n)。

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

        上面实现的clock装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数(factorial)的__name__和__doc__属性。下面用functools.wraps装饰器把相关属性从被装饰函数func复制到clocked中,且正确处理关键字参数。

这样在装饰器内部只是简单地用标准库装饰器functools.wraps装饰了一下闭包clocked(wraps的参数是func和clocked),就将func的__name__和__doc__等属性复制给了clocked。

八、标准库中的装饰器

python内置装饰器:property,classmethod,staticmethod

常见装饰器:functools.wraps,functools.lru_cache,functools.singledispatch

使用functools.lru_cache做备忘

记住一点即可,lru_cache装饰器可以缓存结果,避免传入相同参数时重复计算。

        比如生成第n个斐波那契数这种慢速递归函数。

 

 

可以看到重复计算可很多次,fibonacci(1)调用了8次,fibonacci(2)调用了5次。可以使用lru_cache装饰器避免这些重复计算。

 

需要注意的有两点:

  • 一是使用lru_cache时候后边要带上括号,就像调用普通函数一样,这是因为lru_cache可以接受配置参数。上一小节中的functools.wraps也是,有括号有参数。
  • 二是这里叠放了装饰器,即@lru_cache用来装饰@clock返回的函数上。

用lru_cache改进之后结果如下:可以看到去掉了重复计算。

 lru_cache的两个可选配置参数为maxsize和typed,如下所示:

  • maxsize用来指定缓存多少个调用的结果,如果缓存满了,则旧的结果会被扔掉,maxsize应该设为2的幂。
  • typed参数是布尔值,如果设置为True,则会把不同类型的结果分开保存,比如浮点数1.0和整数1区分开保存。
  • lru_cache存储结果时用字典,且键是调用时传入的定位参数和关键字参数,因此lru_cache装饰的函数,其所有参数都必须是可散列的。

单分派泛函数(functools.singledispatch实现)

需要:根据参数的不同类型进行不同的操作。在C++中,这种需求可以使用函数重载来解决,但是python中不支持重载方法或函数(python是动态类型语言,函数定义时其函数参数没有类型)。常用的做法是用分派函数------即用一系列的if/elif/elif进行条件判断,每个条件分支对应一段处理逻辑。但是这样做不利于模块的用户扩展,且时间一长,分派函数会变得很大。因此python引入了functools.singledispatch装饰器。

  1.  @singledispatch装饰了处理object类型的基函数(base_function)
  2.  各个专门的函数使用 base_fucntion.register(type)来装饰
  3. 专门函数的名称不重要,用 _ 做个占位符即可。
  4. 为每个需要特殊处理的类型都用base_function.register()注册了一个函数,函数参数是类型名,numbers.Integral是int的虚拟超类
  5. 可以叠放多个register装饰器,让同一个函数支持不同的类型。

注册的专门函数应该处理抽象基类(抽象基类和虚拟子类的概念11章讨论),而不要处理具体实现的类别。这样代码兼容更广泛。比如numbers.Integral,python可以将其子类化,使用固定位数可以实现int类型。

        singledispatch机制的一个显著特征是,可以在系统的任何地方和任何模块中注册专门函数,如果后来在新的模块中定义了新的类型,可以轻松地注册一个新的专门函数处理那个类型。还可以为不是自己编写地或者不能修改地类添加自定义函数。

九、叠放装饰器

装饰器是一个返回函数的函数,因此可以组合起来使用,用一个外层装饰器来装饰内层装饰器返回的函数。

把@d1和@d2两个装饰器按顺序应用到f函数上,相当于 f = d1(d2(f)),即:

十、接受参数的装饰器

python把被装饰的函数作为第一个参数传给装饰器,要让装饰器接受其他参数的方法是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后将返回的装饰器应用到要装饰的函数上。

现在来修改本章开始时候的一个示例装饰器register:

 一个参数化的注册装饰器(装饰器工厂函数实现)

本质就是给装饰器传入一个新的参数active,用来控制装饰器是否装饰函数。

 

  1.  装饰器要实现的功能就是将函数注册到registry集合中
  2.  register是装饰器工厂函数,我们给它加了一个参数active,用来控制装饰器的行为
  3.  decorate是真正的装饰器函数,因此其参数是func,即要装饰的函数
  4.  active控制装饰器函数是否装饰func(即是否加入到集合registry中)
  5.  如果active == False,且func已经在集合中,则清除出去
  6.  装饰器函数需要返回一个函数,这里返回的是原函数
  7.  因为register函数是装饰器工厂函数,因此其返回值是真正的装饰器函数

其实这种实现模式并不是真正意义上将更多的参数传给了装饰器,而是通过装饰器工厂函数和参数来得到了不同的装饰器,每种参数对应的装饰器功能不同。

        如果不用@语法,那么可以用普通函数调用的方式来使用装饰器工厂函数,要得到装饰与不装饰函数f的装饰器语句如下:

  • register()(f)
  • register(active=False)(f)

参数化clock装饰器(函数金字塔)

本质上还是要借用装饰器工厂函数来实现。

前边小节中的clock装饰器,添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出。

 

  1. clock是参数化装饰器工厂函数
  2. decorate是真正的装饰器
  3. clocked包装被装饰的函数,即装饰器的返回值
  4. _result是被装饰函数的返回结果
  5. _args是clocked的参数,也是被修饰的函数func的参数,args是用于显示的字符串
  6. result是_result的字符串表示形式,用于显示
  7. 这里使用**locals()是为了在fmt中引用clocked的局部变量
  8. clocked会取代被装饰的函数,其返回值一般都等于被装饰函数func的返回结果
  9. 装饰器要返回被装饰之后的函数
  10. 装饰器工厂函数要返回装饰器函数
  11. 没有传入参数(fmt)直接调用的装饰器工厂函数,返回结果如下:

用了格式字符串参数之后测试结果如下

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值