本文为《爬着学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能比其他语言简短但运行速度不足的典型(当然也有很多情况下运行反而更便捷,以后会深入

>>> b = a

在这样的理解作为前提下,像这样的对另一个变量赋值操作,逻辑就变成了:b指向a指向的对象。换句话说,b的3和a的3是同一个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这个语句的执行逻辑是这样的:

-要对a和b进行操作,如果没有那就要新建变量叫作a或者b,有那就太好了

-这个操作是赋值,需要两个对象

-这两个对象分别是b指向的对象4和a指向的对象3

-将a和b分别指向对象4和对象3

(以上是为了方便理解Python对象而作的解释,有关可迭代对象的赋值操作实现细节不在本文讨论范围内)

这样的逻辑优点在于,a和b甚至可以是不一样的数据类型(再次强调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的内存管理机制对象指向的内存单元,可以说是对象的对象。我们可以直接理解成我们在把变量当作对象引用,但我们要搞清楚背后的原理。

总结起来,

在Python中变量是内存单元的一种标识,我们可以把这个变量当作概念对象来引用,实际上我们在操作内存单元这个实际对象

Python的一切皆为对象指的是概念对象。这叫做变量的引用语义。

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

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

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

函数对象

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

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

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

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

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

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

链接

TODO

Python变量运算特点 ↩

Python内存管理 ↩

python装饰器函数-Python精进-装饰器与函数对象相关推荐

  1. python函数装饰函数_Python精进-装饰器与函数对象

    本文为<爬着学Python>系列第四篇文章. 从本篇开始,本专栏在顺序更新的基础上,会有不规则的更新. 在Python的学习与运用中,我们迟早会遇到装饰器,这个概念对于初识装饰器的新手来说 ...

  2. python函数基础和装饰器

    一.为什么要有函数?没有函数有什么问题? 1.组织结构不清晰,可读性差 2.代码冗余 3.可扩展性差 二.函数的分类: 1.内置函数:python解释器已经为我们定义好了的函数即内置函数,我们可以拿来 ...

  3. python高阶函数闭包装饰器_Python_基础_(装饰器,*args,**kwargs,高阶函数,函数闭包,函数嵌套)...

    一,装饰器 装饰器:本质就是函数,功能是为其它的函数动态添加附加的功能 原则:对修改关闭对扩展开放 1.不修改被修饰函数的源代码 2.不修改被修改函数的调用方式 装饰器实现的知识储备:高阶函数,函数嵌 ...

  4. python学习笔记(装饰器、迭代器生成器、内置函数、软件目录开发规范)

    装饰器 定义:本质是函数,(功能:装饰其他函数):就是为其他函数添加附加功能 模拟场景一,在现有的函数中增加某个功能.现有的做法是定义新函数,并且加入函数中.需要修改源代码. def logger() ...

  5. python装饰器函数-python之路——装饰器函数

    阅读目录 楔子 作为一个会写函数的python开发,我们从今天开始要去公司上班了.写了一个函数,就交给其他开发用了. deffunc1():print('in func1') 季度末,公司的领导要给大 ...

  6. python装饰器实例-Python函数装饰器--实例讲解

    一.装饰器定义: 1.装饰器的本质为函数: 2.装饰器是用来完成被修饰函数的附加功能的 所以:装饰器是用来完成被修饰函数附属功能的函数 装饰器的要求: 1.不能修改被修饰函数的源代码: 2.不能更改被 ...

  7. python装饰器函数-python函数装饰器

    什么是装饰器 装饰器是一个可调用的对象,其参数是另一个函数(被装饰的函数).装饰器可能会: 1,处理被装饰的函数,然后把它返回 2,将其替换成另一个函数或者对象 若有个名为decorate的装饰器,则 ...

  8. python装饰器函数-python装饰器1:函数装饰器详解

    先混个眼熟 谁可以作为装饰器(可以将谁编写成装饰器): 函数 方法 实现了__call__的可调用类 装饰器可以去装饰谁(谁可以被装饰): 函数 方法 类 基础:函数装饰器的表现方式 假如你已经定义了 ...

  9. python装饰器函数-Python函数装饰器常见使用方法实例详解

    本文实例讲述了Python函数装饰器常见使用方法.分享给大家供大家参考,具体如下: 一.装饰器 首先,我们要了解到什么是开放封闭式原则? 软件一旦上线后,对修改源代码是封闭的,对功能的扩张是开放的,所 ...

最新文章

  1. 一份很不错的敏捷产品接口文档模板
  2. mysql实现组队_TiDB Hackathon 参考选题扩充,组队参赛走起!
  3. php内核分析(六)-opcode
  4. 谈一谈自己对依赖、关联、聚合和组合之间区别的理解
  5. asp页面实现301重定向方法
  6. s7-200通信测试软件,S7-200 SMART 与调试助手之间 TCP 通信[技术学习]
  7. 数据结构之队列(循环队列)
  8. cad卸载工具_装不上也卸不掉,我的CAD仿佛已没救!...(CAD/MAX完美安装工具)...
  9. 千万别用MongoDB?
  10. 高等数学基础知识点 导数与微分 思维导图
  11. Keepalived配置报错Unicast peers are not supported in strict mode
  12. 纹波测试方法(收集整理)
  13. table表格表头单元格添加斜线
  14. 迭代模型(Iterative Model)
  15. mysql获取年月日周季度
  16. 数据库设计之商品表分析1
  17. 每个人表面上都想改变自己,但内心却都抗拒改变,这仅仅是人性的懒惰和矛盾?
  18. 三维地下管线系统(CS)视频
  19. win11怎么共享文件夹 Windows11共享文件夹的设置方法
  20. VMProtect1.63分析

热门文章

  1. ES6 -Set 和 Map 数据结构
  2. 【笔记篇】C#笔记1
  3. Snapchat - give sum target listInteger first who hits target wins
  4. 去除inline-block元素间间距,比较靠谱的两种办法
  5. ScriptManager.RegisterStartupScript方法和Page.ClientScript.RegisterStartupScript() 区别
  6. iOS基础网络教程-Swift版本: 1.基础网络概括
  7. 聊聊JS与设计模式之(工厂Factory)篇------(麦当劳的故事)
  8. hdu 携程全球数据中心建设 (球面距离 + 最小生成树)
  9. [ARM-Linux]Linux-MATLAB安装
  10. java类载入器——ClassLoader