13. 装饰器

一、什么是装饰器

  装饰器 指的定义一个函数,该函数是用来装饰其它函数的,即为其它函数添加额外的功能。

  通过装饰器和闭包,可以在不修改原来函数的情况下对函数进行扩展。在开发中,我们都是通过装饰器来扩展函数的功能。在定义函数时,我们可以通过 @装饰器,来使用指定的装饰器装饰当前函数。

def outer(func):
    """
    用来对其它函数进行扩展,使其函数可以在执行前打印开始执行,执行后打印执行结束
    @param func 要扩展的函数
    @return inner 返回扩展函数
    """

    print("outer()开始执行!")
  
    # 创建一个新函数
    def inner(*args, **kw_args):
        # 在原函数执行前扩展功能
        print("inner()开始执行!")

        # 调用被扩展的函数
        result = func(*args, **kw_args)

        # 在原函数执行后拓展功能
        print("inner()执行结束!")

        # 返回原函数执行结果
        return result
  
    print("outer()执行结束!")
  
    # 返回新函数
    return inner

  当 Python 解释器遇到 @outer 代码时,它会将 outer 当作可执行的对象来调用,即 outer(func),并且将下面的 f() 函数指向的代码块当作实参进行传递,此时变成了 outer(f)。接下来执行 outer(func) 函数,在执行的过程中 func 变量指向了 f 指向的那个代码块。然后,将 inner 指向的代码块这个引用当作返回值返回。接下来,让 f 这个变量保存 outer(f) 的返回值。此时会让 f 指向了 inner 指向的那个函数。也就是说,f 指向变成了 outer(func) 函数的返回值。

# 使用@装饰器为函数指定装饰器
@outer
def f():
    print("f()函数执行了!")

@outer 这个装饰器对 f 装饰的时间不是因为 f() 的调用,而是在 Python 解释器从上向下解析到 @outer 的时候就会执行,即开始装饰;

  当执行 f() 时,f() 会调用 inner 指向的那个代码块,即接下来要执行 inner() 函数,当执行到 result = func(*args, **kw_args) 语句时,会跳转到原来 f 刚开始指向的那个函数代码块。

f()

装饰器能够让原本的函数在调用的时候变成了先调用添加的功能,然后在调用原函数的功能,即实现了未修改原代码的情况下,可以对原函数添加功能的效果。但是只能在原函数运行之前或者运行之后添加吗,不能在原函数运行一半时添加。

装饰器将原函数的引用当做实参传递到闭包中,它修改原函数指向为闭包中的内部函数;

二、通用装饰器

  使用装饰器后,我们将原函数名指向的内存地址偷梁换柱成装饰器内部新创建函数(inner())的内存地址。所以,我们应该将装饰器内部创建的函数做的跟原函数一样才可以。但是上面的代码,还存在一些问题。例如运行如下代码,输入的信息不是原函数的信息而是装饰器内部创建函数的信息。

print(f.__name__)
help(f)

  我们我们可以将原函数的属性手动赋值给装饰器内部创建的属性。

def outer(func):
    """
    用来对其它函数进行扩展,使其函数可以在执行前打印开始执行,执行后打印执行结束
    @param func 要扩展的函数
    @return inner 返回扩展函数
    """

    # 创建一个新函数
    def inner(*args, **kw_args):
        # 在原函数执行前扩展功能
        print("inner()开始执行!")

        # 调用被扩展的函数
        result = func(*args, **kw_args)

        # 在原函数执行后拓展功能
        print("inner()执行结束!")

        # 返回原函数执行结果
        return result
  
    # 手动将原函数的属性赋值给创建的额新函数
    inner.__name__ = func.__name__
    inner.__doc__ = func.__doc__
  
    # 返回新函数
    return inner

  此时,我们将原函数的属性赋值给装饰器内部创建的函数,但是由于原函数有太多的属性,我们难免有一些遗漏的属性忘记赋值。为此,Python 内部提供了工具自动完成这件事。

from functools import wraps

def outer(func):
    """
    用来对其它函数进行扩展,使其函数可以在执行前打印开始执行,执行后打印执行结束
    @param func 要扩展的函数
    @return inner 返回扩展函数
    """

    # 创建一个新函数
    @wraps(func)
    def inner(*args, **kw_args):
        # 在原函数执行前扩展功能
        print("inner()开始执行!")

        # 调用被扩展的函数
        result = func(*args, **kw_args)

        # 在原函数执行后拓展功能
        print("inner()执行结束!")

        # 返回原函数执行结果
        return result
  
    # 返回新函数
    return inner

三、叠加多个装饰器

  我们可以同时为一个函数指定多个装饰器,这样函数将会按照从内向外的顺序被装饰。

from functools import wraps

def make_bold(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        print("make_bold装饰器开始执行!")
        result = func(*args, **kwargs)
        print("make_bold装饰器执行结束!")
        return "<b>" + result + "</b>"
    return wrapped

def make_italic(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        print("make_italic装饰器开始执行!")
        result = func(*args, **kwargs)
        print("make_italic装饰器执行结束!")
        return "<i>" + result + "</i>"
    return wrapped
# 装饰顺序从下往上
@make_bold
@make_italic
def test():
    return "hello world!"

  当 Python 解释器执行到 @make_bold 时,发现下一行不是函数,而是另一个装饰器,所以会暂停执行 @make_bold,而是让下一行的装饰器 @make_italic 先执行。

  Python 解释器执行 @make_italic,发现下一行是 test() 函数,相当于执行 test = make_italic(test)。此时的形参 func 指向 test。执行完之后,test 重新指向 make_italic 代码块内的 wrapped 代码块。

  然后,接着又回去执行 @make_blod,相当于执行 test = make_blod(test)。此时的形参 func 指向 test,即 make_italic 代码块内的 wrapped 代码块。执行完之后,test 又重新指向 make_bold 代码块内的 wrapped 代码块。

  最终,test 指向 @make_bold 装饰器的内部函数 wrapped,而 @make_bold 装饰器的形参 func 又指向 @make_italic 装饰器的内部函数 wrapped,@make_italic 装饰器的形参 func 又指向原来的 test 指向的代码块。

# 调用顺序自上而下
print(test())

装饰顺序从下往上,调用顺序自上而下;

四、有参装饰器

  由于我们使用 @装饰器 的方法,外层函数只能有一个参数,并且该参数只能用来接收被装饰对象的内存地址。我们可以在两层函数的基础上在套一层函数。

from functools import wraps

def auth(db_type="file"):
    def middle(func):
        @wraps(func)
        def inner(*args, **kw_args):
            if db_type == "file":
                print("基于文件的验证")
                result = func(*args,**kw_args)
                return result
            elif db_type == "MySQL":
                print("基于MySQL数据库的验证")
                result = func(*args,**kw_args)
                return result
            elif db_type == "SQLite":
                print("基于SQLite数据库的验证")
                result = func(*args,**kw_args)
                return result
            else:
                print("不支持其它验证方式!")
                result = func(*args,**kw_args)
                return result
        return inner
    return middle
@auth(db_type="MySQL")
def index():
    print("返回首页")

index()

  执行 @auth(db_type="MySQL") 相当于以下步骤:

  1. 执行 auth("MySQL"),得到返回值 middle;
  2. 将上一步得到的返回值进行调用,此时会执行 middle(index),得到返回值 inner;
  3. 将第 2 步得到的返回值 inner 赋值给 home;
@auth(db_type="SQLite")
def home():
    print("返回主页")

home()
@auth()
def showInfo(name,age):
    print("name: ", name, ", age: ", age)

showInfo("Sakura",10)

五、类装饰器

  装饰器函数其实是这样一个接口约束,它必须接受一个 callable 对象作为参数,然后返回一个 callable 对象。在 Python 中,一般 callable 对象都是函数。但也有例外,只要某个对象重写了 __call__() 方法,那么这个对象就是 callable 的。

class Test:
    def __init__(self, func):
        print("---初始化---")
        print("func name is %s" % func.__name__)
        self.__func = func
  
    def __call__(self):
        print("---装饰器中的功能---")
        self.__func()
@Test
def test():
    print("---test---")

  当用 Test 类作为装饰器对 test() 函数进行装饰的时候,首先会创建 Test 的实例对象,并且会把 test 这个函数名当做参数传递到 __init__() 方法中形参 func,即在 __init__() 中的属性 __func 指向了 test 指向的函数。test 指向了用 Test 创建出来的实例化对象。

test()

  当在使用 test() 进行调用时,就相当于直接调用实例对象,因此会调用这个对象的 __call__() 方法。为了能够在 __call__() 方法中调用原来 test 指向的函数体,所以在 __init__() 方法中就需要一个实例属性来保存这个函数体的引用。所以,才有了 self.__func = func 这句代码,从而在调用 __call__() 方法中能够调用到 test 之前的函数体。

六、有参类装饰器

class Test:
    def __init__(self, data):
        print("__init__()方法执行")
        self.__data = data
  
    def __call__(self, func):
        print("__call__()方法执行")
        self.__func = func
        return self.call_old_func

    def call_old_func(self):
        print("开始调用装饰器中的功能1")
        self.__func()
        print("开始调用装饰器中的功能2")

@Test(100)
def test():
    print("---test---")

test()
posted @ 2024-09-30 18:50  星光映梦  阅读(13)  评论(0编辑  收藏  举报