装饰器、装饰器类与类装饰器(一)

1. 定义函数时造成高耦合的问题

01_highCouplingExample.py

def print_odds():
    start_time = time.time()
    for i in range(100):
        if i % 2 == 1:
            print(i)
    end_time = time.time()
    print("It takes {} s to find all the odds".format(end_time - start_time))

在定义print_odds函数时,给函数赋予了两个功能

  1. 计时功能
  2. 输出奇数功能

若在项目中遇到更复杂的需求,此时耦合在一起,对代码的修改与维护不友好,所以需要将两个功能给独立开来


2. 要想明白如何将代码独立开来,首先需要理解在python语言中,所有的东西都是一个对象

02_funcDividedExample.py

def count_time(func):
    start_time = time.time()
    func()
    end_time = time.time()
    print("It takes {} s to find all the odds".format(end_time - start_time))


def print_odds():
    for i in range(100):
        if i % 2 == 1:
            print(i)


if __name__ == '__main__':
    count_time(print_odds)

在这里,可以将计时功能赋予count_time函数,而将输出奇数功能赋予'print_odds'函数,在主函数里,将print_odds作为参数传入count_time函数中
若在主函数中执行print(print_odds.__name__),可以得到输出结果为

print_odds

直白来说,就是函数名不加括号时,就表示一个名为自己的函数对象


3.既然函数可以作为对象传入,自然也可以作为对象被传出

03_closureFuncExample.py

def count_time_wrapper(func):
    def improved_func():
        start_time = time.time()
        func()
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))
    return improved_func


def print_odds():
    for i in range(100):
        if i % 2 == 1:
            print(i)


if __name__ == '__main__':
    print_odds_addedCountTime = count_time_wrapper(print_odds)
    print_odds_addedCountTime()

在此例中,count_time_wrapper是一个闭包
闭包本质上就是一个函数,通过传入一个函数和返回一个函数来对传入的函数进行增强
但是需要注意的是返回的不是增强的传入函数,而是返回一个传入函数增强的结果
举个例子
当执行print(print_odds_addedCountTime.__name__)语句后,返回的是
improved_func

也就是说返回的是闭包函数里包含的增强函数,而不是传进去的待增强的函数


4.对上个程序稍加修改,就可以得到我们的第一个装饰器

04_decoratorExample.py

def count_time_wrapper(func):
    """
    This is a function which can decorate func
    :param func: a function  need to be improved
    :return: improved_func
    """
    def improved_func():
        """
        This is a function which can record how long func runs
        :return: None
        """
        start_time = time.time()
        func()
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))
    return improved_func

@count_time_wrapper
def print_odds():
    """
    This is a function which can print odds among 1-100
    :return: None
    """
    for i in range(100):
        if i % 2 == 1:
            print(i)


if __name__ == '__main__':
    print_odds()

只需要在待装饰的函数上用@闭包函数名,即可对待装饰函数进行增强

  • 装饰器其实就是一个语法糖,本质上与闭包函数完全一样
  • 需要注意的是,通过装饰器的写法,是将闭包函数内的增强函数作为对象返回,待增强的函数的对象其实已经变成了闭包函数内的增强函数
    通过执行print(print_odds.__name__)print(print_odds.__doc__)语句,结果是
improved_func

        This is a function which can record how long func runs
        :return: None

如果没有装饰,执行语句,返回的结果是

print_odds

    This is a function which can print odds among 1-100
    :return: None

5.那如何解决装饰器重写函数的名字和注释文档的问题呢

通过导入functiontools类来解决
05_decoratorExample.py

from functools import wraps

# 节省篇幅,注释在此笔记中省略,省略注释同 04_decoratorExample.py
def count_time_wrapper(func):
    @wraps(func)
    def improved_func():
        start_time = time.time()
        func()
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))

    return improved_func


@count_time_wrapper
def print_odds():
    for i in range(100):
        if i % 2 == 1:
            print(i)

@wraps接受一个函数对象来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。
其实在improved_func()函数中可以添加
improved_func.__name__ = func.__name__
improved_func.__doc__ == func.__doc__
等语句使得装饰后的函数属性与之前一致,但是,有现成的@wraps不用干啥?写代码又不是按行数算工资。


6.若需要装饰带参数函数

06_decoratorExample.py

def count_time_wrapper(func):
    @wraps(func)
    def improved_func(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))

    return improved_func


@count_time_wrapper
def print_odds(lim=10000):
    for i in range(lim):
        if i % 2 == 1:
            print(i)

此时仔细讲一下@装饰器的工作原理
只有第一次使用装饰器被装饰的函数时,该函数才会被增强

  • 当被装饰的函数没有被用到时,则该函数不会被增强
  • @装饰器只会生效一次

通过调试可以发现,使用@装饰器增强函数的过程不会被显式出来,即

def count_time_wrapper(func):
    def improved_func(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))
    return improved_func

print_odds = count_time_wrapper(print_odds)

@count_time_wrapper完全等价,且会一次执行到位
个人认为可以理解成这样:
编译器检测到马上要执行一个被装饰器装饰的函数了,那么就立刻对该函数进行加强,该函数加强完毕,或者说该函数被装饰好了,才会开始执行这个函数。


7.函数如何被多个装饰器装饰

对上面代码新增日志记录log_wrapper装饰器
07_decoratorExample.py

def log_wrapper(func):
    @wraps(func)
    def improved_func(*args, **kwargs):
        start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 起始时间
        func(*args, **kwargs)  # 执行函数
        end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 结束时间
        print("Logging: func:{} runs from {} to {}".format(func.__name__, start_time, end_time))

    return improved_func

@log_wrapper
@count_time_wrapper
def print_odds(lim=10000):

经过这样的装饰,可以理解为print_odds函数先被count_time_wrapper函数包裹,再被log_wrapper函数包裹。从装饰器装饰的顺序来说,离被装饰函数越近,则该装饰器则先增强被装饰函数了,增强后的结果再被离的远的装饰器增强
输出结果为

……
99
It takes 0.0 s to find all the odds
Logging: func:print_odds runs from 2022-02-22 19:17:19 to 2022-02-22 19:17:19

8.带有参数的装饰器

不难发现,我们之前自己写的装饰器都不带有参数,但是@wraps装饰器带有参数,这是因为,当使用@装饰器语法时,其实就是在应用一个以单个函数作为参数的一个包裹函数。由此我们理解为@带参数的装饰器(装饰器)语法等同于@带参数的装饰器函数返回的装饰器语法
对上一个代码中的log_wrapper装饰器在做一个闭包函数包裹,该部分修改如下:
08_decoratorExample.py

def logit(logfile='out.log'):
    print("我只执行了一次")

    def log_wrapper(func):
        @wraps(func)
        def improved_func(*args, **kwargs):
            start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 起始时间
            func(*args, **kwargs)  # 执行函数
            end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 结束时间
            print("Logging: func:{} runs from {} to {}".format(func.__name__, start_time, end_time))
            print('在{}中写入日志'.format(logfile))
        return improved_func

    return log_wrapper

#装饰器部分修改如下
@logit('new_out.log')
@count_time_wrapper
def print_odds(lim=10000):

#主函数修改如下
if __name__ == '__main__':
    print_odds(5)
    print_odds(5)

结果为:

我只执行了一次
1
3
It takes 0.0 s to find all the odds
Logging: func:print_odds runs from 2022-02-22 20:25:10 to 2022-02-22 20:25:10
在new_out.log中写入日志
1
3
It takes 0.0 s to find all the odds
Logging: func:print_odds runs from 2022-02-22 20:25:10 to 2022-02-22 20:25:10
在new_out.log中写入日志
  • 解释
    上一节中@log_wrapper相当于print_odds = log_wrapper(print_odds)
    这一节中logit('new_out.log')返回了一个和上一节中log_wrapper一样的闭包函数,所以@logit('new_out.log')其实相当于print_odds = logit('new_out.log')(print_odds)
  • 注意
    我在logit('new_out.log')函数中执行了print("我只执行了一次"),在主函数中,增强后的函数执行了两次,但只出现一次“我只执行了一次”,确实说明了第六节中讲的"只有第一次使用装饰器被装饰的函数时,该函数才会被增强"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容