Python装饰器

Python装饰器

何为装饰器

何为装饰器?顾名思义,就是在一个东西上面添加了一些东西。添加的东西对原来的东西来说就是一种装饰。
在Python中,装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。将返回来的函数对象赋值给原函数变量,则就相当于在原函数不变的基础上给原函数添加了新的功能。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

假设有一个函数

def foo():
    print('i am foo')

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码,我们可以将函数修改为:

def foo():
    print('i am foo')
    logging.info("foo is running")

现在函数bar()和bar2()也有类似的需求,怎么做?再写一个logging在bar函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个函数:专门处理日志 ,日志处理完之后再执行真正的业务代码。
由于函数是一个对象。这意味着函数:能在函数中定义一个函数、能作为参数传递、能作为返回值、可以被定义在另外一个函数内。这些事装饰器可以实现的基础。

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()
def foo():
    print('i am foo')
def bar():
    print('i am bar')
def bar2():
    print('i am bar2')  
use_logging(foo)  
use_logging(bar)
use_logging(bar2)

逻辑上不难理解, 但是这样的话,我们每次都要将一个函数作为参数传递给use_logging函数。而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,执行运行bar(),但是现在不得不改成use_logging(bar)。那么有没有更好的方式的呢?不用改变函数名称也能添加功能呢?当然有,答案就是装饰器。

简单装饰器

def use_logging(func):
    def wrapper(*args, **kwargs):
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

def bar():
    print('i am bar')
bar = use_logging(bar)
bar()

函数use_logging就是装饰器,它把执行真正业务方法的func包裹在函数里面,看起来像bar被use_logging装饰了。在这个例子中,函数进入和退出时 ,被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。执行过程就是use_logging(bar)–>wapper()–>bar()。不难理解,在这段代码中,我们将一个函数 bar 作为参数传入函数use_logging,返回一个 wrapper 函数并赋值到bar,此时执行 bar 相当于执行 wrapper 函数。当我们执行 wrapper 函数时会先执行logging.warn 再执行先前传入的 func 参数也就是 bar函数。

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

在这里,use_logging 这个函数就是一个装饰器,功能是在执行被装饰的函数之前打印函数名。
在 python 中, 有一种语法糖可以代替bar = use_logging(bar)这一步的操作,@符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作,以上的代码可以改写成。

def use_logging(func):
    def wrapper(*args, **kwargs):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapper
@use_logging
def bar():
    print("i am bar")
bar()

@use_logging
def foo():
    print("i am foo")
foo()

如上所示,这里在函数前加上 @use_logging相当于在定义函数后执行了一条语句,bar = use_logging(bar) 。这样我们就可以省去bar = use_logging(bar)这一句了,直接调用bar()即可得到想要的结果。如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

带参数的装饰器

上面的装饰器虽然实现了业务函数添加功能的效果,但是在业务函数之外所添加的新功能区域还不可以传递参数,为了实现这个功能,我们可以在装饰器外部再套上一层函数,用该函数的参数接收我们想要打印的数据(用在业务函数之外,装饰器之内的参数),并将先前的 decorator 函数作为返回值。这就是闭包的一种功能,就是用闭包来生成一个命名空间,在命名空间中保存我们要打印的值 value。

def decorator(value):
    def use_logging(func):
        def wrapper(*args, **kwargs):
            print(value)
            logging.warning("%s is running" % func.__name__)
            return func(*args)
        return wrapper
    return use_logging
@decorator(value="warn")
def bar(name):
    print("i am %s" % name)
bar('bar')

这样我们就可以在业务函数之外使用参数value了,业务函数使用自己原来的参数。

类装饰器

再来看看类装饰器,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器依靠类内部的_ call() _方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。
_ _ call() _ _方法作用 是由于当我们调用一个对象时,实际上调用的是它的 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秒

functools.wraps 装饰器

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、 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 函数 (say_hello = decorator(say_hello) 返回来的是函数wrapper),导致函数名,帮助文档变成了 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

内置装饰器

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

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'>

参考资料

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值