装饰器

浏览 1626

课文

装饰器来自 Decorator 的直译,理解装饰这个词就等于理解了装饰器。

什么叫装饰,就是装点、提供一些额外的点缀。在 python 中的装饰器则是提供了一些额外的功能。

函数装饰器

在学习闭包的时候我们就已经知道,函数是一个对象。

这意味着函数:

  • 能在函数中定义一个函数
  • 能作为参数传递
  • 能作为返回值

来看一个简单的例子。

def decorator(func):
    def wrapper(*args, **kwargs):
        print('123')
        return func(*args, **kwargs)

    return wrapper

def say_hello():
    print('同学你好')

say_hello_super = decorator(say_hello)
say_hello_super()
123
同学你好

在这段代码中,我们将一个函数 say_hello 作为参数传入函数 decorator,返回一个 wrapper 函数并赋值到 say_hello_super,此时执行 say_hello_super 相当于执行 wrapper 函数。当我们执行 wrapper 函数时会先打印 123 再执行先前传入的 func 参数也就是 say_hello 函数。

注意 wrapper*args**kwargs 参数,这是必须的, *args 表示所有的位置参数,**kwargs 表示所有的关键字参数。之后再将其传到 func函数中, 这样保证了能完全传递所有参数。

在这里,decorator 这个函数就是一个装饰器,功能是在执行被装饰的函数之前打印 123

在 python 中, 有一种语法糖可以代替 say_hello_super = decorator(say_hello) 这一步的操作,以上的代码可以改写成。

def decorator(func):
    def wrapper(*args, **kwargs):
        print('123')
        return func(*args, **kwargs)

    return wrapper

@decorator
def say_hello():
    print('同学你好')

say_hello()
123
同学你好

这里在函数前加上 @decorator 相当于在定义函数后执行了一条语句, say_hello = decorator(say_hello)

带参数的装饰器

之前的装饰器是在每次执行函数前打印 123, 如果我们想指定打印的值,那该怎么办?

def info(value):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(value)
            return func(*args, **kwargs)

        return wrapper

    return decorator

@info('456')
def say_hello():
    print('同学你好')

say_hello()
456
同学你好

我们可以在装饰器外部再套上一层函数,用该函数的参数接收我们想要打印的数据,并将先前的 decorator 函数作为返回值。这就是之前学到的闭包的一种功能,就是用闭包来生成一个命名空间,在命名空间中保存我们要打印的值 value

wraps 装饰器

一个函数不止有他的执行语句,还有着 __name__(函数名),__doc__ (说明文档)等属性,我们之前的例子会导致这些属性改变。

def decorator(func):
    def wrapper(*args, **kwargs):
        """doc of wrapper"""
        print('123')
        return func(*args, **kwargs)

    return wrapper

@decorator
def say_hello():
    """doc of say hello"""
    print('同学你好')

print(say_hello.__name__)
print(say_hello.__doc__)
wrapper
doc of wrapper

由于装饰器返回了 wrapper 函数替换掉了之前的 say_hello 函数,导致函数名,帮助文档变成了 wrapper 函数的了。

解决这一问题的办法是通过 functools 模块下的 wraps 装饰器。

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """doc of wrapper"""
        print('123')
        return func(*args, **kwargs)

    return wrapper

@decorator
def say_hello():
    """doc of say hello"""
    print('同学你好')

print(say_hello.__name__)
print(say_hello.__doc__)
say_hello
doc of say hello

内置装饰器

有三种我们经常会用到的装饰器, propertystaticmethodclassmethod,他们有个共同点,都是作用于类方法之上。

property 装饰器

property 装饰器用于类中的函数,使得我们可以像访问属性一样来获取一个函数的返回值。

class XiaoMing:
    first_name = '明'
    last_name = '小'

    @property
    def full_name(self):
        return self.last_name + self.first_name

xiaoming = XiaoMing()
print(xiaoming.full_name)
小明

例子中我们像获取属性一样获取 full_name 方法的返回值,这就是用 property 装饰器的意义,既能像属性一样获取值,又可以在获取值的时候做一些操作。

staticmethod 装饰器

staticmethod 装饰器同样是用于类中的方法,这表示这个方法将会是一个静态方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有 self 参数,也无法访问实例化后的对象。

class XiaoMing:
    @staticmethod
    def say_hello():
        print('同学你好')

XiaoMing.say_hello()

# 实例化调用也是同样的效果
# 有点多此一举
xiaoming = XiaoMing()
xiaoming.say_hello()
同学你好
同学你好

classmethod 装饰器

classmethod 依旧是用于类中的方法,这表示这个方法将会是一个类方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有 self 参数,也无法访问实例化后的对象。相对于 staticmethod 的区别在于它会接收一个指向类本身的 cls 参数。

class XiaoMing:
    name = '小明'

    @classmethod
    def say_hello(cls):
        print('同学你好, 我是' + cls.name)
        print(cls)

XiaoMing.say_hello()
同学你好, 我是小明
<class '__main__.XiaoMing'>

类装饰器

刚刚我们接触的装饰器是函数来完成,实际上由于 python 的灵活性, 我们用类也可以实现一个装饰器。

类能实现装饰器的功能, 是由于当我们调用一个对象时,实际上调用的是它的 __call__ 方法。

class Demo:
    def __call__(self):
        print('我是 Demo')

demo = Demo()
demo()
我是 Demo

通过这个特性,我们便可以用类的方式来完成装饰器,功能与刚开始用函数实现的一致。

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

    def __call__(self, *args, **kwargs):
        print('123')
        return self.func(*args, **kwargs)

@Decorator
def say_hello():
    print('同学你好')

say_hello()
123
同学你好

用函数来能实现的功能为什么需要类来实现?

因为通过类我们可以将执行过程拆解到各函数中,降低代码的复杂度,甚至可以通过类属性实现一些复杂功能。

比如说我们有一些计算耗时很长的函数,并且每次计算的结果不变,那么我们就可以通过类定义一个缓存装饰器,来缓存第一次执行的结果。

import time

class Cache:
    __cache = {}

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

    def __call__(self):

        # 如果缓存字典中有这个方法的执行结果
        # 直接返回缓存的值
        if self.func.__name__ in Cache.__cache:
            return Cache.__cache[self.func.__name__]

        # 计算方法的执行结果
        value = self.func()
        # 将其添加到缓存
        Cache.__cache[self.func.__name__] = value
        # 返回计算结果
        return value

@Cache
def long_time_func():
    time.sleep(5)
    return '我是计算结果'

start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')

start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')
我是计算结果
计算耗时5.001157283782959秒
我是计算结果
计算耗时0.0秒

实战

实现一个装饰器,能够计算函数的执行耗时。

解析

import time

def calc(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f'{func.__name__}函数执行耗时{end-start}秒。')
        return result

    return wrapper

@calc
def count_2():
    for i in range(0, 2):
        print(f'第{i+1}次计数')
        time.sleep(1)  # 暂停一秒钟

@calc
def count_5():
    for i in range(0, 5):
        print(f'第{i+1}次计数')
        time.sleep(1)  # 暂停一秒钟

count_2()
count_5()
第1次计数
第2次计数
count_2函数执行耗时2.0178287029266357秒。
第1次计数
第2次计数
第3次计数
第4次计数
第5次计数
count_5函数执行耗时5.0245983600616455秒。
点击查看

评论

登录参与讨论

暂无评论

共 0 条
  • 1
前往
  • 1

课程目录

第一课:命名空间与作用域 第二课:闭包 第三课:装饰器 第四课:迭代器 第五课:生成器

学习遇到困难?微信扫码进入社群与小伙伴一起交流讨论。