函数装饰器与闭包

发布于 2020-02-13 21:14:08   阅读量 121  点赞 1  

一、函数装饰器

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

示例:

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

# 相当于

def target():
    print('running target()')

target = decorate(target)


装饰器执行的时机

 装饰器在被装饰的函数定义之后立即执行,这通常发生在模块加载时;而被装饰的函数只有在 main 明确调用时才执行。

叠加装饰器

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

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

# 等价于

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


参数化装饰器

 工厂函数内部的装饰器通过闭包获取参数,工厂函数必须作为函数调用  由于在使用装饰器时,Pyhon 自动将被装饰函数作为第一个传入装饰器函数,所以若要定义接收配置参数的装饰器,则需定义工厂函数。由工厂函数接受参数传入,返回装饰器,再使用装饰器装饰函数。

示例:

registry = set()

def register(active=True):
    def decorate(func):
        print('running register(active=%s)->decorate(%s)'%(active,func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func

    return decorate

@register(active=False)
def f1():
    print('running f1()')

@register()
def f2():
    print('running f2()')

def f3():
    print('running f3()')
  • 定义一个工厂函数,在工厂函数内定义装饰器;
  • 参数有工厂函数接收,装饰器通过闭包获取参数;
  • 装饰传入的参数依旧为函数;
  • 使用装饰器的时候通过@工厂函数(arg_list)的形式,其中工厂函数(arg_list)返回一个配置好的装饰器,由@调用这个装饰器。   通过闭包可以更好实现装饰器


二、闭包

 闭包指延申了作用域的函数,其中包含了在函数定义体中引用,但是不在定义体中定义的非全局变量(自由变量)。需注意的是,只有涉及嵌套函数时才有闭包问题。

闭包条件:

  1. 在一个外部函数中定义了内函数;
  2. 内函数里运用了外函数声明的临时变量;
  3. 外函数的返回值是内函数的引用。

一般情况下,若一个函数结束,函数内部所有的东西都会被释放,局部变量消失。但闭包是一种特殊情况,当外函数在结束时发现自己的临时变量会在内函数中用到,就把这个临时变量绑定给内部函数,然后再释放其他内存。  示例:

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager


averager = make_averager()
averager(10)
>> 10

averager(20)
>> 15

averager(20)
>> 17.5

# 调用make_averager时,返回一个averager函数对象。每次调用averager时,会将参数添加到系列参数中,计算当前平均值。

averager2 = make_averager()
averager2(20)
>> 20

 观察该函数,发现series变量为外部函数中的局部变量,当将内部函数返回时,其本地作用域已销毁,但其会将内部函数中用到的series变量绑定给内部函数。而在内部函数中,series称作自由变量,指未在本地作用域中绑定的变量

 通过上述例子,对于闭包可以有以下了解:

  1. 闭包中引用的自由变量值与具体闭包有关,每个闭包实例的自由遍历互不影响;
  2. 通过自由变量,可以在每次函数的调用间存储状态(数据),这是其他函数所不具备的。

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

闭包不仅对构建装饰器有帮助,还能协助构建GUI程序时的面向事件编程,或使用回调处理异步I/O。


审查闭包

 对于闭包函数,其__code__属性中(编译后的函数定义体)保存着局部变量与自由变量的名称。

 而其自由变量的值绑定在__closure__属性中,该属性为一个列表,各个自由变量对应其中的cell对象,cell对象的cell_contents属性保存其真正的值:

averager.__code__.co_varnames
>> ('new_values','total')

averager.__code__.co_freevars
>> ('series')

averager.__closure__[0].cell_contents
>> [10,20,20]


nolocal声明

 在闭包的使用中可能存在一些陷阱,比如在闭包内对半丁的自由变量赋值(特别是+=、-=等增量赋值),这样会将自由变量变为局部变量:

# 错误示范
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1      # 创建局部变量count
        total += new_value  # 创建局部变量total
        return total/count

    return averager

 对于以上陷阱,只能遵守以下准则:

  • 不能对自由变量重新赋值(包括+=、-=等隐式赋值);
  • 对于数字、字符串、元组等不可变类型来说,只能读取,不能更新;
  • 对于可变类型来说,更新时调用原地更新方法。

为了解决以上问题,Python 引入了nolocal声明,它的作用是将变量标记为自由变量,若为nolocal声明的变量赋予新值,闭包中保存的绑定会更新:

# 正确示例
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nolocal count,total
        count += 1
        total += new_value
        return total/count

    return averager


三、标准库中的装饰器

 以下介绍两个functools中定义的实用装饰器:

缓存功能:lru_cache

@functools.lru_cache提供了缓存功能,它将耗时的函数计算结果保存起来,避免传入相同的参数时重复计算。缓存的条目不会无限增长,一段时间不用的缓存条目会被扔掉。

 其函数签名为:

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

 配置参数详解:

  • maxsize:指定存储多少个调用接受后扔掉旧的结果,腾出空间;
  • typed:将相等但不同类型的参数结果分开存储(如 1 与 1.0)。

由于其接受参数,故即便不做配置,使用的时候也需在后面加上括号。

lru_cache使用字典存储结果,且键根据调用时传入的参数创建,故被lru_cache装饰函数,其所有参数都必须为可散列的。

使用示例:

import functools

@functools.lru_cache()
def fibonacci(n):
    if n<2:
        return n
    else:
        return fibonacci(n-2)+fibonacci(n-1)


单分派泛函数:singledispatch

 可以将整体方案拆分成多个模块,使用@functools.singledispatch装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行一组函数。

由于只根据第一个参数的类型做区分,于是称为分派。

用法示例:

from functools import singledispatch

@singledispatch     # 标记基函数
def my_function(obj):
    print("This is a Object type argument.")

@my_function.register(str)      # 借助基函数标记各个专门函数
def _(arg):
    print("This is a str type argument.")

@my_function.register(int)
def _(arg):
    print("This is a int type argument.")

...
  • 需先用@singledispatch装饰器标记处理object类型的基函数;

  • 为各个专门函数使用@<base_functon>.register(<type>)装饰;

  • 专门函数名称无关紧要,一般使用_即可;

  • 各个专门函数都无法匹配的参数,将传入基函数,由基函数处理

    如以上示例调用my_function(2.0),将打印 "This is a Object type argument."


Last Modified : 2020-09-17 15:23:58