【Python进阶】装饰器高级用法——类内定义装饰器并传递self参数

1,431 阅读6分钟

装饰器

装饰器来自Decorator的直译。什么叫装饰,就是装点、提供一些额外的功能。在Python中的装饰器则是提供了一些额外的功能。 装饰器本质上是一个Python函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。 装饰器用于有以下场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的应用有插入日志、增加计时逻辑来检测性能、加入事务处理等。 装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。 概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

一、使用函数定义装饰器

不带参数装饰器

# coding: utf-8
import time


def cost(func):
    """计算运行时间装饰器"""
    def wrapper(*args, **kwargs):
        # 开始时间
        s_time = time.time()

        # 运行函数
        result = func(*args, **kwargs)

        # 结束时间
        e_time = time.time()

        # 打印结果
        print({
            'args': args,
            'kwargs': kwargs,
            'result': result,
            's_time': s_time,
            'e_time': e_time,
            'cost': e_time - s_time
        })
        return result
    return wrapper


# 装饰带参数函数
@cost
def fibonacci(n: int):
    """斐波那契数列"""
    if n <= 2:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)


# 装饰不带参数函数
@cost
def speak():
    print('hello world')

    
if __name__ == '__main__':
    print(fibonacci(10))
    speak()

输出:

{'args': (2,), 'kwargs': {}, 'result': 1, 's_time': 1681352368.4956758, 'e_time': 1681352368.4956758, 'cost': 0.0}
{'args': (1,), 'kwargs': {}, 'result': 1, 's_time': 1681352368.4956758, 'e_time': 1681352368.4956758, 'cost': 0.0}
{'args': (3,), 'kwargs': {}, 'result': 2, 's_time': 1681352368.4956758, 'e_time': 1681352368.4956758, 'cost': 0.0}

...

{'args': (8,), 'kwargs': {}, 'result': 21, 's_time': 1681352368.511631, 'e_time': 1681352368.5126326, 'cost': 0.001001596450805664}
{'args': (10,), 'kwargs': {}, 'result': 55, 's_time': 1681352368.4956758, 'e_time': 1681352368.5126326, 'cost': 0.016956806182861328}
55

hello world
{'args': (), 'kwargs': {}, 'result': None, 's_time': 1681359050.2040539, 'e_time': 1681359050.2040539, 'cost': 0.0}

分析:此时的@cost相当于将fibonacci函数的内存地址传入cost函数,并返回wrapper函数的内存地址。因此在代码结尾中调用fibonacci() ,本质上是执行wrapper函数。因为执行的是wrapper函数,而func函数是fibonacci的内存地址,所以调用func,执行fibonacci

备注:@是语法糖
不用语法糖的情况下,使用下面语句也能实现装饰作用:把fibonacci再加工,再传给fibonacci \

fibonacci = cost(fibonacci)

装饰器带参数

把装饰器再包装,实现传递装饰器参数。

# coding: utf-8

# 带参数的装饰器
def query(method):
    def wrapper(func):
        def sub_wrapper(*args, **kwargs):
            # 打印装饰器的参数
            print(f'查询方式:{method}')

            # 返回函数运行结果
            return func(*args, **kwargs)
        return sub_wrapper
    return wrapper


@query(method='POST')
def fetch(url):
    print(f'fetch url: {url}')


if __name__ == '__main__':
    fetch(url='https://www.baidu.com')

输出:

查询方式:POST
fetch url: https://www.baidu.com

分析:带参数的装饰器与普通的装饰器多加了一层,其实就是将python参数传入query函数,并返回wrapper函数的内存地址, 再将fetch函数内存地址传入wrapper函数,并返回了sub_wrapper函数的内存地址。而在代码末尾调用fetch,其实本质是调用了sub_wrapper函数。

二、在类里定义装饰器,装饰本类内函数

类装饰器,装饰函数和类函数调用不同的类函数

把装饰器写在类里

在类里面定义个函数,用来装饰其它函数,严格意义上说不属于类装饰器。

# coding: utf-8

class Logger:

    # 方式一
    @staticmethod
    def info(func):
        def wrapper(*args, **kwargs):
            print(f'log_type: info'.center(50, '-'))
            return func(*args, **kwargs)
        return wrapper

    # 方式二
    def error(func):
        def wrapper(*args, **kwargs):
            print(f'log_type: error'.center(50, '-'))
            return func(*args, **kwargs)

        return wrapper

    # 方式三
    def debug(self, func):
        def wrapper(*args, **kwargs):
            print(f'log_type: debug'.center(50, '-'))
            return func(*args, **kwargs)

        return wrapper

    # 方式四
    @classmethod
    def critical(cls, func):
        def wrapper(*args, **kwargs):
            print(f'log_type: critical'.center(50, '-'))
            return func(*args, **kwargs)

        return wrapper


@Logger.info
def greet(text):
    print(f'greet: {text}')


@Logger.error
def speak(text):
    print(f'speak: {text}')


my_logger = Logger()


@my_logger.debug
def tell(text):
    print(f'tell: {text}')


@Logger.critical
def eat(food):
    print(f'eat: {food}')


if __name__ == '__main__':
    greet('hello world')
    speak('how are you')
    tell('I am fine')
    eat('apple')

输出:

------------------log_type: info------------------
greet: hello world
-----------------log_type: error------------------
speak: how are you
-----------------log_type: debug------------------
tell: I am fine
----------------log_type: critical----------------
eat: apple

为了让装饰器在使用上更加灵活,我们把类的实例方法作为装饰器,此时在包裹函数中就可以持有实例对象,便于修改属性和拓展功能。

装饰器装饰同一个类里的函数

需求点:想要通过装饰器修改类里的self属性值。

# coding: utf-8
from functools import wraps


class Test(object):
    def __init__(self):
        self.reset = True
        self.flag = True

    # 在类里定义一个装饰器
    def info(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            print('log_type: info'.center(50, '-'))
            if self.reset:
                print('Reset is Ture, change flag...')
                self.flag = False
            return func(self, *args, **kwargs)
        return wrapper

    @info
    def speak(self, text):
        print(f'speak: {text}')


if __name__ == '__main__':
    t = Test()
    t.speak('hello world')
    print(t.flag)

输出:

------------------log_type: info------------------
Reset is Ture, change flag...
speak: hello world
False

三、类装饰器

定义一个类装饰器,装饰函数,默认调用__call__方法

# coding: utf-8

class Test:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('函数__call__被调用了'.center(50, '-'))
        return self.func(*args, **kwargs)


@Test
def speak(text):
    print(f'speak: {text}')


if __name__ == '__main__':
    speak('hello world')

输出:

------------------函数__call__被调用了------------------
speak: hello world

定义一个类装饰器,装饰类中的函数,默认调用__get__方法

实际上把类方法变成属性了,还记得类属性装饰器@property,下面自已做一个property

class Test:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        '''
        instance:代表实例
        owner:代表类本身
        '''
        print('调用的是get函数'.center(50, '-'))
        return self.func(instance)


class Person(object):
    def __init__(self):
        self.result = 0

    @Test
    def speak(self):
        print('speak: hello world')
        return True


if __name__ == '__main__':
    p = Person()
    p.speak

输出:

--------------------调用的是get函数---------------------
speak: hello world

做一个求和属性sum,统计所有输入的数字的和

class Decorator:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        print('调用的是get函数')
        return self.func(instance)


class Test:
    def __init__(self, *args, **kwargs):
        self.value_list = []
        if args:
            for i in args:
                if str(i).isdigit():
                    self.value_list.append(i)
        if kwargs:
            for v in kwargs.values():
                if str(v).isdigit():
                    self.value_list.append(v)
    @Decorator
    def sum(self):
        result = 0
        print(self.value_list)
        for i in self.value_list:
            result += i

        return result


if __name__ == '__main__':
    t = Test(1,2,3,4,5,6,7,8,i=9,ss=10,strings = 'lll')
    print(t.sum)

编写一个类装饰器,将装饰器应用于所有方法

import inspect


class DecoratedAllMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, cls=None):
        def wrapper(*args, **kwargs):
            print("decorate: before".center(50, '-'))
            try:
                # 实例方法
                ret = self.func(obj, *args, **kwargs)
            except TypeError:
                # 类方法或者静态方法
                ret = self.func(*args, **kwargs)
            print("decorate: after".center(50, '*'))
            return ret
        for attr in "__module__", "__name__", "__doc__":
            setattr(wrapper, attr, getattr(self.func, attr))
        return wrapper


class DecoratedInstanceMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, cls=None):
        def wrapper(*args, **kwargs):
            print("decorate instance method: before".center(50, '-'))

            ret = self.func(obj, *args, **kwargs)

            print("decorate instance method: after".center(50, '*'))
            return ret
        for attr in "__module__", "__name__", "__doc__":
            setattr(wrapper, attr, getattr(self.func, attr))
        return wrapper


class DecoratedClassMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, cls=None):
        def wrapper(*args, **kwargs):
            print("decorate class method: before")
            ret = self.func(*args, **kwargs)
            print("decorate class method: after")
            return ret
        for attr in "__module__", "__name__", "__doc__":
            setattr(wrapper, attr, getattr(self.func, attr))
        return wrapper


def decorate_class(cls):
    for name, meth in inspect.getmembers(cls):
        if inspect.ismethod(meth) or inspect.isfunction(meth):
            setattr(cls, name, DecoratedAllMethod(meth))

    return cls


@decorate_class
class Person:

    def __init__(self, name):
        self.name = name
        print("__init__")

    def call(self):
        print(self.name)

    @staticmethod
    def speak(text):
        print(f"speak: {text}")

    @classmethod
    def eat(cls):
        print("eat...")


if __name__ == '__main__':
    p = Person(name='张三')
    p.call()
    p.speak('hello world')
    Person.speak('你好')
    p.eat()
    Person.eat()

缓存和计时装饰器的综合练习

# coding: utf-8
import time


class CacheDecorator:
    """缓存装饰器"""
    __cache = {}

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # 如果缓存中有对应的方法名,则直接返回对应的返回值
        if self.func.__name__ in CacheDecorator.__cache:
            return CacheDecorator.__cache[self.func.__name__]
        # 如果缓存中没有对应的方法名,则进行计算,并将结果缓存
        else:
            result = self.func(*args, **kwargs)
            CacheDecorator.__cache[self.func.__name__] = result
            return result


def cost_time(func):
    """时间计算装饰器"""
    def wrapper(*args, **kwargs):
        s_time = time.time()
        result = func(*args, **kwargs)
        e_time = time.time()
        print(f'cost time: {e_time - s_time}')
        return result

    return wrapper


@cost_time
@CacheDecorator
def test():
    """模拟耗时较长,每次执行返回结果都一样的情况"""
    print("start...")
    time.sleep(3)
    print("end...")
    return 999


if __name__ == '__main__':
    t1 = test()
    t2 = test()
    print(t1)
    print(t2)

参考文章