美文网首页Python
闭包、装饰器

闭包、装饰器

作者: 柏丘君 | 来源:发表于2017-06-14 17:45 被阅读134次

    在学习 Python 的时候,庆幸自己有 JavaScript 的基础,在学习过程中,发现许多相似的地方,如导包的方式、参数解包、lambda 表达式、闭包、引用赋值、函数作为参数等。
    装饰器是 Python 语言中的一个重点,要学习装饰器的使用,应该首先从闭包看起,以掌握装饰器的基本原理。

    词法作用域

    闭包是函数能够访问并记住其所在的词法作用域,由于 Python 和 JavaScript 都是基于词法作用域的,所以二者闭包的概念是共通的。
    为了说明 Python 是基于词法作用域,而不是动态作用域,我们可以看下面的例子:

    def getA():
        return a
    
    def test():
        a = 100
        print(getA())
    
    test()
    

    运行结果为:

    Traceback (most recent call last):
      File "C:\Users\Charley\Desktop\py\py.py", line 8, in <module>
        test()
      File "C:\Users\Charley\Desktop\py\py.py", line 6, in test
        print(getA())
      File "C:\Users\Charley\Desktop\py\py.py", line 2, in getA
        return a
    NameError: name 'a' is not defined
    

    报错了~我们再在 getA 函数所在的作用域声明一个变量 a

    def getA():
        return a
        
    a = 10010
    def test():
        a = 100
        print(getA())
    
    test()
    

    运行结果为:

    10010
    

    这里输出了 10010 ,说明 getA 函数是依赖于词法作用域的,其作用域在函数定义伊始就决定的,其作用域不受调用位置的影响。
    理解了词法作用域,就理解了闭包。

    闭包

    前面说到过:闭包是函数能够访问并记住其所在的词法作用域的特性。那么只要函数拥有这个特性,这个函数就是一个闭包,理论上,所有函数都是闭包,因为他们都可以访问其所在的词法作用域。像这样的函数也是一个闭包:

    a = 100
    def iAmClosure():
        print(a)
    
    iAmClosure()
    

    理论上是如此,但在实际情况下,闭包的定义要复杂一点点,但仍然基于前面的理论:如果一个函数(外层函数)中返回另外一个函数(内层函数),内层函数能够访问外层函数所在的作用域,这就叫一个闭包。
    下面是一个例子:

    def outer():
        a = 100
        def inner():
            print(a)
        return inner
    
    outer()()
    

    运行结果如下:

    100
    

    如上所示,inner 函数就是一个闭包。

    闭包中调用参数函数

    同 JavaScript,Python 中也可以将函数作为参数传递,由于 Python 是引用传值,因此实际上传入的参数并不是原始函数本身,而是原始函数的一个引用。基于闭包的特性,我们也可以在闭包中访问(或者说调用)这个函数:

    def outer(fn):
        def inner():
            # 闭包能够访问并记住其所在的词法作用域
            # 因此在闭包中可以调用 fn 函数
            fn()
        return inner
    
    def test():
        print("We will not use 'Hello World'")
    
    ret = outer(test)
    ret()
    

    运行结果:

    We will not use 'Hello World'
    

    装饰器引入

    认识了闭包,就可以来说一说装饰器了。想象有这么一种需求:
    你所在的公司有一些核心的底层方法,后来公司慢慢壮大,增加了其他的部门,这些部门都有自己的业务,但它们都会使用这些核心的底层方法,你所在的部门也是如此。
    有一天,项目经理找到你,让你在核心代码的基础上加一些验证之类的玩意,但是不能修改核心代码(否则会影响到其他的部门),你该怎么做呢?
    首先你可能想到这种方式:

    def core():
        pass
    
    def fixCore():
        doSometing()...
        core()
    
    fixCore()
    

    通过一个外层函数将 core 函数进行包装,在执行了验证功能后再调用 core 函数。
    这时项目经理又说了,你不能改变我们的调用方式呀,我们还是想以 core 的方式进行调用,于是你又修改了代码:

    def core():
        pass
    
    tmp = core;
    def fixCore():
        tmp()
    
    core = fixCore
    core()
    

    通过临时函数 tmp 交换了 corefixCore,狸猫换太子。这下就可以愉快的直接使用 core 了。
    这是项目经理又说了,我们需要对多个核心函数进行包装,总不能全部使用变量交换吧,并且这样很不优雅,再想想其他的办法?
    好吧,要求真多!于是你想啊想,想到了闭包的方式:将需要包装的函数作为参数传入外层函数,外层函数返回一个内层函数,该函数在执行一些验证操作后再调用闭包函数。这样做的好处是:

    • 可以对任意函数进行包装,只要将函数作为参数传入外层函数
    • 可以在执行外层函数时对返回值进行任意命名

    你写的代码是这个样子的:

    # 外层函数,接收待包装的函数作为参数
    def outer(fn):
        def inner():
            doSometing()...
            fn()
        return inner
    
    # 核心函数1
    def core1():
        pass
    
    # 核心函数2
    def core2():
        pass
    
    # core1 重新指向被包装后的函数
    core1 = outer(core1)
    # core2 重新指向被包装后的函数
    core2 = outer(core2)
    
    # 调用核心函数
    core1()
    core2()
    

    大功告成!简直完美。同时恭喜你,你已经实现了一个装饰器,装饰器的核心原理就是:闭包 + 函数实参。

    Python 原生装饰器支持

    在上面你已经实现了一个简单的装饰器,也知道了装饰器的基本原理。其实,在 Python 语言中,有着对装饰器的原生支持,但核心原理依旧不变,只是简化了一些我们的操作:

    # 外层函数,接收待包装的函数作为参数
    def outer(fn):
        def inner():
            print("----验证中----")
            fn()
        return inner
    
    # 应用装饰器
    @outer
    # 核心函数1
    def core1():
        print("----core1----")
    # 应用装饰器
    @outer
    # 核心函数2
    def core2():
        print("----core2----")
    
    core1()
    core2()
    

    运行结果如下:

    ----验证中----
    ----core1----
    ----验证中----
    ----core2----
    

    Python 原生的装饰器支持,省去了传参和重命名的步骤,应用装饰器时,会将装饰器下方的函数(这里为 core1core2)作为参数,并生成一个新的函数覆盖原始的函数。

    装饰器函数的执行时机

    装饰器函数(也就是我们前面所说的外层函数)在什么时候执行呢?我们可以进行简单的验证:

    def outer(fn):
        print("----正在执行装饰器----")
        def inner():
            print("----验证中----")
            fn()
        return inner
    
    @outer
    def core1():
        print("----core1----")
    
    @outer
    def core2():
        print("----core2----")
    

    运行结果为:

    ----正在执行装饰器----
    ----正在执行装饰器----
    

    这里我们并没有直接调用 core1core2 函数,装饰器函数执行了。也就是说,解释器执行过程中碰到了装饰器,就会执行装饰器函数

    多重装饰器

    我们也可以给函数应用多个装饰器:

    def outer1(fn):
        def inner():
            print("----outer1 验证中----")
            fn()
        return inner
    
    def outer2(fn):
        def inner():
            print("----outer2 验证中----")
            fn()
        return inner
    
    @outer2
    @outer1
    def core1():
        print("----core1----")
    
    core1()
    

    运行结果如下:

    ----outer2 验证中----
    ----outer1 验证中----
    ----core1----
    

    从输出效果中可以看到:装饰器的执行是从下往上的,底层装饰器执行完成后返回函数再传给上层的装饰器,以此类推。

    给被装饰函数传参

    如果我们需要给被装饰函数传参,就需要在装饰器函数返回的 inner 函数上做文章了,让其代理接受被装饰器函数的参数,再传递给被装饰器函数:

    def outer(fn):
        def inner(*args,**kwargs):
            print("----outer 验证中----")
            fn(*args,**kwargs)
        return inner
    
    
    @outer
    def core(*args,a,b):
        print("----core1----")
        print(a,b)
    
    core(a = 1,b = 2)
    

    运行结果为:

    ----outer 验证中----
    ----core1----
    1 2
    

    这里提一下 参数解包的问题:在 inner 函数中的 *** 表示该函数接受的可变参数和关键字参数,而调用参数函数 fn 时使用 *** 表示对可变参数和关键字参数进行解包,类似于 JavaScript 中的扩展运算符 ...。如果直接将 argskwargs 作为参数传给被装饰函数,那么被装饰函数接收到的只是一个元组和字典,所以需要在解包后传入。

    对有返回值的函数进行包装

    如果被包装函数有返回值,如何在包装获取返回值呢?先看一下下面的例子:

    def outer(fn):
        def inner():
            print("----outer 验证中----")
            fn()
        return inner
    
    @outer
    def core():
        return "Hello World"
    
    print(core())
    

    运行结果为:

    ----outer 验证中----
    None
    

    为什么函数执行的返回值是 None 呢?不应该是 Hello World 吗?这是因为装饰的过程其实是引用替换的过程,在装饰之前,core 变量指向其自初始的函数体,在装饰后就重新进行了指向,指向到了装饰器函数所返回的 inner 函数,我们没有给 inner 函数定义返回值,自然在调用装饰后的 core 函数也是没有返回值的。为了让装饰后的函数仍有返回值,我们只需让 inner 函数返回被装饰前的函数的返回值即可

    def outer(fn):
        def inner():
            print("----outer 验证中----")
            return fn()
        return inner
    
    @outer
    def core():
        return "Hello World"
    
    print(core())
    

    运行结果如下:

    ----outer 验证中----
    Hello World
    

    装饰器的参数

    有时候我们想要根据不同的情况对函数进行装饰,可以有以下两种处理方式:

    • 定义多个不同条件下的装饰器,根据条件应用不同的装饰器
    • 定义一个装饰器,在装饰器内部根据条件的不同进行装饰

    第一种方法很简单,这里说一下第二种方式。
    要在装饰器内部对不同条件进行判断,我们就需要一个或多个参数,将参数传入:

    # main 函数接受参数,根据参数返回不同的装饰器函数
    def main(flag):
        # flag 为 True
        if flag:
            def outer(fn):
                def inner():
                    print("立下 Flag")
                    fn()
                return inner
            return outer
        # flag 为 False
        else:
            def outer(fn):
                def inner():
                    print("Flag 不见了!")
                    fn()
                return inner
        return outer
    
    # 给 main 函数传入 True 参数
    @main(True)
    def core1():
        pass
    
    # 给 main 函数传入 False 参数
    @main(False)
    def core2():
        pass
    
    core1()
    core2()
    

    运行结果如下:

    立下 Flag
    Flag 不见了!
    

    上面我们根据给 main 传入不同的参数,对 core1core2 函数应用不同的装饰器。这里的 main 函数并不是装饰器函数,其返回值才是装饰器函数,我们是根据 main 函数的返回值对目标函数进行装饰的

    类作为装饰器

    除了函数,类也可以作为装饰器,在说类作为装饰器之前,首先需要了解 __call__ 方法。

    __call__ 方法

    我们创建的实例也是可以调用的, 调用实例对象将会执行其内部的 __call__ 方法,该方法需要我们手动实现,如果没有该方法,实例就不能被调用:

    class Test(object):
        def __call__(self):
            print("我被调用了呢")
    
    t = Test()
    t()
    

    运行结果:

    我被调用了呢
    

    类作为装饰器

    我们已经知道对象的 __call__ 方法在对象被调用时执行,其实类作为装饰器的结果就是将被装饰的函数指向该对象,在调用该对象时就会执行对象的 __call__ 方法,要想让被装饰的函数执行 __call__ 方法,首先会创建一个对象,因此会连带调动 __new____init__ 方法,在创建对象时,test 函数会被当做参数传入对象的 __init__ 方法。

    class Test(object):
        # 定义 __new__ 方法
        def __new__(self,oldFunc):
            print("__new__ 被调用了")
            return object.__new__(self)
        # 定义 __init__ 方法
        def __init__(self,oldFunc):
            print("__init__ 被调用了")
        # 定义 __call__ 方法
        def __call__(self):
            print("我被调用了呢")
    
    # 定义被装饰函数
    @Test
    def test():
        print("我是test函数~~")
    
    test()
    

    运行结果:

    __new__ 被调用了
    __init__ 被调用了
    我被调用了呢
    

    保存原始的被装饰函数

    装饰后的 test 函数指向了新建的对象,那么有没有办法保存被装饰之前的原始函数呢?通过前面我们已经知道,在新建对象的时候,被装饰的函数会作为参数传入 __new____init__ 方法,因此我们可以在这两个方法中获取原始函数的引用:

    class Test(object):
        # 定义 __new__ 方法
        def __new__(self,oldFunc):
            print("__new__ 被调用了")
            return object.__new__(self)
        # 定义 __init__ 方法
        def __init__(self,oldFunc):
            print("__init__ 被调用了")
            self.__oldFunc = oldFunc
        # 定义 __call__ 方法
        def __call__(self):
            print("我被调用了呢")
            self.__oldFunc()
    
    # 定义被装饰函数
    @Test
    def test():
        print("我是test函数~~")
    
    test()
    

    运行结果如下:

    __new__ 被调用了
    __init__ 被调用了
    我被调用了呢
    我是test函数~~
    

    总结

    本文主要讲到了 Python 中闭包和装饰器的概念,主要有以下内容:

    • Python 是基于词法作用域
    • 闭包是函数能记住并访问其所在的词法作用域
    • 利用礼包实现简单的装饰器
    • Python 原生对装饰器的支持
    • 给函数应用多个装饰器
    • 如何给被装饰函数传参
    • 如何给有返回值的函数应用装饰器
    • 如何根据不同条件为函数应用不同的装饰器
    • 类作为装饰器的情况以及 __call__ 方法

    完。

    相关文章

      网友评论

        本文标题:闭包、装饰器

        本文链接:https://www.haomeiwen.com/subject/rwnsqxtx.html