Python精进-装饰器与函数对象

0.12字数 6316阅读 1239

本文为《爬着学Python》系列第四篇文章。
从本篇开始,本专栏在顺序更新的基础上,会有不规则的更新。


在Python的学习与运用中,我们迟早会遇到装饰器,这个概念对于初识装饰器的新手来说可能理解起来会有一些障碍。但究其原因,新手之所以觉得装饰器理解起来有困难,是因为对Python对象、函数对象的理解不够深刻。希望本文能够帮助有困惑的新手通过巩固对Python对象的认识来轻松地理解装饰器。

废话少说,开始正文吧。


装饰器是什么

Python的装饰器一般来说是以这样的形式出现的:

@decorator
def func(*args):
    print('func_text')
    return 0

我们注意到在定义函数时,我们先以@声明了一个装饰名。这个形式作为Python的语法糖,对于熟悉装饰器的人来说非常便利,但不利于初学者理解。好在,Python语言中的装饰器还有既易于理解也不繁琐冗长的形式:

def func(*args):
    print('func_text')
    return 0
func = decorator(func)

上面这个代码非常好理解,在我们定义完一个函数以后,我们通过另一个函数调用它,并且把这次调用的结果赋值给这个函数。
什么情况下我们会需要这么做呢?也就是说,我们需要decorator这个函数做什么事情呢?这个decorator函数一般长下面这个样子:

def decorator(func):
    def inner():
        print('something before func')
        func()
        print('something after func')
    return inner

从代码中可以看到,我们在装饰函数中定义了一个内部函数,在这个内部函数中调用作为参数传进来的待修饰函数,并且做了一些微小的工作。我们在不影响func函数结构的前提下对它的功能进行了修改,使它在完成自身功能的同时也能适应我们给他的变化。
在使用装饰器之前,我们调用func输出结果是这样的:

>>> func()
'func_text'

在装饰器的作用下,我们再调用func输出:

>>> func = decorator(func)
>>> func()
'something before func'
'func_text'
'something after func'

很显然,我们没有改func函数内部的代码,我们没有改变它本该做到的事情,但是我们让它可以做一些额外的事情。这是我们看到现在func = decorator(func)装饰方法的逻辑。在我们调试某个函数,或者需要记录时间戳或加入工作日志时,这些就可以靠装饰器做到,并且可以进行批量操作。

但是,装饰器不止这么简单而已,它能实现的功能要丰富得多。但在进一步研究装饰器的功能之前,我觉得我们有必要加深对Python函数的认识。那么不妨再回头看看,为什么我们可以进行刚才的这些操作。这里所说的操作,包括:给函数赋值,函数作为值赋给另一个函数,函数作为参数在另一个函数中调用。

在以上三个操作中,都是以"函数"作为主体,但事实上,这三个"函数"并不都是同一种东西。在Python中说到"函数",有些时候,我们是指函数对象,而有些时候,我们指的是函数变量。这两个概念到底有什么区别呢?

Python中的对象

>>> a = 3

在我们对整数变量赋值时,a是整型变量,3是一个整数对象。在Python中,从内存中新建对象3,我们把变量指向这个对象,就完成了变量的赋值。这也是为什么Python中不需要声明变量类型,因为它本来就没有类型,它更类似于名称或者标记。变量指向什么类型的对象,我们可以看作(事实上无类型)这个变量就是这个类型的变量,可以依此把他叫做整型变量或者浮点型变量等。

虽然实质上变量本身依然只是个标记而已,但是我们使用这个变量的时候,目的不是使用它的名称,目的在于使用它的对象。我们需要变量来省去直接使用对象的不方便的地方。
就好比说,如果没有人民代表,开不成人民代表大会。但是我们不在乎人大代表是谁,我们只在乎他是谁的人大代表,他代表那些人民有什么样的诉求 :)

在给a赋值以后,我们就完成了变量a的声明,同时这个变量指向了一个对象3,同时我们得到了一个叫做"a"的对象。从此开始a既是变量又是对象。那么怎么区分呢?目前可先简单理解为,当a出现在赋值语句=左边时它是变量,在右边它就是对象。那么为什么Python要这么设计呢?因为这是动态语言所具有的特征。

这样做的好处在于,我们对变量的操作更加灵活。Python的一句赋值语句就涵盖了大量的操作与信息,这也是Python能比其他语言简短但运行速度不足的典型(当然也有很多情况下运行反而更便捷,以后会深入[1])。

>>> b = a

在这样的理解作为前提下,像这样的对另一个变量赋值操作,逻辑就变成了:b指向a指向的对象。换句话说,b3a3是同一个3。这和"a的值是3,b和a的值相等,b的值也等于3"是不一样的逻辑。看起来差不多,或者说理解起来有难度是因为,3作为一个整数它是不可变对象,当a指向一个可变对象时就好理解了。这个在下一部分函数对象中会进一步进行说明,我们会理解的。

在此我们可以先再举一个喜闻乐见的具有Python特色的例子(Python与爬虫的简单介绍):

>>> a = 3
>>> b = 4
>>> a, b = b, a

我们对于变量换值的理解为"a的值给b,b的值给a",计算机执行时只能一句一句执行,所以往往需要中间变量来缓存先被换掉的值。我们的人脑能完成"a的值给b,b的值给a"操作,那是因为我们记得a原来是多少,b原来是多少,简单的操作不至于让我们人脑忘记原来的值。但是电脑比较程序化,它只能记住a目前的值是多少,没人告诉它就算把别的值给a,也别忘了a原来的值。Python支持多变量同时赋值,且赋值完成前,变量对应的对象指向的对象不会变,赋值完成后变量的。在a, b = b, a这个语句的执行逻辑是这样的:

-要对ab进行操作,如果没有那就要新建变量叫作a或者b,有那就太好了
-这个操作是赋值,需要两个对象
-这两个对象分别是b指向的对象4a指向的对象3
-将ab分别指向对象4和对象3
(以上是为了方便理解Python对象而作的解释,有关可迭代对象的赋值操作实现细节不在本文讨论范围内)

这样的逻辑优点在于,ab甚至可以是不一样的数据类型(再次强调Python变量其实没有数据类型),可以是字符串和列表进行对换(暂时想不到在什么场景有必要)。我们甚至可以对等式右边的对象进行一些简单的操作,比如a, b = b, a + b也是可以的,某些时候a, b = b, a(b)也是可以的,某些时候a, b = b, a()也是可以的……

>>> def a(n=None):
...     print(n)
...
>>> b = 'heart'
>>> a, b = b, a()
None
>>> print(b)
None
>>>

所以有时候不要懊恼为什么痴情会什么都换不来,生活要比Python更灵活,但我可以确定是你换的对象不对 :)

在以上的例子中,我们可以看到如何Python用灵巧的变量声明方式来节省内存(很久以后会出一篇讨论Python的内存管理机制[2])的同时简化流程。我们在引用对象时,真正引用的是对象指向的内存单元,可以说是对象的对象。我们可以直接理解成我们在把变量当作对象引用,但我们要搞清楚背后的原理。

总结起来,

  • 在Python中变量是内存单元的一种标识,我们可以把这个变量当作概念对象来引用,实际上我们在操作内存单元这个实际对象
  • Python的一切皆为对象指的是概念对象。这叫做变量的引用语义

那么现在先留个问题:Python是如何用这样的对象定义方法处理内置的简单数据结构如list、set、tuple的呢?它们自己是对象,但是他们还有元素啊,那些元素怎么处理呢,列表如何最终指向内存单元的一个个值呢?

这个问题先存疑,在本文靠后的部分会给出一点我的见解。

以上算是对Python中对象的一种肤浅的解释。这是为了我们理解接下来的函数对象做个铺垫。

函数对象

现在回到我们之前的问题,为什么在Python中说到函数,有些时候,我们是指函数对象,而有些时候,我们指的是函数变量?

之所以会有这种现象,是因为函数对象并不是所有语言中都默认或者说常见的,比如在C/C++中,我们需要通过函数指针或类或结构体来完成类似函数对象功能(能力各有不同)。C语言的灵魂在于指针,Python的灵魂在于一切皆为对象。

如果我们把Python中的函数也当作刚才的ab就好理解了,函数的定义和变量赋值其实是差不多的操作,都是在进行变量的初始化。所以你进行

>>> def a():
...     return 3
...
>>> b = 3

这两个操作可以看作事实上是同一种操作,只不过赋值的对象类型不同,所以我们把他们看作不同的变量或对象。我们把a称作函数,把b称作整数。

所以,我们可以进行这样的操作:

>>> c = a
>>> d = a()

这两行语句执行的结果是,c指向了return 3这个函数d指向了这个函数的返回值整数3。我们知道了函数被调用的情况下所指向的对象就是它的返回值(如果没有,就会返回None这个对象,就是上面我们所举过的例子)。

在这基础之上,我们可以有装饰器这样的操作:

def decorator(func):
    def inner():
        print('something before func')
        func()
        print('something after func')
    return inner

def func(*args):
    print('func_text')
    return 0

func = decorator(func)

decorator函数的内部函数inner中我们用到了decorator的参数并且调用它,只要这个对象可调用(callable),程序就不会报错(比如具有__call__方法的类也能适用于这个修饰器)。如果能理解为什么最后赋值是func = decorator(func)而不是func = decorator(func())就可以说大致理解了函数对象这个概念了,那么理解装饰器工作原理也就不难了。

装饰器怎么用

其实构造装饰器的方法在本文的开头就已经介绍过了。在这里我们就只解释一下@decorator方法。

Python灵活的对象操作给我们构造装饰器带来了极大的便利,但是还是有人不满足,这个方式还是太臃肿。于是Python提供了修饰器语法糖:

def decorator(func):
    def inner():
        print('something before func')
        func()
        print('something after func')
    return inner

@decorator
def func(*args):
    print('func_text')
    return 0

可能你会觉得把func = decorator(func)改成@decorator也不算什么简化。甚至在我们需要批量修饰一些函数的时候,前者其实用起来倒比后者还要方便。

之所以会出现这样的情况,是因为装饰器的应用场景不局限于此。在给待修饰函数修补功能时,这些功能很小时我们叫他修补,但是这些功能反而要比待修饰函数本身更重要时,修饰器就充当了类似模板的角色。

试想一下这样的场景。在我们使用第三方开发的框架时,或者我们需要反复在不同函数中调用某个函数实现功能时,我们可以给这些函数加一个变量,然后再把要调用的函数当作参数传进来并且在函数体内调用。这听上去就很麻烦,所以我们需要装饰器简化这样的操作。

我们一般是先想好了某个函数需要完成什么功能,要怎样实现这样的功能,然后我们开始定义这个函数。因此,当我们需要模板来帮助我们创建这个函数时,我们就可以直接写出我们需要什么模板

@decorator
def func(*args):
    pass

这是符合程序设计逻辑的。如果我们先定义函数,再利用另一个函数来调用这个函数,可能并不如@decorator方法思路那么流畅。

因此在程序设计过程中@decorator方法往往更实用,它面向设计,而func = decorator(func)可能形式上更面向修改。所以前者更适用于开发,后者更适用于运维。也就是说,程序员对一个顺序设计结构的修饰器有需求,@decorator方法也就应运而生了。

这也是为什么我们见到@decorator修饰器绝大部分场景是在应用第三方库或者框架进行快速开发。

到目前为止我们了解了装饰器的实现原理,装饰器主要的应用场景,装饰器大致的应用思路。但是这远远达不到实用的程度。在进一步学习装饰器的使用方法前,我们再需要强化一下对函数的理解。我们目前对函数的理解到位了吗?试一试就知道了:

def decorator(func):
    def inner():
        func()
        return 1
    return inner

def func(*args):
    return 2

func = decorator(func)

print(func())

如果能给出以上代码最终的输出结果,那么你对Python函数这个概念的理解基本上合格了。

正确答案,输出:1

想不清楚也没关系,我们再来像分析a, b = b, a这个语句的执行一样简单分析一下这段代码的执行逻辑:

-定义了decorator,指向定义内容,返回一个对象inner
-在decorator的内部定义函数inner,这个函数被调用时会调用decorator的参数指向的函数,最后返回一个整数对象1
-定义了func,指向定义内容,返回一个整数对象2
-对func进行赋值,要找到将decorator(func)所指向的对象
-decorator(func)指向以func为参数调用decorator后返回的对象inner
-调用func函数,指向一个保存了一个函数对象作为参数的inner函数内容,需要它的返回对象
-在这些函数内容运行过程中,某个函数对象返回了整数对象2,但是没关系inner还没返回对象
-inner返回了整数对象1
-将整数对象1作为参数调用内置print函数
-输出1

这里面有个值得深究的地方:如果我们直接调用inner函数(没有外部定义过),那么会触发未定义错误,但是经过func = decorator(func)赋值定义以后,我们知道它指向一个带参数的inner,这次它就可以直接调用了。这是为什么呢?

不知道你还记不记得,本文靠前部分我们曾经留了个问题没解决:Python是如何用对象定义方法处理内置的简单数据结构的?现在我们来处理一下。

我们可以简单理解为赋值语句有"路由功能",但更推荐理解为:在定义列表时,我们真正创建的对象是列表元素,这些元素各自指向一个内存单元,然后我们通过创建列表对象来创建一个元素索引的集合,最后我们把变量指向这个列表对象。这种处理方式就像是把列表当作一个函数,把索引当作参数,返回索引对应元素指向的对象。

所以我们要对之前的一个结论进行修正。

>>>def a():
...   return 3
...
>>>b = 3

我们之前说对函数定义就类似对变量赋值,其实,对函数定义,更像是对列表赋值。

>>>def a():
...   return 3
...
>>>b = [3]

这之间的区别只能靠各位去领会了。你也可以先往下看再回来体会。

回到刚才的问题。为什么不能直接调用inner,但是赋值以后就可以用新名字直接调用了?

我们可以理解为,要使用列表中的元素,我们必须要用列表名[索引]的方式,我们没办法在不提及列表名的情况下使用这个元素。除非,我们用一个变量指向这个元素变量 = 列表名[索引],这样我们就有了一个对象经过这个元素的介绍,找到指向这个元素指向的内存单元地址。我们可以通过直接访问这个变量来访问该地址。

经过分析后我们可能还可以进一步修正我们的结论,对函数定义,其实更像是对元组赋值。

学习Python真有意思:)

装饰器的基本用法

在上一节中我们所讨论的"装饰器怎么用",更倾向于"要装饰器做什么",现在我么来讨论装饰器的具体操作方法。

为什么说上一节的"装饰器怎么用"不纯粹呢。我们已经分析过,装饰器实质上是函数,那么,我们实际使用函数场景中遇到的很多操作还没有提及。所以,这一节我们主要讲带参数的装饰器,并且在此之前先简单说一下多层装饰器调用顺序。

多个装饰器的调用很容易看懂,按照形式上的嵌套理解就可以了。

@redecorator
@decorator
def func(*args):
    pass

等价于

func = redecorator(decorator(func))

多层装饰实际上应用不是特别多,理解起来也比较简单,在此不展开讨论了。接下来我们讨论带参数的装饰器

我们先直接给出需要参数时,装饰器的一般形式:

def foo(a):
    def decorator(func):
        def inner(*args):
            print(a)
            return func(*args)
        return inner
    return decorator

@foo('hello')
def bar(c):
    print(c)
    return 0

bar('world')

在这个形式中,@foo(a)相当于bar = foo(a)(bar),也就是说,我们要明确一点,在定义foo函数时,foo需要的参数是依次满足的。这么说可能比较抽象。

@foo
def bar(c):
    print(c)
    return 0

如果我们在定义foo装饰器函数时声明了参数a,那么如果我们使用它装饰bar时,不明确a的对象。整个bar函数会作为foo的参数a。正如之前简单装饰器那样,@foo相当于bar = foo(bar),而如果这样,显然和我们的装饰器功能不适配

要想让有参数的装饰器函数能够装饰没有额外参数的被装饰函数,只要对参数进行判别就可以了,无论是isinstance判别是否是函数(其实不可以),还是直接判断有没有__call__方法都是可以的。原因在于两者区别就在于多了一重嵌套,拆掉嵌套就可以正常运行了(内部用到参数的地方也要修改)。例如:

def foo(a):
    def decorator(func):
        def wrap(*args):
            if not callable(a):
                print(a)
            return func(*args)
        return wrap
    if callable(a):
        return decorator(a)
    return decorator

@foo
def bar(c):
    print(c)
    return 0

bar('world')

除了这些需要说明,其它倒也没有太多可说的,因为说白了就是嵌套函数传递参数。嵌套函数怎么闭包怎么传参,装饰器也可以这么做。因此我们学习的重点就不在于装饰器了,而在于函数的运用。关于函数的运用,这个问题又太宏大了一点,我只能说需要靠平时的学习一点点去熟悉体会。没有哪个语言会不需要函数来完成工作。函数运用水平和程序设计能力水平息息相关,任重道远。

是的,我特地开了一节来说装饰器的真正使用方法,却又看似想糊弄过去:)

但是我的本意是,各位要始终记得一点,装饰器就是函数嵌套,加上函数赋值。我们要理解装饰器,只需要理解Python函数对象,之后把它当函数用就可以了。学习需要灵活和变通,需要保持清醒。

装饰器的其他用法

这里装饰器的其他用法主要指装饰器在类上的使用。主要包括装饰和装饰类中的方法。在前面的学习过程中,我们提到过

def decorator(func):
    def inner():
        func()
        return 1
    return inner

这里面的func对象不一定是函数,只要它具有__call__方法,就可以用装饰器。这也是我们可以用它来装饰类和类方法的原因。关于类相关的知识,可能要过很久介绍面向对象编程时才会提及了。
这里我也不展开讲了,给学有余力的各位两个链接参考Python——编写类装饰器 - Gavin - CSDN博客Python 装饰器装饰类中的方法 - hesi9555的博客 - CSDN博客

是的,我又糊弄过去了:)

写在最后

本文是我在进行的系列教程的第一篇长文,第一篇真正讲解Python技术的文章。内容虽然不多但讲得还是比较仔细吧。前前后后写了大概四五个小时。

说到底,本文的目的在于学习如何去认识Python装饰器和函数,因此在实用部分我偷懒了(就是这么直接)。只有认识够深刻,我们才真正理解如何去用这些东西,为什么这样去用。但是古话说"纸上得来终觉浅",编程也是这样,实践环节是容不得偷懒的。而我不愿意讲解实战,是因为涉及到函数运用、类的设计这样的知识的时候,和本文的关系就不大了,那些都是足够单独拿出来研究值得花几年去训练的东西,想在一篇文章的一部分里面再怎么讲解都会显得浅薄无力。弄懂装饰器工作原理,结合自身设计函数设计类的能力,自己尝试去使用装饰器实现功能,这是我无法替你做的。

再说,毕竟我不是在写书、不是在写论文,它说到底还是交流性质啊!

希望能一起学习,一起进步。

链接

  1. Python 装饰器为什么难理解? - 知乎专栏
  2. 如何正确理解Python函数是第一类对象 - FooFish
  3. Python——编写类装饰器 - Gavin - CSDN博客
  4. Python 装饰器装饰类中的方法 - hesi9555的博客 - CSDN博客
  5. 最后留个彩蛋吧Stop Writing Classes - YouTube

TODO


  1. Python变量运算特点

  2. Python内存管理

推荐阅读更多精彩内容