Python学习笔记二十三(闭包 / 装饰器 )

时间:2021-09-12 22:46:13

函数

什么是函数? 将具有某种功能的代码放到一起, 构成一个函数.
为什么说函数? 因为需要研究一个问题, 函数可以嵌套调用, 那么可不可以嵌套定义?

函数的嵌套调用
def func1():
    print("func1")


def func2():
    # 嵌套调用
    func1()


func1()
func2()

# 运行结果
# func1
# func1

函数能使用函数名调用, 那么函数名是什么? 标识符, 标识符里包含变量 / 函数名, 那么函数能不能通过变量传递 / 调用吗?

def func1():
    print("func1")


def func2():
    # 函数通过变量传递
    f2 = func1    
    print("f2 type:%s" % type(f2))
    # 通过变量调用函数
    f2()


func1()
func2()

# 运行结果
# func1
# f2 type:<class 'function'>
# func1

函数可以通过变量传递 / 调用,那么回到最初的问题, 函数能嵌套调用, 函数能嵌套定义吗?

闭包
def func2():
    # 定义函数func1
    def func1():
        print("func1")


func2()

# 运行结果 

通过运行结果可以得出, 可以嵌套定义 ( 没报错:) ) , 那么为什么没有结果?
函数能够传递,函数在定义的时候不会执行, 那么可以明确一点func2 被调用,所以func2 被执行了.
func2 被执行 ,那么func1 定义在func2 函数体内, 所以func1 被定义了, 但是func1 没有被调用.那么能不能通过函数传递调用func1?

def func2():
    # 定义函数func1
    def func1():
        print("func1")

    return func1


f = func2()  # 得到func1 的引用 
print(type(f))  # 查看f 的类型
f()  # 调用函数

# 运行结果
# <class 'function'>
# func1

这种形式叫闭包1, 在函数内部定义另一个函数, 并且外部函数func2 返回内部函数func1 的引用.
虽然这是一个闭包, 但是没人会这么用, 用两个函数去做一个函数可以完成的事, 而且还这么麻烦.
相信大家都画过数学中的函数图像, 画图像需要确定坐标, 今天就用程序确定 x = 某值时 , y的值 ( y= x的平方 )

def func2(x):
    # 内部函数func1 要使用一个X 的值, 需要从外部传入
    # 为什么要从外部传入?
    def func1():
        print(x ** 2)

    return func1


f = func2(2)  # 得到func1 的引用
f()  # 调用函数

# 运行结果
# 4

一定要从外部传入吗?不一定,就上面这个例子只是简单演示闭包的用法, 而且上面的例子用一个函数也可以完成, 那么闭包大多数的应该用在哪? 装饰器或者叫语法糖.

函数装饰器

只要你写过类方法 / 静态方法, 那么你就用过装饰器.下面来看看装饰器

print("start")  # 开始Debug


# 函数定义 会创建 不会执行
def func2(x):
    print("func2 被调用")
    print("x type = %s" % type(x))

    def func1():
        print("func1 被调用")
        x()

    return func1


@func2 # 程序暂停,执行 func3 = func2(func3)
def func3():
    print("func3 被调用")


func3()  # 调用函数

# 运行结果
# start
# func2 被调用
# x type = <class 'function'>
# func1 被调用
# func3 被调用

装饰器的运行 debug

Python学习笔记二十三(闭包 / 装饰器 )

装饰器运行 图解

Python学习笔记二十三(闭包 / 装饰器 )

装饰器运行 文字描述
  • 程序执行到 line:5, func2 被定义 ,
  • 程序执行到 line:16是一个装饰器,不执行,
    • 运行(检测) line:17 定义func3, 同时检测到是一个函数,返回 line:16 执行func2
    • 如果line:17 不是函数,继续向下执行检测,直到遇到函数, 再次返回
  • func2 被执行 ( 因为@func2 ==> func3 = func2(func3) ),
    • x = 原func3 ,既 x 指向原func3 的函数体;
    • print(“func2 被调用”) # func2 被调用
      print(“x type = %s” % type(x)) # x type = <class ‘function’>
    • func1 被定义
    • return func1, 既 func3 = func2(func3) = func1,装饰完成func3 被指向func1
  • 程序执行到 line:21,因为func3 在上面被装饰过,所以现在的func3( 装饰完的 ) 指向func1
    • func3() ==> func1(), 既 line:10 被执行, print(“func1 被调用”)
    • line:11 因为x = 原func3 ,所以 x() 原func3 的方法体被执行, print(“func3 被调用”)
    • 函数调用完成, 返回被调用处 ,所以 print(“func3 被调用”) ( line:18) 完成后 返回 x() 处(line:11) ,
    • x() 执行完(line:11) , 返回func3() 处 (line:21), 程序执行完成
特殊记忆方法

根据上面的装饰器运行过程, 我们尝试推出一个方法, 便于记忆和解决装饰器的过程.

Python学习笔记二十三(闭包 / 装饰器 )

使用特殊记忆方法
print("start")  # 开始Debug


# 函数定义 不会执行
def func2(x):
    print("func2 被调用")
    print("x type = %s" % type(x))

    def func1():
        print("func1 被调用")
        x()

    return func1

def func4(x):
    print("func4 被调用")
    print("x type = %s" % type(x))

    def func5():
        print("func5 被调用")
        x()

    return func5
@func4
@func2
def func3():
    print("func3 被调用")


func3()  # 调用函数

# 运行结果
# start
# func2 被调用
# x type = <class 'function'>
# func4 被调用
# x type = <class 'function'>
# func5 被调用
# func1 被调用
# func3 被调用

Python学习笔记二十三(闭包 / 装饰器 )

装饰过程:

  • func3 是一个函数的定义压栈
  • @func2 得到func3 函数的引用 压栈
  • func2 被调用 func1 压栈 ( func2 被调用 x type = <class ‘function’>)
  • @func4 的到func1 函数的引用 压栈
  • func4 被调用 func5 压栈 ( func4 被调用 x type = <class ‘function’>)

调用过程:

  • 装饰后调用func3 ,func3 指向func5 ,func5被调用
    • func5 出栈, func5 被调用
    • x(),x 指向func1 func1 被调用
  • func1 被调用 func4 出栈 ( 装饰过程,执行过,出栈无效果, 只有func4出栈, func1 在栈顶次可以调用) ,func1 被调用
    • func1 出栈, func1 被调用
    • x(),x 指向func3 func3 被调用

通过上面的我们大概了解装饰过程,以及装饰后的调用过程, 那么如果是有参有返回值的函数怎么装饰呢?

首先我们知道了 闭包外部函数的形参存储着原函数( 被装饰函数) , 闭包内部调用了原函数( 被装饰函数),因为装饰器的调用是由解释器完成的,既@闭包外部函数 , 外部函数的参数个数我们无法改变, 那么只能改变闭包内部函数给 原函数( 被装饰函数)传参.

Python学习笔记二十三(闭包 / 装饰器 )

如图, func3 需要有参数,@func2 的参数个数无法改变, 我们尝试改变func1 的参数,同时x 最终指向原函数,所以x 必须和func3 的参数一致.

print("start")  # 开始Debug


# 函数定义 不会执行
def func2(x):
    def func1(*args, **kwargs):
        print("func1 被调用")
        print("args %s " % args.__str__())
        print("kwargs %s " % kwargs.__str__())
        x(*args, **kwargs)

    return func1


@func2
def func3(*args, **kwargs):
    print("func3 被调用")
    print("args %s " % args.__str__())
    print("kwargs %s " % kwargs.__str__())


func3("Dragon", "Fang", Dragon=18, Fang="nan")  # 调用函数

# 运行结果
# start
# func1 被调用
# args ('Dragon', 'Fang') 
# kwargs {'Dragon': 18, 'Fang': 'nan'} 
# func3 被调用
# args ('Dragon', 'Fang') 
# kwargs {'Dragon': 18, 'Fang': 'nan'} 

通过改变闭包内部函数可以给带参被装饰函数传参, 那么返回值怎么办?

  • 调用函数都会回到被调用处,装饰后func3() 调用,既func1 被调用
  • func1 执行, x() 被调用==>原func3 被调用, 原func3执行完成有返回值,返回到 x() 处,
  • x() 得到值, 如果不将值返回, 那么func1 执行完默认返回None,既 装饰后func3() 处的到None 值,所以 x() 处需要将得到的值返回.
print("start")  # 开始Debug


# 函数定义 不会执行
def func2(x):
    def func1(*args, **kwargs):
        print("func1 被调用")
        return x(*args, **kwargs)

    return func1


@func2
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"


print(func3("Dragon", "Fang", Dragon=18, Fang="nan"))

# 运行结果
# start
# func1 被调用
# func3 被调用
# DragonFang

上面的装饰器可以装饰以下四种函数

  • 无参数,无返回值
  • 无参数,有返回值
  • 有参数,无返回值
  • 有参数,有返回值
装饰器应用场景

装饰器可以在不改变原函数的基础上,添加新的功能 ( 符合开闭原则2), 例如计算函数运行耗时, 在优化时常用.

import time


def func(func_args):
    def in_func(*args, **kwargs):
        # 在函数开始运行前,得到当前时间
        start_time = time.time()

        result_value = func_args(*args, **kwargs)

        # 在函数运行结束后,获取当前时间,作差 ==> 得到函数的运行时间
        print(time.time() - start_time)
        return result_value

    return in_func

@func
def test():
    time.sleep(2)

test()

# 运行结果
# 2.00081205368042
函数装饰器-给装饰器传参

有个问题,函数内部嵌套函数的定义是两层, 如果是三层呢?或者更多层呢?先看看三层,三层往上不讨论,层级太深不是一件好事.

def out_test():
    def test(x):
        def in_test(*args, **kwargs):
            print("func1 被调用")
            return x(*args, **kwargs)

        return in_test

    return test 

如上, 三层函数, 首先这是一个函数, 可用通过out_test() 得到test(x) 函数的引用, 然后可以通过test(x) 的到in_test (*args, **kwargs)函数的引用.
那么如果使用这么一个函数作为装饰器,会是什么效果?

Python学习笔记二十三(闭包 / 装饰器 )

报错?缺少参数?添加参数,添加不定长参数,一步到位 :)

print("start")  # 开始Debug


def out_test(*args, **kwargs):
    print("out_test 被调用")

    def test(x):
        print("test 被调用")

        def in_test(*args, **kwargs):
            print("in_test 被调用")
            return x(*args, **kwargs)

        return in_test

    return test


@out_test("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"


print(func3("Dragon", "Fang", Dragon=18, Fang="nan"))

# 运行结果
# start
# out_test 被调用
# test 被调用
# in_test 被调用
# func3 被调用
# DragonFang

最外层被调用,然后内层被调用, out_test 被调用, 然后 test 被调用, 最后 in_test 被调用, 那么他们分别作了什么事?

def out_test(*args, **kwargs):
    print("out_test 被调用")
    print(args)
    print(kwargs)

    def test(x):
        print("test 被调用")

        print(x)

        def in_test(*args, **kwargs):
            print("in_test 被调用")
            print(args)
            print(kwargs)
            return x(*args, **kwargs)

        return in_test

    return test


@out_test("test", Dragon=20)
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"


print(func3("Dragon", "Fang", Dragon=18, Fang="nan"))

# 运行结果 
# out_test 被调用
# ('test',)
# {'Dragon': 20}
# test 被调用
# <function func3 at 0x000001BAA0C48A60>
# in_test 被调用
# ('Dragon', 'Fang')
# {'Dragon': 18, 'Fang': 'nan'}
# func3 被调用
# DragonFang

out_test 被调用打印了值, 然后test 被调用 , func3 被打印, 因此我们可以推测出@out_test(“test”, Dragon=20) ==> out_test(“test”, Dragon=20) 然后 使用 out_test 的返回值对func3 进行装饰

那么可以通过三层函数的形式给装饰器传参.

类装饰器

@classmethod / @staticmethod 都是类装饰器

# classmethod 源码
class classmethod(object):
    """ classmethod(function) -> method Convert a function to be a class method. A class method receives the class as implicit first argument, just like an instance method receives the instance. To declare a class method, use this idiom: class C: @classmethod def f(cls, arg1, arg2, ...): ... It can be called either on the class (e.g. C.f()) or on an instance (e.g. C().f()). The instance is ignored except for its class. If a class method is called for a derived class, the derived class object is passed as the implied first argument. Class methods are different than C++ or Java static methods. If you want those, see the staticmethod builtin. """
    def __get__(self, *args, **kwargs): # real signature unknown
        """ Return an attribute of instance, which is of type owner. """
        pass

    def __init__(self, function): # real signature unknown; restored from __doc__
        pass

    @staticmethod # known case of __new__
    def __new__(*args, **kwargs): # real signature unknown
        """ Create and return a new object. See help(type) for accurate signature. """
        pass

    __func__ = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default

    __isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default


    __dict__ = None # (!) real value is ''

仿照@classmethod ,制作一个类装饰器.

Python学习笔记二十三(闭包 / 装饰器 )

报错?init魔法方法缺少参数?给参数

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print(args)
        print(kwargs)


@ClassMethod
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"

# 运行结果
# (<function func3 at 0x000001CC350C2E18>,)
# {}

@ClassMethod 调用了init 魔法方法? 那么应该有一个ClassMethod 类的对象被创建,
既@ClassMethod ==> ClassMethod() 得到对象, 使用对象装饰func3.

验证,调用装饰后的func3

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print(args)
        print(kwargs)


@ClassMethod
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"

func3()
# 运行结果
# Traceback (most recent call last):
# File "E:/workspace/pycharm/pycharm/model.py", line 12, in <module>
# func3()
# (<function func3 at 0x00000276789F2E18>,)
# TypeError: 'ClassMethod' object is not callable
# {}

报错? ‘ClassMethod’ object is not callable ,对象不可调用, 证明有一对象被创建, 而且对象需要有一个回调方法. 魔法方法__call__ 3 可以使一个实例对象变为一个可调用对象.

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print("__init__")
        print(args)
        print(kwargs)

    def __call__(self, *args, **kwargs):
        print("__call__")
        print(args)
        print(kwargs)

@ClassMethod
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"

func3()
# 运行结果
# __init__
# (<function func3 at 0x00000204124F2E18>,)
# {}
# __call__
# ()
# {}

成功运行,证明我们的类装饰器没问题, 那么函数装饰器能传参, 类装饰器怎么传参? call 魔法方法有两个空值, 是不是可以利用一下?

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print("__init__")
        print(args)
        print(kwargs)

    def __call__(self, *args, **kwargs):
        print("__call__")
        print(args)
        print(kwargs)


@ClassMethod("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    return "DragonFang"


func3()

# 运行结果
# Traceback (most recent call last):
# __init__
# File "E:/workspace/pycharm/pycharm/model.py", line 19, in <module>
# ('Dragon', 'Fang')
# func3()
# {'Dragon': 18, 'Fang': 'nan'}
# TypeError: 'NoneType' object is not callable
# __call__
# (<function func3 at 0x000001FCCACB2E18>,)
# {}

call魔法方法被执行, 输出 (<function func3 at 0x000001FCCACB2E18>,) 和 {} ,然后
‘NoneType’ object is not callable ,空对象不能调用? 我们知道函数 或者方法 默认是有默认返回值的, 默认值是None ,也就是说 call 魔法方法返回了一个None 给调用处,既@ClassMethod(“Dragon”, “Fang”, Dragon=18, Fang=”nan”) 处, 使用 @None 装饰func3 是不对的, 那么我们返回一个函数, 既使用函数装饰器装饰func3

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print("__init__")
        self.args = args
        self.kwargs = kwargs

    def __call__(self, func):
        print("__call__")
        self.func = func
        return self.test

    def test(self):
        print("test")
        self.func(*self.args, **self.kwargs)


@ClassMethod("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    print(args)
    print(kwargs)


func3()

# 运行结果
# __init__
# __call__
# test
# func3 被调用
# ('Dragon', 'Fang')
# {'Dragon': 18, 'Fang': 'nan'}

运行成功,给类装饰器传参完成,有几点需要注意

  • 参数,参数是传给init 魔法方法的,需要使用属性接收
  • call 魔法方法返回的是一个函数的引用
  • test方法中,self.func 是原函数, 通过属性传不定长参数需要拆包

类装饰器传参-方式2

上面是在初始化时传参,那能不能将通过方法传参?

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print("__init__")
        self.args = args
        self.kwargs = kwargs

    def __call__(self, func):
        print("__call__")
        self.func = func
        return self.test
    @classmethod
    def test(cls, *args, **kwargs): 
        cls.args = args
        cls.kwargs = kwargs


@ClassMethod.test("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    print(args)
    print(kwargs)


func3()

# 运行结果
# Traceback (most recent call last):
# test
# File "E:/workspace/pycharm/pycharm/model.py", line 18, in <module>
# @ClassMethod.test("Dragon", "Fang", Dragon=18, Fang="nan")
# TypeError: 'NoneType' object is not callable

test 被打印,说明test 类方法被调用, ‘NoneType’ object is not callable 又是这错误! 给test类方法一个返回值

class ClassMethod(object):
    def __init__(self, *args, **kwargs):
        print("__init__")

        print(args)
        print(kwargs)

    def __call__(self):
        print("__call__")

    @classmethod
    def test(cls, *args, **kwargs):
        cls.args = args
        cls.kwargs = kwargs
        return cls


@ClassMethod.test("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    print(args)
    print(kwargs)


func3()

# 运行结果
# __init__
# (<function func3 at 0x0000023DD1962E18>,)
# {}
# __call__

通过以上实验可以推断@ClassMethod.test(“Dragon”, “Fang”, Dragon=18, Fang=”nan”)
==> 先调用test 类方法, 然后 调用 init魔法方法初始化对象, 最后通过实例对象装饰func3
那么按照这个顺序完善一下ClassMethod 类

class ClassMethod(object):
    def __init__(self, func):
        print("__init__")

        self.func = func

    def __call__(self):
        return self.func(*ClassMethod.args, **ClassMethod.kwargs)

    @classmethod
    def test(cls, *args, **kwargs):
        cls.args = args
        cls.kwargs = kwargs
        return cls


@ClassMethod.test("Dragon", "Fang", Dragon=18, Fang="nan")
def func3(*args, **kwargs):
    print("func3 被调用")
    print(args)
    print(kwargs)


func3()

# 运行结果
# __init__
# func3 被调用
# ('Dragon', 'Fang')
# {'Dragon': 18, 'Fang': 'nan'}

perfect 完美

总结:

  • 闭包:函数内部嵌套函数的定义,构成闭包
  • 闭包的作用:大部分用在装饰器
  • 装饰器:@函数名 / @类名 的形式叫做装饰器或者语法糖
  • 装饰器的作用:在不改变原函数的基础上添加功能
  • 装饰前的函数原函数,被闭包外部函数的形参保存
  • 装饰后的函数,实际上是闭包的内部函数,
  • 通用装饰器格式:
# 不需要给装饰器传参
def func(func_args):
    def in_func(*args, **kwargs):
        print("添加功能")
        return func_args(*args, **kwargs)

    return in_func
# 需要给装饰器传参
def out_func(*out_args, **out_kwargs):
    def func(func_args):
        def in_func(*args, **kwargs):
            print("添加功能")
            return func_args(*args, **kwargs)

        return in_func

    return func
  • 装饰器特殊记忆方式,出栈入栈,入栈: 装饰过程,出栈:调用过程
  • 类装饰器,类需要包含 init 魔法方法 和 call 魔法方法 构成基本的类装饰器
  • 类装饰器传参,除了init 魔法方法 和 call 魔法方法 之外需要定义另外的方法
    • @ClassMethod(“Dragon”, “Fang”, Dragon=18, Fang=”nan”), 这种传参方式需要定义另外的方法, 辅助调用原函数
    • @ClassMethod.test(“Dragon”, “Fang”, Dragon=18, Fang=”nan”), 这种传参方式需要定义另外的方式, 辅助保存参数

到此结 DragonFangQy 2018.5.26