个人博客,记录知识防止遗忘
python 闭包和装饰器
python 闭包和装饰器

python 闭包和装饰器

python有很多灵活的语法,闭包和装饰器就是其中很常见很实用的设计。
闭包和装饰器整体思路采用“面向切面编程”的理念,用来实现一些辅助功能时非常好用,例如日志打印、时间计数、账户鉴权等。
本文将简单介绍原理,并结合几个简单案例说明。

内容大纲

1 闭包

1.1 概念

python的闭包,简而言之就是 :
在函数中再次嵌套一个函数,并且内部函数引用外部函数的变量,外部函数的返回值是内部函数

闭包作用就是将原函数做一些处理(如功能增强等)然后返回。

如下图实例所示:
定义一个函数aaa,再其内部定义另一函数bbb,并且函数aaa的返回值是函数bbb整体,此时便形成了一个闭包
aaa的作用是对原函数bbb进行处理(传入变量x),最后返回原函数bbb
image.png

def aaa(x):
    def bbb(y):
        return x - y
    return bbb

print(aaa(6)(4))

1.2 闭包实例

闭包的常见用法就是将某些功能编写到闭包函数中,当有其他任何函数需要使用此功能时,直接显示调用一次即可。

举个例子,闭包就是裁缝店的老板提前制作出一些不同的帽子,当有客人需要的时候,他直接拿来戴上就行。不用客人自己自己制做

例:(此时的UseTime并不是最终的版本,因为其缺少参数传入,并且没有用wraps装饰)

def UseTime(func):
    '''
    定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
    '''
    def aaa():
        import time
        time_1 = time.time()
        bbb = func()
        time_2 = time.time()

        total_time = time_2 - time_1
        print("\n执行花费的时间为: {:.3} s".format(total_time))

        return bbb
    return aaa

def IsPrime(num):
    '''判断某数字是不是质数'''
    if num < 2:
        return False
    elif num == 2:
        return True
    else:
        for i in range(2, num):
            if num % i == 0:
                return False
            else:
                return True

def Total():
    '''
    统计1-10000之间有多少质数
    '''
    count = 0
    for i in range(1, 10001):
        if IsPrime(i):
            count += 1
    return count

if __name__ == '__main__':

    # 将原函数Total通过闭包函数UseTime进行加工(增加时间统计功能)
    Total = UseTime(Total)  

    print('质数共有 ' + str(Total()) + ' 个')

可以看到,上述代码主要有以下内容:

  • 定义了一个闭包函数UseTime 来统计其他函数的执行时间,
  • 定义了一个函数IsPrime 判断接收的数字是否是质数,
  • 定义了一个函数Total 调用IsPrime 来判断1-10000中有多少个质数
  • 最后的main函数中,让Total “戴上”能统计出其执行时间的 “帽子”

执行结果如下:
image.png
可以看到,打印的结果中,不仅包含了Total函数自身的统计结果,还包含了他的帽子——UseTime的执行结果
这种方法也被称为面向切面编程,即主函数只实现基本的功能,剩余的都由各种函数闭包来装饰,最终“拼接”实现一个完整的应用。

这种情况下,例如上述代码中的 Total = UseTime(Total) 这种显示声明看起来会很乱。
并且每次调用函数都会执行一次Total = UseTime(Total),相当于增强了很多次,这样就会出现执行上述代码,却打印了很多条执行时间的情况。

此时就需要使用 语法糖装饰器来实现了

2 语法糖和装饰器

2.1 装饰器

装饰器本质上是闭包语法糖,后续结合语法糖说明

装饰器被调用时有2点需要注意:
① 装饰器的增强时机是在第一次调用之前。
只有在第一次调用被装饰的函数时,闭包函数才会被调用,装饰器才会真正生效。
若整个程序的运行过程中都没有调用被装饰的函数,那闭包函数也不会被调用,意味着装饰器实际上是无效的。

② 装饰器只增强一次原函数
闭包函数只会被调用一次。
当第二次以上调用原函数时,实际上调用的直接就是增强后的函数(防止出现闭包函数“套娃”的情况)

2.1.1 使用方法

例如以上例子的代码 Total = UseTime(Total) ,其实可以在定义函数Total 时,使用@UseTime直接进行装饰
image.png
以上两种写法等价

2.1.2 参数 以及 @wraps

很多函数都需要传入参数,通过上述方式定义的装饰器无法接受参数,会报错。
所以还需要对其进一步优化:

from functools import wraps
def UseTime(func):
    '''
    定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
    '''
    def aaa(*args, **keyword):
        import time    
        time_1 = time.time()
        bbb = func(*args, **keyword)
        time_2 = time.time()

        total_time = time_2 - time_1
        print("\n执行花费的时间为: {:.3} s".format(total_time))
        return bbb
    return aaa

其实在装饰器中,被装饰后的函数(内函数)已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响使用,Python的functools包中提供了一个叫wraps的装饰器来消除这样的副作用

所以无论写什么装饰器,最好都在实现之前加上functools的wrap,它能保留原有函数的名称和属性。

例如以下代码,在定义装饰器UseTime时,对其内函数使用wraps进行装饰:

from functools import wraps
def UseTime(func):
    '''
    定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
    '''
    @wraps(func)
    def aaa(*args, **keyword):
        import time    
        time_1 = time.time()
        bbb = func(*args, **keyword)
        time_2 = time.time()

        total_time = time_2 - time_1
        print("\n执行花费的时间为: {:.3} s".format(total_time))
        return bbb
    return aaa

以上代码是一个完整的常见装饰器

2.1.3 常用装饰器的写法

实际使用中,装饰器的写法都是较为固定的,基本都是以下的框架:

def UseTime(func):
    '''
    定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
    '''
    @wraps(func)
    def aaa(*args, **keyword):

        # 此处可以新增加一些功能

        bbb = func(*args, **keyword)

        # 此处可以新增加一些功能

        return bbb
    return aaa

2.2 语法糖

装饰器之所以能用 @装饰器名 的方式来简写,根本原因是:装饰器本质上是闭包的语法糖
闭包之前举例说明过了,那什么是语法糖呢?可以吃吗...

语法糖指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。

语法糖其实只是一种代码的“简写方式” 它本身并没有增加任何新功能。
所以意味着包含语法糖的代码完全可以转化为不包含语法糖的写法,两者完全等价。
例如图中的2种写法是等价的,但右边的看起来更整洁

2.3 类装饰器

既然函数可以通过装饰器来增加新功能,那class是否也能用类似的方法来偷懒呢?
实际上是可以的,这也就是 类装饰器

具体的实现方法要比函数装饰器稍微复杂一些,但逻辑是差不多的。
以后用到的时候再补充吧,暂时偷个懒~

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注