抛开“闭包”,用最简单的方法理解python修饰器

python修饰器

在说修饰器之前,我们先从一些简单的东西入手。

函数也是一个对象

首先说明一点,当我想强调函数的功能的时候我会称之为”函数“,而我想强调函数的对象性质的时候我会称之为”函数对象“,而“函数”与“函数对象”本质上都是同样一个东西。

有一个函数a:

def a():
    return "这是函数a"

我们可以这样定义一个变量varA:

def a():
    return "这是函数a"


varA = a()
print(varA)

最终,你能看到结果“这是函数a”。varA = a()这句语句的作用是,先执行a函数,然后把函数返回的结果存储在varA中。

但是,如果你把上面的代码这样改一下,返回的结果会大有不同:

def a():
    return "这是函数a"


varA = a  # 把a后面的括号去掉了
print(varA)

如果你是新手,你看到这个代码的第一反应应该是,这个代码会报错。因为a是一个变量,但是这个代码里并没有定义一个变量a,所以varA = a这行代码应该因为之前没有定义过变量a而报错。

然而实际结果是,你看到了这么一个东西:<function a at 0x000001BE4A9CCD30>

这是什么意思呢?

大家知道,在python中是万物皆对象的。如果你还没有学过面向对象编程,建议你先去b站看一下这个老师讲的教程。也就是说,python中的函数也是一种对象,会有一个变量指向这个对象,否则这个对象会被定义为“垃圾”而被回收掉。我们在定义完a函数以后,python自动帮我们创建了一个名为a的变量,并且这个名为a的变量指向的是a这个函数对象。所以我们用varA = a的时候,并没有报错,这个a指向的是函数对象a的内存地址,也就是 0x000001BE4A9CCD30 。

简单来讲,就是varA = a 并没有执行函数a,只是让varA指向的是函数对象a的内存地址;而varA = a()则是先执行函数a,把执行完的结果赋值给varA。

我们再看以下代码:

def a():
    return "这是函数a"


varA = a
print(varA())  # varA后面加了括号

这样你就能如愿的看到“这是函数a”了。varA()执行了varA所指向的函数对象a。

这个视频中,up还做了一个很有趣的实验:

def a():
    return "这是函数a"


varA = a
del a  # 把a删除掉
print(varA)

最终显示出了函数a的内存地址。这样操作的逻辑是,先通过varA创建了一个变量,这个变量指向函数对象a。我们使用del a删除的只是创建函数a自动生成的这个a变量,并没有删除掉函数对象a。究其原因,是因为还有变量varA指向着函数对象a,当没有任何变量指向函数对象a的时候函数对象a才会被回收掉。

另一个函数

我们再写一个函数:

def a():
    print("这是函数a")

def b(func):
    print("这是函数b")
    return func

print(b(a))

我们得到了两个结果,上面的是“这是函数b”,下面的是<function at 0x000002E00A81CD30>。来给大家分析一下这个程序是怎么执行的吧:

首先它执行了函数b,向函数b传入了a变量作为参数,这个a变量指向的是函数对象a。在执行函数b的时候,首先输出了”这是函数b“,然后返回了传入的函数对象a的内存地址。如果把return func改为return func(),它就会返回”这是函数a“而不再是< function at 0x000002E00A81CD30 >。

在上面的例子中,如果我们使用b(a),我们会得到print方法显示出的”这是函数b“,和函数对象a。如果我们想额外看到”这是函数a“,则执行一下返回的函数对象a就好了,即:

def a():
    print("这是函数a")

def b(func):
    print("这是函数b")
    return func  # 返回传入的函数对象

varA = b(a)      # 此时varA是函数对象,因为传入b的值是函数对象a
print(varA())    # varA()返回函数A的值

至于你会问,为什么我们要先让varA指向b(a)返回的函数对象a,再通过varA()执行函数a,而不直接用a()执行函数a。这里我只是举一个简单的但没什么用的例子,实际的应用场景一定会比这个例子有用的多,但同时也会复杂得多,为了简单才写了这样一个例子。但同时,要记住我们的目标是理解python的修饰器怎么用,而不是纠结于我给出的例子。

修饰器

终于,我们开始要学习修饰器了。我们使用修饰器的方法,把上面的例子改写一下:

def b(func):
    print("这是函数b")
    return func  # 返回传入的函数对象

@b  # 这里b指的是函数b
def a():
    print("这是函数a")

print(a())

最后显示出了三行字,第一行是”这是函数b“,第二行是”这是函数a“,第三行是”None“。

这里大家可以注意到我把函数b和函数a换了一下位置,让函数b再函数a上面,这是因为@b这个修饰器需要调用函数b,函数b一定要在被调用的修饰器之前才可以成功被调用,不然会报错。

在def a():上面添加修饰器@b,等同于我们用varA = b(a)让varA这个变量指向函数b返回的函数对象a,再通过varA()让函数对象a运行。

因此,修饰器是一种语法糖,它不改变原有代码的功能,也可以被非修饰器的语法所替代,但这样写可以方便程序员coding。在程序运行时,程序会把@b转换为a = b(a),然后运行a()。

我把python实际执行的代码也放出来,和我写的代码是有些不同的:

def b(func):
    print("这是函数b")
    return func  # 返回传入的函数对象

def a():
    print("这是函数a")

a = b(a)      # 此时的a等同于我之前写的varA,是一个全新的变量,指向函数b所返回的函数对象
a()

使用修饰器时,需要有一个传入参数和返回值都是函数对象的函数。在上面的例子中,传入参数和返回值都是同一个函数对象,也就是函数对象a;而在实际操作中,我们并不需要让传入参数和返回参数是同一个函数对象。

使用修饰器的特点是,使用者并不知道有函数b的存在,便能执行函数b的功能。而对于开发者可以把原本在函数a中的一些功能从函数a中剥离出去,方便代码的维护。


2021.7.28更新

昨天我写的“闭包函数”部分是错的,今天已更正。在接下来我讲一讲为什么修饰器需要一个传入参数和返回值都是函数对象的函数,以及正确的函数闭包。

为什么需要函数的传入参数和返回值都是函数对象

我们直接看代码吧:

def b(func):
    return func

@b
def a():
    pass

a()

此时程序运行的顺序是,程序先运行第八行的a(),然后找到第五行的def a():,此时第四行有一个修饰器,所以程序知道它要把第八行的a(),改成这样两行代码:

a = b(a)
a()

其实程序只是在运行a()前,自动添加了a = b(a)这样一行代码。我们来分析一下这个代码吧:

由于通过a()运行了函数a,说明a是在指向着一个函数对象;又因为a = b(a),我们可以得到b(a)是一个函数对象。

我们暂且不管b(a)的传入参数,仅仅观察函数的返回值b(),根据前面的推到,b(),也就是函数b的返回值也一定是一个函数对象。

下面我们来看看b(a)中的传入参数a是个什么东西。如果你有基础知识的话,你就不难理解此时的a和赋值号前面的a不是一个东西。由于前面我们并没有手动给变量a赋值,那么这个a就是创建函数a时程序自动生成的变量a,它指向着函数对象a。因此,使用修饰器以后默认给函数b传入函数对象a,函数b就要接收它,不然程序就会报错。因此函数b的传入参数就要是一个函数对象。

闭包函数和修饰器

网上能找到的讲解python修饰器的材料,基本都是先讲闭包函数,再讲修饰器。我们也来学习一下闭包函数,并看看它和修饰器之间的联系。闭包函数一般有以下的特点:

  • 闭包的函数在一个函数内;
  • 闭包函数需要引用外部函数的变量。

因此,上文中使用的函数b并不是一个闭包函数。如果你想判断一个函数是不是闭包函数,可以使用“函数名.__closure__”来查看,如果返回结果是None说明它不是一个闭包函数。但在上面的代码里修饰器也确实能用,因此我们可以判断出,修饰器并不一定要求闭包函数,这个up讲修饰器的视频也是错误的。下面我们重新来写一个闭包函数:

def a():
    print("这是函数a")
    text = 1
    def b():
        print("这是闭包函数b")
        print(text)
    print(b.__closure__)
    return b  # 返回传入的函数对象

varA = a()  # varA存储的是函数a返回的函数对象b
varA()      # 运行函数b

这里text变量的作用仅仅是为了让闭包函数b使用函数a的变量。如果没有这个text变量,或者函数b不去使用这个text变量,那么函数b就不再是闭包函数了,大家有兴趣的话可以自己把print(text)删掉,看看print(b.__closure__)显示的是什么值。

我们知道,修饰器需要的是一个函数的传入参数和返回值都是一个函数对象,而闭包函数急其外面的函数并不要求传入参数和返回值都是一个函数对象。因此,修饰器与闭包函数并没有什么联系,网上的那些教程也只是在用闭包函数举例,在我看来作为教程来讲解修饰器着实复杂了一些。不过在实际应用场景下,可能会遇到闭包函数与修饰器同时使用的场景。

想看更多?来我博客的开发区看看吧!https://samweiqi.wang/index.php/%e7%bc%96%e7%a8%8b/

留下评论