美文网首页Python
第七章:函数装饰器和闭包

第七章:函数装饰器和闭包

作者: 许志辉Albert | 来源:发表于2021-02-13 14:16 被阅读0次

    7.1 装饰器基本知识

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

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

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

    上述代码的效果与下述写法一样:

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

    两种写法的最终结果一样:上述两短代码片段执行完毕后得到的target不一定是原来那个target函数,而是decorate(target)返回的函数。

    为了确认被装饰的函数会被替换,请看以下实例

    def deco(func):
        def inner():
            print('running inner()')
        return inner  #deco 返回inner
    
    @deco
    def target():   #使用deco装饰target
        print('running target()')
    
    target()  #调用被装饰的target其实会运行inner
    target    #审查对象,发现target现在是inner的引用
    
    1

    严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。

    综上,装饰器的一大特色是,能把被装饰的函数替换成其他函数,第二个特性是,装饰器在加载模块时立即执行。

    7.2 Python何时执行装饰器

    装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python加载模块时),如下示例中的registration.py模块所示。

    registry = [] #registry 保存被@register 装饰的函数引用
    
    def register(func):    register 的参数是一个函数
        print('running register(%s)' %func) #为了演示,显示被装饰的函数
        registry.append(func) #把func存入registry
        return func    #返回func:必须返回函数;这里返回的函数与通过参数传入一样
    
    @register     
    #f1和f2被@register 装饰。
    def f1():
        print('running f1()')
    
    @register
    def f2():
        print('running f2()')
    
    def f3(): #f3没有装饰
        print('running f3()')
    
    def main(): #main显示registry ,然后代用f1() f2() 和f3()
        print('running main')
        print('registry ->' , registry)
        f1()
        f2()
        f3()
    
    if __name__ = '__main__':
        main() #只有把registration.py当做脚本运行时才调用main
    
    

    当把registration.py当做脚本运行时 得到如下结果:


    2

    注意,register 在模块中其他函数之前运行(两次)。调用register时,传给它的参数是被装饰的函数,例如<function f1 at 0x100631bf8>。

    加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及f3,只在main明确调用它们时才执行。

    7.3 使用装饰器改进“策略”模式

    使用注册装饰器可以改进第六章的电商促销折扣示例

    proms = []
    
    def promotion(promo_func):
        promos.append(promo_func)
        return promo_func
    
    @promotion
    def fidelity(order):
        """为积分为1000或以上的顾客提供5%的折扣"""
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0 
    
    @promotion
    def bulk_item(order):
        """单个商品为20个或以上时提供10%折扣"""
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * .1
        return discount
    
    @promotion
    def large_order(order):
        """订单中的不同商品达到10个或者以上时提供7%折扣"""
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * .07
        return 0 
    
    def best_promo(order):
        """选择最佳折扣"""
        return max(pormo(order) for promo in promos)
    

    7.4 变量作用域规则

    #一个函数,读取一个局部变量和一个全局变量
    def f1(a):
        print(a)
        print(b)
    f1(3)
    
    3

    出现错误并不奇怪,在以上实例中,如果先给全局变量b赋值,然后再调用f,那就不会出错

    b  = 6
    f1(3)
    
    4

    看一下以下示例的f2函数,前两行代码和f1一样,然后为b赋值,再打印它的值,可是,在赋值之前,第二个print失败了

    b = 6
    def f2(a):
        print(a)
        print(b)
        b = 9
    f2(3)
    
    5

    注意,首先输出了3 ,这表明print(a)语句执行了,但是第二个语句print(b)执行不了。原先以为会打印6 因为有个全局变量b,而且print(b)之后为局部变量b赋值的。

    可事实上,python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python会尝试从本地环境获取b。后面调用f2(3)时,f2的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值

    这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变 量是局部变量。这比 JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘 记把变量声明为局部变量(使用 var),可能会在不知情的情况下获取全局变量。
    如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

    b = 6
    def f3(a):
        global b
        print(a)
        print(b)
        b = 9  
    
    f3(3)
    
    6
    b
    
    7
    f3(3)
    
    8
    b = 30
    b
    
    9

    7.5 闭包

    在博客圈,人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数 不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。 因此,很多人是同时知道这两个概念的。
    其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的 非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
    这个概念难以掌握,最好通过示例理解。
    假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中 某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。

    #计算移动平均的类
    class Averager():
    
        def __init__(self):
            self.series = []
    
        def __call__(self , new_value):
            self.series.append(new_value)
            total = sum(self.series)
            return total / len(self.series)
    
    avg = Averager()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    
    10
    # 计算移动平均的高阶函数
    def make_averager():
        series = []
    
        def averager(new_value):
            series.append(new_value)
            total = sum(series)
            return total/len(series)
    
        return averager
    

    注意,这两个示例有共通之处:调用 Averager() 或 make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在示例 7-8 中,avg 是 Averager 的实例;在示 例 7-9 中是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n 放入系列值中, 然后重新计算均值。

    Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示 例中的 avg 函数在哪里寻找 series 呢?

    例中的 avg 函数在哪里寻找 series 呢?
    注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10) 时,make_averager 函数已经返回了,而它的本 地作用域也一去不复返了。

    在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地 作用域中绑定的变量

    11

    相关文章

      网友评论

        本文标题:第七章:函数装饰器和闭包

        本文链接:https://www.haomeiwen.com/subject/agxsxltx.html