• 聊聊Python中的闭包和装饰器


    1. 闭包

    首先我们明确一下函数的引用,如下所示:

    def test1():
        print("--- in test1 func----")
    
    # 调用函数
    test1()
    
    # 引用函数
    ret = test1
    
    print(id(ret))
    print(id(test1))
    
    #通过引用调用函数
    ret()

    运行结果:

    --- in test1 func----
    140212571149040
    140212571149040
    --- in test1 func----

    以y=kx+b为例,请计算一条线上的某个点,即给x值计算出y值。下面以这个例子引出闭包的概念。

    方法1

    # 第1种
    k = 1
    b = 2
    y = k*x+b
    # 缺点:如果需要多次计算,那么就的写多次y = k*x+b这样的式子

    方法2

    # 第2种
    def line_2(k, b, x):
        print(k*x+b)
    
    line_2(1, 2, 0)
    line_2(1, 2, 1)
    line_2(1, 2, 2)
    # 缺点:如果想要计算多次这条线上的y值,那么每次都需要传递k,b的值,麻烦

    方法3

    # 第3种: 全局变量
    k = 1
    b = 2
    def line_3(x):
        print(k*x+b)
    
    line_3(0)
    line_3(1)
    line_3(2)
    k = 11
    b = 22
    line_3(0)
    line_3(1)
    line_3(2)
    # 缺点:如果要计算多条线上的y值,那么需要每次对全局变量进行修改,代码会增多,麻烦

    这里引申一下,关于全局变量,要是直接读取,不修改的话,是不用加global的。

    而且所谓的修改,指的是地址变了,假如我指向的是一个列表,指向没变,列表中的内容改变了,也不用加global的。

    我们看一下下面的例子:

    a = 100
    b = 'chenchi'
    c = [1, 2, 3]
    
    def func():
        print('a的值:{},a的地址:{}'.format(a, id(a)))
        print('b的值:{},b的地址:{}'.format(b, id(b)))
        print('c的值:{},c的地址:{}'.format(c, id(c)))
    
    def func2():
        global a
        global b
        a += 1
        b += 'ccc'
        # 这里c不用声明global,c的地址没有发生变化
        c.append(4)
    
    if __name__ == '__main__':
        func()
        func2()
        func()

    运行结果:

    列表的地址没变,不用加global。

    方法4

    # 第4种:缺省参数
    def line_4(x, k=1, b=2):
        print(k*x+b)
    
    line_4(0)
    line_4(1)
    line_4(2)
    
    line_4(0, k=11, b=22)
    line_4(1, k=11, b=22)
    line_4(2, k=11, b=22)
    # 优点:比全局变量的方式好在:k, b是函数line_4的一部分 而不是全局变量,因为全局变量可以任意的被其他函数所修改
    # 缺点:如果要计算多条线上的y值,那么需要在调用的时候进行传递参数,麻烦

    方法5

    # 第5种:实例对象
    class Line5(object):
        def __init__(self, k, b):
            self.k = k
            self.b = b
    
        def __call__(self, x):
            print(self.k * x + self.b)
    
    
    line_5_1 = Line5(1, 2)
    # 对象.方法()
    # 对象()
    line_5_1(0)
    line_5_1(1)
    line_5_1(2)
    line_5_2 = Line5(11, 22)
    line_5_2(0)
    line_5_2(1)
    line_5_2(2)
    # 缺点:为了计算多条线上的y值,所以需要保存多个k, b的值,因此用了很多个实例对象, 浪费资源

    关于__call__()的用法:

    方法6

    采用闭包的方式,什么是闭包呢?

    在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。

    如下所示:

    # 第6种:闭包
    def line_6(k, b):
        def create_y(x):
            print(k*x+b)
        return create_y
    
    
    line_6_1 = line_6(1, 2)
    line_6_1(0)
    line_6_1(1)
    line_6_1(2)
    line_6_2 = line_6(11, 22)
    line_6_2(0)
    line_6_2(1)
    line_6_2(2)

    闭包,红色部分return的不能加(),不加()是函数引用,否则就应该是函数的返回值。

    在上面的例子中,函数line_6与变量k,b构成闭包,闭包具有提高代码可复用性的作用。

    由于闭包引用了外部函数的局部变量,则外部函数的局部变量没有及时释放,消耗内存,但是相对于实例对象来说,已经好多了。

    思考:函数、匿名函数、闭包、对象 当做实参时 有什么区别?
    1. 匿名函数能够完成基本的简单功能,传递是这个函数的引用 只有功能。
    2. 普通函数能够完成较为复杂的功能,传递是这个函数的引用 只有功能。
    3. 闭包能够将较为复杂的功能,传递是这个闭包中的函数以及数据,因此传递是功能+数据。
    4. 对象能够完成最为复杂的功能,传递是很多数据+很多功能,因此传递是功能+数据。

    修改外部函数中的变量

    x = 300
    def outer():
        x = 200
        def inner():
            nonlocal x
            # global x
            print('---1---x=%d' % x)
            x = 100
            print('---1---x=%d' % x)
        return inner
    
    t1 = outer()
    t1()

    注意,这是python3,输出结果如下:

    如果把nonlocal改成global,第一个x就变成300了。

    2. 装饰器

    在聊Python中的装饰器之前,我们明确一个概念:Python中的装饰器就是实现了设计模式中的装饰器模式,只不过不像Java用的实现接口的方式,而是采用闭包的方式。

    装饰器模式可以参考我的博客:

    https://www.cnblogs.com/DarrenChan/p/10343885.html

    我们从一个简单的例子入手:

    def set_func(func):
        def call_func():
            print("---这是权限验证1----")
            print("---这是权限验证2----")
            func()
        return call_func
    
    # 下面就相当于test1 = set_func(test1)
    @set_func
    def test1():
        print("-----test1----")
    
    
    test1()

    运行结果:

    这里就相当于是实现了一个代理,我们重新执行的test1()实际上是代理。

    我们所营造出来的一种效果就是,test1()加不加@set_func,方法执行的方式是不变的。

    @set_func的含义是:

    set_func(test1)

    我们可以用任何一个方式来接收,比如:

    ret = set_func(test1)

    ret()

    这样执行结果没有变化,但是感觉不协调,所以我们用test1来接收,即

    test1 = set_func(test1)

    test1()

    就是把test1重新指向了call_func()函数。

    我们可以看到,set_func函数里面就是闭包。

    一个装饰器可以对多个函数进行装饰

    def set_func(func):
        def call_func(a):
            print("---这是权限验证1----")
            print("---这是权限验证2----")
            func(a)
        return call_func
    
    
    @set_func  # 相当于 test1 = set_func(test1)
    def test1(num):
        print("-----test1----%d" % num)
    
    
    @set_func  # 相当于 test2 = set_func(test2)
    def test2(num):
        print("-----test2----%d" % num)
    
    
    test1(100)
    test2(200)

    运行结果:

    装饰器在没有调用函数之前已经装饰了

    def set_func(func):
        print("---开始进行装饰")
        def call_func(a):
            print("---这是权限验证1----")
            print("---这是权限验证2----")
            func(a)
        return call_func
    
    
    @set_func  # 相当于 test1 = set_func(test1)
    def test1(num):
        print("-----test1----%d" % num)
    
    
    @set_func  # 相当于 test2 = set_func(test2)
    def test2(num):
        print("-----test2----%d" % num)
    
    # 装饰器在调用函数之前,已经被python解释器执行了,所以要牢记 当调用函数之前 其实已经装饰好了,尽管调用就可以了
    # test1(100)
    # test2(200)

    运行结果:

    对不定长参数的函数进行装饰

    def set_func(func):
        print("---开始进行装饰")
        def call_func(*args, **kwargs): # 这里是形参,相当于定义,前面接收元组多个,后面接收字典多个
            print("---这是权限验证1----")
            print("---这是权限验证2----")
            # func(args, kwargs)  # 不行,相当于传递了2个参数 :1个元组,1个字典
            func(*args, **kwargs)  # 这里是实参,拆包,对应拆元组和拆字典
        return call_func
    
    
    @set_func  # 相当于 test1 = set_func(test1)
    def test1(num, *args, **kwargs):
        print("-----test1----%d" % num)
        print("-----test1----" , args)
        print("-----test1----" , kwargs)
    
    
    test1(100)
    test1(100, 200)
    test1(100, 200, 300, mm=100)

    运行结果:

    代码中的红色部分,传值的问题,可以参考:

    https://www.cnblogs.com/DarrenChan/p/7931735.html

    对带有返回值的函数进行装饰(装饰模板)

    def set_func(func):
        print("---开始进行装饰")
        def call_func(*args, **kwargs):
            print("---这是权限验证1----")
            print("---这是权限验证2----")
            # func(args, kwargs)  # 不行,相当于传递了2个参数 :1个元组,1个字典
            return func(*args, **kwargs)  # 拆包
        return call_func
    
    
    @set_func  # 相当于 test1 = set_func(test1)
    def test1(num, *args, **kwargs):
        print("-----test1----%d" % num)
        print("-----test1----" , args)
        print("-----test1----" , kwargs)
        return "ok"
    
    
    @set_func
    def test2():
        pass
    
    ret = test1(100)
    print(ret)
    
    ret = test2()
    print(ret)

    运行结果:

    你可能会问,如果被修饰的函数没有返回值,那么红色部分是否会报错?

    在Python中不会的,返回None而已。

    这也是我们的一个模板写法。

    多个装饰器对同一个函数进行装饰

    def add_qx(func):
        print("---开始进行装饰权限1的功能---")
        def call_func(*args, **kwargs):
            print("---这是权限验证1----")
            return func(*args, **kwargs)
        return call_func
    
    
    def add_xx(func):
        print("---开始进行装饰xxx的功能---")
        def call_func(*args, **kwargs):
            print("---这是xxx的功能----")
            return func(*args, **kwargs)
        return call_func
    
    
    @add_qx
    @add_xx
    def test1():
        print("------test1------")
    
    
    test1()

    运行结果:

    这就跟包裹一样,红色部分越上面的越先执行。至于开始装饰为什么先执行的xx,是因为@add_qx找下面,发现不是函数,而@add_xx下面才是函数,所以开始进行装饰的顺序反过来了。

    举例:

    def set_func_1(func):
        def call_func():
            # "<h1>haha</h1>"
            return "<h1>" + func() + "</h1>"
        return call_func
    
    def set_func_2(func):
        def call_func():
            return "<td>" + func() + "</td>"
        return call_func
    
    
    @set_func_1
    @set_func_2
    def get_str():
        return "haha"
    
    print(get_str())

    结果:

    <h1><td>haha</td></h1>

    使用类当做装饰器

    class Test(object):
        def __init__(self, func):
            self.func = func
    
        def __call__(self):
            print("这里是装饰器添加的功能.....")
            return self.func()
    
    
    @Test  # 相当于get_str = Test(get_str)
    def get_str():
        return "haha"
    
    print(get_str())

    运行结果:

    装饰器带参数

    这种情况需要在原有的外面,再次嵌套一层,作为外部变量传参。

    运行结果:

  • 相关阅读:
    Mysql数据备份命令
    git命令大全
    git 安装及普通命令
    git学习资料
    php 魔术方法 和 魔术常量
    react事件处理(绑定)
    java父类、子类构造函数调用过程
    java字符串String的intern()方法,举例说明
    java关键字static使用的“坑”(准备、初始化)
    java虚拟机
  • 原文地址:https://www.cnblogs.com/DarrenChan/p/10346935.html
Copyright © 2020-2023  润新知