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

9.装饰器的条件——可调用

Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象
装饰器要能实现的原理,就是用闭包函数调用被装饰函数
而能被调用其实都是定义的函数都自带__call__方法,比如

def sayLove():
    print('上邪,我欲与君相知,长命无绝衰。山无陵,江水为竭。冬雷震震,夏雨雪。天地合,乃敢与君绝。')


sayLove()
sayLove.__call__()

输出结果为:
上邪,我欲与君相知,长命无绝衰。山无陵,江水为竭。冬雷震震,夏雨雪。天地合,乃敢与君绝。
上邪,我欲与君相知,长命无绝衰。山无陵,江水为竭。冬雷震震,夏雨雪。天地合,乃敢与君绝。

可见函数可调用就是因为有__call__方法
对于可调用对象,实际上“名称()”可以理解为是“名称.__call__()”的简写
那么类呢?
一般来说,类只能用来实例化,但是当我们定义里类里面的__call__方法后,就可以实现实例的调用

class SayLove:
    def __init__(self, content):
        self.content = content

    def __call__(self, name):
        print(name + self.content)


t = SayLove(", I love you.")
t('Ning')

输出结果为:
Ning, I love you.
  • 题外话:可以使用hasattr(类.方法/函数, '__call__')语句来判断实例对象所包含指定名称的是方法还是属性

10.利用类的调用实现装饰器类——先使用init的假的装饰器类

10.1装饰器不含参数

将之前的代码count_time_wrapper闭包函数修改为如下10_classDecoratorExample.py

class CountTimeWrapper:
    """
    This is a class decorator for counting time
    """
    def __init__(self, func):
        print("我实例化啦")
        self.func = func

    def __call__(self, *args, **kwargs):
        print("我已经被装饰好啦")
        start_time = time.time()
        self.func(*args, **kwargs)
        end_time = time.time()
        print("It takes {} s to find all the odds".format(end_time - start_time))

@CountTimeWrapper
def print_odds(lim=10000):

在主函数中使用

if __name__ == '__main__':
    print_odds(5)
    print_odds(5)
    print(print_odds.__class__)
    print(print_odds.__doc__)

# 输出结果为:
我实例化啦
我已经被装饰好啦
1
3
It takes 0.0 s to find all the odds
我已经被装饰好啦
1
3
It takes 0.0 s to find all the odds
<class '__main__.CountTimeWrapper'>

    This is a class decorator for counting time

由此可见顺序是

  • ①程序识别到print_odds函数被装饰,开始隐式实例化CountTimeWrapper
  • ②实例化时,将print_odds函数对象作为参数传入
  • ③通过_call__使实例化的类可以调用并且返回给print_odds
  • ④此时print_odds不再是一个函数,而是一个可以调用的实例化的CountTimeWrapper

@CountTimeWrapper这个语法糖可以完全等价为print_odds = CountTimeWrapper(print_odds)
证据就是可以直接执行print_odds.func()函数,结果与未装饰的函数一致

10.2装饰器含参数

先说结论,如果类装饰器含有默认参数且不需要修改的话,可以使用,若需要修改,则无法实现
将之前的代码logit(logfile='out.log')闭包函数修改为如下10_classDecoratorExample.py

class Logit:
    """
    This is a class decorator for counting time
    """
    def __init__(self, func, logfile='out.log'):
        print("我实例化啦")
        self.func = func
        self.logfile = logfile

    def __call__(self, *args, **kwargs):
        print("我已经被装饰好啦")
        start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 起始时间
        self.func(*args, **kwargs)  # 执行函数
        end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 结束时间
        print("Logging: func:{} runs from {} to {}".format(self.func.__name__, start_time, end_time))
        print('在{}中写入日志'.format(self.logfile))

原理同10.1,@Logit这个语法糖可以完全等价为print_odds = Logit(print_odds),如第8节所述

使用@装饰器语法时,其实就是在应用一个以单个函数作为参数的一个包裹函数

而这个语法糖并不支持传入多参数。
而且,这种装饰器类从某种意义上来说,算不上是装饰器,因为装饰器装饰了一个函数,应该再返回一个函数,而这种方法是将一个可以调用的实例化的类返回给函数,函数不再是函数了。
这种假装饰器类缺点很多

  • 无法多重装饰,因为把一个函数装饰成类了,下一个装饰函数的装饰器就没办法了
  • 无法使用functools.wraps无法保存原来的函数属性

11.真正的装饰器类(用于装饰函数的一个类)

11.1装饰器类的实现及解释

11_1classDecoratorExample.py

import time
from functools import wraps


class Logit:
    """
    This is a class decorator for counting time
    """

    def __init__(self, logfile='out.log'):
        print("Logit实例化啦")
        self.logfile = logfile

    def __call__(self, func):
        @wraps(func)
        def log_wrapper(*args, **kwargs):
            print("我已经被log_wrapper装饰好啦")
            func(*args, **kwargs)  # 执行函数
            print('在{}中写入日志'.format(self.logfile))

        return log_wrapper


class CountTimeWrapper:
    """
    This is a class decorator for counting time
    """

    def __init__(self):
        print("CountTimeWrapper实例化啦")

    def __call__(self, func):
        @wraps(func)
        def count_time_wrapper(*args, **kwargs):
            print("我已经被count_time_wrapper装饰好啦")
            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 count_time_wrapper


@Logit('a.log')
@CountTimeWrapper()
def print_odds(lim=10000):
    """
    This is a function which can print odds among 1-lim
    :return: None
    """
    for i in range(lim):
        if i % 2 == 1:
            print(i)


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

原理如下:

  • ①程序识别到print_odds函数被装饰,开始隐式实例化装饰器类
  • ②实例化后,将待装饰函数传入实例化的装饰器类的__call__()进行调用
  • ③返回__call__()里面的装饰器函数

注意:

  • @CountTimeWrapper()这个语法糖可以理解为print_odds = CountTimeWrapper().__call__(print_odds),之后装饰好了的函数再被print_odds = @Logit('a.log').__call__(print_odds)装饰
  • ②装饰器函数可以通过functools.wraps无法保存原来的函数属性
  • ③若被装饰函数是一个有返回值的执行函数,可以在装饰函数中通过return func(*args, **kwargs)返回出来
    返回结果为:
Logit实例化啦
CountTimeWrapper实例化啦
我已经被log_wrapper装饰好啦
我已经被count_time_wrapper装饰好啦
1
3
It takes 0.0 s to find all the odds
在a.log中写入日志
我已经被log_wrapper装饰好啦
我已经被count_time_wrapper装饰好啦
1
3
It takes 0.0 s to find all the odds
在a.log中写入日志

装饰器语法糖@装饰器语法

@Logit('a.log')
@CountTimeWrapper()
def print_odds(lim=10000):

与下面的语句完全等价

print_odds = Logit('a.log').__call__(CountTimeWrapper().__call__(print_odds))

理解上确实是自下而上的包裹,但程序运行起来确是需要自上而下的实例并调用

11.2装饰器类的继承

第八节带有参数的装饰器其实就是用嵌套函数的方式实现装饰器可以传入参数
而是用装饰器类可以达到同样的效果

  • 从形式上,显然装饰器类的方式更加简洁,易读性更强,方便维护与更新
  • 再者,装饰器类就是一个类,类时可以被继承的,所以如果想要不改变原装饰器,而对装饰器增加新的功能,可以采用继承的方式,对装饰器进行功能的增加

11_1classDecoratorExample.py进行修改,修改成11_2classDecoratorExample.py
修改的地方主要是对Logit装饰器类进行修改并,并在EmailLogit装饰器类中对Logit装饰器类进行继承

class Logit:

    def __init__(self, logfile='out.log'):
        print("Logit实例化啦")
        self.logfile = logfile

    def __call__(self, func):
        @wraps(func)
        def log_wrapper(*args, **kwargs):
            print("我已经被log_wrapper装饰好啦")
            func(*args, **kwargs)  # 执行函数
            self.log()

        return log_wrapper

    def log(self):
        print('在{}中写入日志'.format(self.logfile))

将装饰器中的增强的函数作为装饰器类的方法进行定义,并由__call__()装饰待装饰函数时调用,而不是直接在__call__()装饰待装饰函数直接在内部增加

class EmailLogit(Logit):
    print("EmailLogit实例化啦")

    def __init__(self, email='admin@test.txt', *args, **kwargs):
        self.email = email
        super(EmailLogit, self).__init__(*args, **kwargs)

    def log(self):
        super(EmailLogit, self).log()
        print("把{}日志文件发送给邮箱:{}".format(self.logfile, self.email))

EmailLogit装饰器类继承Logit装饰器类时,对Logit装饰器类中的增强函数功能进行重写,增加了将日志文件发送给管理员邮箱的功能
此时使用@EmailLogit(email='601038667@qq.com', logfile='a.log')直接对print_odds函数装饰
结果为

EmailLogit实例化啦
Logit实例化啦
我已经被log_wrapper装饰好啦
1
3
在a.log中写入日志
把a.log日志文件发送给邮箱:601038667@qq.com
我已经被log_wrapper装饰好啦
1
3
在a.log中写入日志
把a.log日志文件发送给邮箱:601038667@qq.com
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容