一文彻底搞懂Python装饰器

测试开发栈

共 16723字,需浏览 34分钟

 · 2021-08-10

什么是装饰器?

Python装饰器(fuctional decorators)就是用于拓展原来函数功能的一种函数,目的是在不改变原函数名(或类名)的情况下,给函数增加新的功能。 

这个函数的特殊之处在于它的返回值也是一个函数,这个函数是内嵌“原" 函数的函数。

一般而言,我们要想拓展原来函数代码,最直接的办法就是侵入代码里面修改,例如:

import time

def f():
    print("hello")
    time.sleep(1)
    print("world"

这是我们最原始的的一个函数,然后我们试图记录下这个函数执行的总时间,那最简单的做法就是改动原来的代码:

import time
def f():
    start_time = time.time()
    print("hello")
    time.sleep(1)
    print("world")
    end_time = time.time()
    execution_time = (end_time - start_time)*1000
    print("time is %d ms" %execution_time)

但是实际工作中,有些时候核心代码并不可以直接去改,所以在不改动原代码的情况下,我们可以再定义一个函数。(但是生效需要再次执行函数)

import time
def deco(func):
    start_time = time.time()
    f()
    end_time = time.time()
    execution_time = (end_time - start_time)*1000
    print("time is %d ms" %execution_time)

def f():
    print("hello")
    time.sleep(1)
    print("world")

if __name__ == '__main__':
    deco(f)
    print("f.__name__ is",f.__name__)
    print()


输出如下:

helloworldtime is 1000 msf.__name__ is f


这里我们定义了一个函数deco,它的参数是一个函数,然后给这个函数嵌入了计时功能。但是想要拓展这一千万个函数功能,

就是要执行一千万次deco()函数,所以这样并不理想!接下来,我们可以试着用装饰器来实现,先看看装饰器最原始的面貌。


import time
def deco(f):
    def wrapper():
        start_time = time.time()
        f()
        end_time = time.time()
        execution_time = (end_time - start_time) * 1000
        print("time is %d ms" % execution_time)

    return wrapper

@deco
def f():
    print("hello")
    time.sleep(1)
    print("world")

if __name__ == '__main__':
    f()

 

输出如下:

helloworldtime is 1000 ms


这里的deco函数就是最原始的装饰器,它的参数是一个函数,然后返回值也是一个函数。

其中作为参数的这个函数f()就在返回函数wrapper()的内部执行。然后在函数f()前面加上@deco,

f()函数就相当于被注入了计时功能,现在只要调用f(),它就已经变身为“新的功能更多”的函数了,(不需要重复执行原函数)。 


扩展1:带有固定参数的装饰器

import time
def deco(f):
    def wrapper(a,b):
        start_time = time.time()
        f(a,b)
        end_time = time.time()
        execution_time = (end_time - start_time)*1000
        print("time is %d ms" % execution_time)
    return wrapper

@deco
def f(a,b):
    print("be on")
    time.sleep(1)
    print("result is %d" %(a+b))

if __name__ == '__main__':
    f(3,4)


输出如下:

be onresult is 7time is 1000 ms


扩展2:无固定参数的装饰器


import time
def deco(f):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        f(*args, **kwargs)
        end_time = time.time()
        execution_time = (end_time - start_time)*1000
        print("time is %d ms" %execution_time)
    return wrapper

@deco
def f(a,b):
    print("be on")
    time.sleep(1)
    print("result is %d" %(a+b))

@deco
def f2(a,b,c):
    print("be on")
    time.sleep(1)
    print("result is %d" %(a+b+c))

if __name__ == '__main__':
    f2(3,4,5)
    f(3,4)


输出如下:

be onresult is 12time is 1000 msbe onresult is 7time is 1000 ms

扩展3:使用多个装饰器,装饰一个函数

 

import  time
def deco01(f):
    def wrapper(*args, **kwargs):
        print("this is deco01")
        start_time = time.time()
        f(*args, **kwargs)
        end_time = time.time()
        execution_time = (end_time - start_time)*1000
        print("time is %d ms" % execution_time)
        print("deco01 end here")
    return wrapper

def deco02(f):
    def wrapper(*args, **kwargs):
        print("this is deco02")
        f(*args, **kwargs)
        print("deco02 end here")
    return wrapper

@deco01
@deco02
def f(a,b):
    print("be on")
    time.sleep(1)
    print("result is %d" %(a+b))

if __name__ == '__main__':
    f(3,4)


输出如下:


this is deco01this is deco02be onresult is 7deco02 end heretime is 1009 msdeco01 end here


装饰器调用顺序

装饰器是可以叠加使用的,那么使用装饰器以后代码是啥顺序呢?

对于Python中的”@”语法糖,装饰器的调用顺序与使用 @ 语法糖声明的顺序相反。

在这个例子中,f(3, 4) 等价于 deco01(deco02(f(3, 4)))。


Python内置装饰器

在Python中有三个内置的装饰器,都是跟class相关的:staticmethod、classmethod 和property。

  • staticmethod 是类静态方法,其跟成员方法的区别是没有 self 参数,并且可以在类不进行实例化的情况下调用

  • classmethod 与成员方法的区别在于所接收的第一个参数不是 self (类实例的指针),而是cls(当前类的具体类型)

  • property 是属性的意思,表示可以通过通过类实例直接访问的信息

对于staticmethod和classmethod这里就不介绍了,通过一个例子看看property。

class Foo(object):

    def __init__(self, var):
        super(Foo, self).__init__()
        self.__var = var

    @property
    def var(self):
        return self.__var

    @var.setter
    def var(self, var):
        self.__var = var


if __name__ == "__main__":
    foo = Foo("var1")
    print(foo.var)
    foo.var = "var2"
    print(foo.var)


注意,对于Python新式类(在 py3 里面的继承 object 的类(默认),以及它的子类都是新式类),如果将上面的 “@var.setter” 装饰器所装饰的成员函数去掉,则Foo.var 属性为只读属性,使用 “foo.var = ‘var 2′” 进行赋值时会抛出异常。但是,对于Python classic class,所声明的属性不是 read-only的,所以即使去掉”@var.setter”装饰器也不会报错。


参数化装饰器

在实际代码中可能需要使用参数化的装饰器。如果用函数作为装饰器的话,那么解决方法很简单:再增加一层包装。例如:


def repeat(number=3):
    """多次重复执行原始函数,返回最后一次调用的值作为结果"""
    def actual_decorator(function):
        def wrapper(*args, **kwargs):
            result = None
            for i in range(number):
                result = function(*args, **kwargs)
            return result
        return wrapper
    return actual_decorator

@repeat()  # 即使有默认参数,这里的括号也不能省略
def f():
    print("喵")

@repeat(2)
def g(a,b):
    print("g函数执行中")
    return a + b

if __name__ == "__main__":
    f()
    print()
    print(g(1, 2))



  • 保存原始函数的文档字符串和函数名

    看下面的例子:


def dumy_dec(function):
    def wrapped(*args, **kwargs):
        """包装函数内部文档"""
        return function(*args, **kwargs)
    return wrapped

@dumy_dec
def function():
    """原始的文档字符串"""

if __name__ == "__main__":
    print(function.__name__)
    print(function.__doc__)


使用装饰器后,我们如果想查看原始函数的函数名或原始函数的文档字符串,返回的却是:


wrapped包装函数内部文档


解决这个问题的正确办法,是使用functools模块内置的wraps()装饰器。


from functools import wraps

def preserving_dec(function):
    @wraps(function)  # 注意看这里!
    def wrapped(*args, **kwargs):
        """包装函数内部文档"""
        return function(*args, **kwargs)
    return wrapped

@preserving_dec
def function():
    """原始的文档字符串"""
if __name__ == "__main__":
    print(function.__name__)
    print(function.__doc__)


结果如下:


function原始的文档字符串


用类来实现装饰器


装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。由此使用用户自定义的类也可以实现装饰器:


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

    def __call__(self, *args, **kwargs):
        print("Do something here before call The original function")
        print(self.function.__doc__)
        result = self.function(*args, **kwargs)
        print("Do something here after call The original function")
        return result

@Decorator
def add(a, b):
    """原始函数的文档字符串"""
    print("The original function is running")
    return a + b

if __name__ == "__main__":
    add(1, 2)


结果如下:

Do something here before call The original function原始函数的文档字符串The original function is runningDo something here after call The original function


  • 不能装饰静态方法和类方法


from datetime import datetime

def logging(func):
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
        return func(*args, **kwargs)
    return wrapper

class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 装饰实例方法,OK
    def run(self):
        print(f"{self.model} is running!")

    @logging  # 装饰静态方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print(f"The model of your car is {obj.model}")
        else:
            print(f"{obj} is not a car!")

car = Car("麒麟")
car.run()
Car.check_model_for(car)


会报错:


Traceback (most recent call last):  File "F:/AutoOps_platform/装饰器.py", line 253, in <module>    Car.check_model_for(car)  File "F:/AutoOps_platform/装饰器.py", line 231, in wrapper    print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))AttributeError: 'staticmethod' object has no attribute '__name__'[DEBUG] 2021-08-06 13:11:19.070195: enter run()麒麟 is running!


@staticmethod 这个装饰器,其实返回的并不是一个callable对象,而是一个staticmethod 对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod 之前就好了:

@staticmethod
@logging  # 先装饰就没问题
def check_model_for(obj):
    if isinstance(obj, Car):
        print(f"The model of your car is {obj.model}")
    else:
        print(f"{obj} is not a car!")


[DEBUG] 2021-08-06 13:14:40.404121: enter run()麒麟 is running![DEBUG] 2021-08-06 13:14:40.404121: enter check_model_for()The model of your car is 麒麟


使用第三方库优化你的装饰器

decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。


from decorator import decorate
from datetime import datetime

def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
    return func(*args, **kwargs)

def logging(func):
    return decorate(func, wrapper)  # 用wrapper装饰func
@logging
def f(a, b):
    return a + b

f(1,2)


你也可以使用它自带的@decorator装饰器来完成你的装饰器。


from decorator import decorator
from datetime import datetime

@decorator
def logging(func, *args, **kwargs):
    print(f"[DEBUG] {datetime.now()}: enter {func.__name__}()")
    return func(*args, **kwargs)

@logging
def f(a, b):
    return a + b

f(1,2)


[DEBUG] 2021-08-06 13:19:44.920748: enter f()


decorator.py实现的装饰器能完整保留原函数的name,doc和args。


也可以使用第三方库wrapt:

import wrapt

@wrapt.decorator  # without argument in decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print(f"[DEBUG]: enter {wrapped.__name__}()")
    return wrapped(*args, **kwargs)

@logging
def say(something):
    print(f"is saying {something}")

say("喵")

[DEBUG]: enter say()is saying 喵


使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。


如果你需要使用wrapt写一个带参数的装饰器,可以这样写:

def logging(level):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print("[{}]: enter {}()".format(level, wrapped.__name__))
        return wrapped(*args, **kwargs)
    return wrapper

@logging(level="INFO")
def say(something):
    print(f"is saying {something}")

say("喵")


[INFO]: enter say()is saying 喵



总结:

本文介绍了Python装饰器的一些使用,装饰器的代码还是比较容易理解的。只要通过一些例子进行实际操作一下,就很容易理解了。



测试开发栈

软件测试开发合并必将是趋势,不懂开发的测试、不懂测试的开发都将可能被逐渐替代,因此前瞻的技术储备和知识积累是我们以后在职场和行业脱颖而出的法宝,期望我们的经验和技术分享能让你每天都成长和进步,早日成为测试开发栈上的技术大牛~~


长按二维码/微信扫描关注


欢迎加入QQ群交流和提问:427020613

互联网测试开发一站式全栈分享平台


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报