【Python】【进阶】装饰器

本文用一个基本的计算奇数和的功能函数,和一个在功能函数执行前后各打印一个时间的装饰器函数,来理解python的装饰器。最后用两个装饰器的同时使用,来理解多装饰器的执行顺序。

一、基本函数功能

首先看一个比较简单的函数sum_odds(): 传入一个正整数,打印从0到这个数之间所有的奇数,返回所有奇数的和。

def sum_odds(num=10):
    """
    传入一个正整数,打印从0到这个数之间所有的奇数,返回所有奇数的和
    """
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


sum_odds()


执行结果:

0到10之间所有的奇数为: [1, 3, 5, 7, 9]

新建另一个函数check_time(func),把原功能函数sum_odds()作为一个对象传入新建的函数。这样在调用新函数时,可以实现运行原函数的功能,同时也加入了我们最终目的装饰器的功能,在sum_odds()的前后打印时间。

这一步的目的是,将一个函数作为对象传入另一个函数,暂时忽略掉sum_odds()的返回值。

import datetime


def check_time(func):
    """
    在func前后各打印一行时间
    """
    print('start time: {}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')))
    func()
    print('end time: {}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')))


def sum_odds(num=10):
    """
    传入一个正整数,打印从0到这个数之间所有的奇数,返回所有奇数的和
    """
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


check_time(sum_odds)


执行结果:

start time: 2022-01-20 21:27:25:559702
0到10之间所有的奇数为:1, 3, 5, 7, 9, 
end time: 2022-01-20 21:27:25:559914

下一步需要用到一个新的概念:闭包函数。

二、闭包函数

闭包函数,简单理解,就是在一个外部函数里面又定义了一个内部函数。并且这个内部函数有使用到外部函数的变量。

看一个简单的例子,__closure__可以用来判断一个函数是否是闭包函数,如果是闭包函数,返回cell,如果不是,返回None

从执行结果看,使用一个全局变量的函数fuc1不是闭包函数,只使用内部函数自己变量的函数fuc3也不是闭包函数,只有使用了外部函数master的变量的fuc2才是闭包函数。外部函数master也不是闭包函数。

global_variable = 2


def master():
    master_variable = 5

    def func1():
        return global_variable
    print('使用外部全局变量的func1: ', func1.__closure__)

    def func2():
        return master_variable
    print('使用外部函数变量的func2: ', func2.__closure__)

    def func3(fuc_variable):
        return fuc_variable
    print('使用内部函数变量的func3: ', func3.__closure__)
    
master()
print('master: ', master.__closure__)


执行结果:

使用外部全局变量的func1:  None
使用外部函数变量的func2:  (<cell at 0x000001571EEFC768: int object at 0x00007FFADB027CC0>,)
使用内部函数变量的func3:  None
master:  None

闭包函数还有一个特性就是可以保留外部函数的信息,即使外部函数已经被执行完或者被多次执行,外部函数返回的闭包函数都可以保留当时的信息。

def master(m):
    k = 5

    def func(n):
        return k * m + n
    return func

f1 = master(1)
print(f1(2))  # 7
f2 = master(2)
print(f1(2))  # 7
print(f2(2))  # 12

把我们最开始的基本功能函数和时间打印的函数整合成一个闭包函数

import datetime


def check_time(func):
    def improve_func():
        print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        func()
        print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
    return improve_func


def sum_odds(num=10):
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


improve_sum_odds = check_time(sum_odds)
improve_sum_odds()


执行结果:

start time: 20220209212354080105
0到10之间所有的奇数为: [1, 3, 5, 7, 9]
end time: 20220209212354080105

这个例子里面可以看到,在执行improve_sum_odds()的时候,已经做到了我们最初的需求,在实际功能函数的前后各打印一个时间。其实,这就是装饰器内部的运行逻辑。

三、装饰器

上面的例子,已经发现了装饰器的内部实现,只需要按照装饰器的写法,加一个@符号就变成了装饰器。

import datetime


def check_time(func):
    def improve_func():
        print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        func()
        print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
    return improve_func

@check_time
def sum_odds(num=10):
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


# improve_sum_odds = check_time(sum_odds)
# improve_sum_odds()
sum_odds()

执行结果:

start time: 20220209213204352297
0到10之间所有的奇数为: [1, 3, 5, 7, 9]
end time: 20220209213204352297

这么写会有另一个问题,print(sum_odds.name)返回的结果是improve_func,也就是在外部函数return出来的闭包函数。

此时需要使用到wraps方法。在闭包函数前面用wraps装饰一下,并且传入func

import datetime
from functools import wraps


def check_time(func):
    @wraps(func)
    def improve_func():
        print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        func()
        print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
    return improve_func

@check_time
def sum_odds(num=10):
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


sum_odds()
print(sum_odds.__name__)


执行结果:

start time: 20220209215445636430
0到10之间所有的奇数为: [1, 3, 5, 7, 9]
end time: 20220209215445637430
sum_odds

此时,一个不带传参的装饰器就写完了。如果加入传参,最好用语法糖来理解加入。

四、语法糖

这里先了解一下语法糖的概念,对后面参数传递和多装饰器同时使用的理解有帮助。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。 - - - 百度百科

简单来说,将一段代码换一种更加方便易懂的写法,新的写法就是语法糖。且语法糖需要满足新写法和旧写法完全等价。

a += 1就是一个简单的语法糖写法,它完全等价于a = a + 1

五、参数传递

1、原函数传参

装饰器也是一个语法糖的写法,例子中的@check_time完全等价于sum_odds = check_time(sum_odds)。然后再来传参就好理解了。

函数运行sum_odds(20)等价于check_time(sum_odds)(20),而check_time(sum_odds)返回的是improve_func,所以应该把参数传给improve_func。就变成了:

def check_time(func):
    @wraps(func)
    def improve_func(num):
        print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        func(num)
        print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
    return improve_func

这样还会有一个问题,原函数sum_odds(num=10)中带有默认值的传参变成了一定要主动传入的num,这里用个*args, **kwargs就可以解决了。同时也保证了需要传入任意个参数的函数都可以使用这个装饰器。

import datetime
from functools import wraps


def check_time(func):
    @wraps(func)
    def improve_func(*args, **kwargs):
        print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        func(*args, **kwargs)
        print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
    return improve_func

@check_time
def sum_odds(num=10):
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)

sum_odds()
sum_odds(20)


执行结果:

start time: 20220209223209864084
0到10之间所有的奇数为: [1, 3, 5, 7, 9]
end time: 20220209223209864213
start time: 20220209223209864213
0到20之间所有的奇数为: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
end time: 20220209223209864213

2、装饰器传参

装饰器函数的传参还是按照语法糖的理解,@check_time完全等价于sum_odds = check_time(sum_odds)。也就是说把功能函数作为一个参数传给@后面的整体。

如果要将check_time改为check_time(),那么加上@check_time(m)就等价于sum_odds = check_time(m)(sum_odds)。熟悉的表达式,check_time(m)作为整体,返回一个闭包函数,返回的这个闭包函数就是原来的装饰器函数。

import datetime
from functools import wraps


def out_func(m):
    print(f'装饰器传参: {m}')

    def check_time(func):
        @wraps(func)
        def improve_func(*args, **kwargs):
            print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
            func(*args, **kwargs)
            print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
        return improve_func
    return check_time


@out_func(999)
def sum_odds(num=10):
    odds = [i for i in range(num) if i % 2 == 1]
    print(f'0到{num}之间所有的奇数为: {odds}')
    return sum(odds)


sum_odds()


执行结果:

装饰器传参: 999
start time: 20220210100937216753
0到10之间所有的奇数为: [1, 3, 5, 7, 9]
end time: 20220210100937217750

补充:前面为了考虑装饰器本身,一直有忽略函数结果的返回。这个其实比较简单,在improve_func中把结果return出来就可以了。有兴趣可以按照上面说的语法糖完全等价的形式把函数本来的样子写出来,再理解。或者直接理解为把结果逐级return到最外层的装饰器函数。

def out_func(m):
    print(f'装饰器传参: {m}')

    def check_time(func):
        @wraps(func)
        def improve_func(*args, **kwargs):
            print('start time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
            result = func(*args, **kwargs)
            print('end time: {}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')))
            return result
        return improve_func
    return check_time

六、多装饰器

多装饰器看起来比较麻烦,但是其实按照闭包函数和语法糖的特性去拆分,很好理解的,这里用一个很简单的例子来看。

def aaa(func):
    print("装饰器aaa外部函数开始执行 --- aaa-out")

    def callaaa_func():
        print("装饰器aaa内部函数开始执行 --- aaa-in")
        func()
        print("装饰器aaa内部函数执行结束 --- aaa-in")

    print("装饰器bbb外部函数执行结束 --- aaa-out")
    return callaaa_func


def bbb(func):
    print("装饰器bbb外部函数开始执行 --- bbb-out")

    def callbbb_func():
        print("装饰器bbb内部函数开始执行 --- bbb-in")
        func()
        print("装饰器bbb内部函数执行结束 --- bbb-in")

    print("装饰器bbb外部函数执行结束 --- bbb-out")
    return callbbb_func


@aaa
@bbb
def main_func():
    print("主函数被执行 --- main")


print("--------------分界线-------------")
main_func()

我没有查到自己理想的对于多装饰器执行顺序的解释,这里按照半死记半理解的方式说明。

  • 首先是装饰的过程,

    装饰的过程在@的时候就会被执行,从最下面的装饰器开始执行装饰过程。这里暂时只能想到死记。

    即先执行bbb(main_func)

    装饰器bbb外部函数开始执行 — bbb-out 装饰器bbb外部函数执行结束 — bbb-out

    最后return的是一个函数对象,不会被执行,不会有继续的打印。

    再执行aaa(bbb)

    装饰器aaa外部函数开始执行 — aaa-out 装饰器bbb外部函数执行结束 — aaa-out

    最后return的是一个函数对象,不会被执行,不会有继续的打印。

  • 然后是主流程的print:

    --------------分界线-------------

  • 最后main_func()的执行。main_func()的执行需要进行拆分。

main_func  # 后面还有个()最后再加上。

即为: 
aaa(bbb(main_func))

即为: 
def callaaa_func():  # 最外层的aaa()返回的是callaaa_func函数: return callaaa_func
    print("装饰器aaa内部函数开始执行 --- aaa-in")
    bbb(main_func)()  # 这里需要把func用前面aaa()括号里面的直接代替,就是bbb(main_func),在加上之前func后面自己的括号。
                      # 后面的括号不能漏掉,我一开始漏了,想了半天不知道怎么执行成功的。
    print("装饰器aaa内部函数执行结束 --- aaa-in")

即为如下函数的执行: 
def callaaa_func():
    print("装饰器aaa内部函数开始执行 --- aaa-in")
    # def callbbb_func():  # 这里是bbb(main_func)应该返回一个还没执行的函数,也就是这一行。但是后面还有个()来执行,就只剩函数里面的部分。
        print("装饰器bbb内部函数开始执行 --- bbb-in")
        main_func()
        print("装饰器bbb内部函数执行结束 --- bbb-in")
    print("装饰器aaa内部函数执行结束 --- aaa-in")

再把第一行说的()加上,函数被执行,即:
装饰器aaa内部函数开始执行 --- aaa-in
装饰器bbb内部函数开始执行 --- bbb-in
主函数被执行 --- main
装饰器bbb内部函数执行结束 --- bbb-in
装饰器aaa内部函数执行结束 --- aaa-in

把前面三部分结合起来,得到结果:

装饰器bbb外部函数开始执行 — bbb-out 装饰器bbb外部函数执行结束 — bbb-out 装饰器aaa外部函数开始执行 — aaa-out 装饰器bbb外部函数执行结束 — aaa-out --------------分界线------------- 装饰器aaa内部函数开始执行 — aaa-in 装饰器bbb内部函数开始执行 — bbb-in 主函数被执行 — main 装饰器bbb内部函数执行结束 — bbb-in 装饰器aaa内部函数执行结束 — aaa-in

多装饰器还有个好玩的,看看这段代码:

def outaaa_func():
    print("装饰器aaa外部函数开始执行 --- aaa-1")

    def aaa(func):
        print("装饰器aaa外部函数开始执行 --- aaa-2")

        def callaaa_func():
            print("装饰器aaa内部函数开始执行 --- aaa-3")
            func()
            print("装饰器aaa内部函数执行结束 --- aaa-3")

        print("装饰器bbb外部函数执行结束 --- aaa-2")
        return callaaa_func

    print("装饰器bbb外部函数执行结束 --- aaa-1")
    return aaa


def outbbb_func():
    print("装饰器bbb外部函数开始执行 --- bbb-1")

    def bbb(func):
        print("装饰器aaa外部函数开始执行 --- bbb-2")

        def callbbb_func():
            print("装饰器aaa内部函数开始执行 --- bbb-3")
            func()
            print("装饰器aaa内部函数执行结束 --- bbb-3")

        print("装饰器bbb外部函数执行结束 --- bbb-2")
        return callbbb_func

    print("装饰器bbb外部函数执行结束 --- bbb-1")
    return bbb


@outaaa_func()
@outbbb_func()
def main_func():
    print("主函数被执行 --- main")


print("--------------分界线-------------")
main_func()
# outaaa_func()(outbbb_func()(main_func))


执行结果:
装饰器aaa外部函数开始执行 --- aaa-1
装饰器bbb外部函数执行结束 --- aaa-1
装饰器bbb外部函数开始执行 --- bbb-1
装饰器bbb外部函数执行结束 --- bbb-1
装饰器aaa外部函数开始执行 --- bbb-2
装饰器bbb外部函数执行结束 --- bbb-2
装饰器aaa外部函数开始执行 --- aaa-2
装饰器bbb外部函数执行结束 --- aaa-2
--------------分界线-------------
装饰器aaa内部函数开始执行 --- aaa-3
装饰器aaa内部函数开始执行 --- bbb-3
主函数被执行 --- main
装饰器aaa内部函数执行结束 --- bbb-3
装饰器aaa内部函数执行结束 --- aaa-3

我可以默默告诉自己,代码运行到@outaaa_func()的时候直接先把@后面的outaaa_func()执行,再把@后面的outbbb_func()执行。再开始装饰过程。

但是吧,总觉得没到最深层的逻辑,只能当作一个记忆的方法。

七、类装饰器

就说说有这个东西,还不会!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值