人人都能看懂的 Python 装饰器入门教程!

很多人认为理解了装饰器的概念和用法后,会觉得自己的 Python 水平有一个明显的提高。

但很多教程在一上来就会给出装饰器的定义以及基本用法,例如你一定会在很多文章中看到例如代码运行时间计时器等相关常用装饰器。

直接从应用入手这样学习当然十分有效,但不是看过就忘就是似懂非懂的状态,因为装饰器从来就不是一个单独的概念,就像数学分析中求积分一样,你可以通过公式快速算出需要求的积分,但是若明白积分是由极限定义的,之后再看积分将会是不一样的视角。

本文我将尝试说清楚为什么需要现装饰器、什么是装饰器、以及如何写一个简单的装饰器,但要彻底理解装饰器还要从函数开始说起,下面是有关函数的四个重要的概念,希望大家可以明白。

01

有关函数的四个重要概念

相信你在大多数文章中,至少也能知道例如「装饰器是装饰函数」,「在不修改函数代码的情况下增加额外功能」等核心概念,但首先要知道为什么函数能够被装饰。

例如在《流畅的Python》一书中,讲到函数的一开始就提出了一个概念,函数是一等对象

正如书中所说,在Python中一个函数既可以作为参数被传递,也能作为另一个函数的返回值,这也是函数可以被装饰的关键,在介绍装饰器之前,下面有必要通过简单的代码对这段话做一个更直观的理解。

1.1 函数中传递函数

函数中传递函数意思就是可以将函数当作变量来使用,我们来看一个简单的示例。

在下面的代码中,func1是一个普通的函数,接受两个参数a,b并返回他们的和。func2不一样的地方在于多接收了一个func参数,这个func变量需要是一个函数

def func1(a,b):
    
    print(f'函数 {func1.__name__} 正在执行')
    
    return a + b
    
def func2(func,c,d):
    
    print(f'函数 {func2.__name__} 正在执行')
    
    return func(c,d)

现在让我们来执行func1

>>> func1(1,2)函数 func1 正在执行3

下面func1作为参数执行func2

>>> func2(func1,3,4)
函数 func2 正在执行
函数 func1 正在执行
7

可以看到,先执行func2,在func2接收到fun1后,再次执行func1并返回。注意这里的func1没有括号,它只不过是和a,b一样的参数被使用,理解这点后我们继续看下一个知识点。

1.2 函数中定义函数

在定义一个函数后,可以继续在函数内部定义新的函数。为了理解这点,我们来看下面简单的示例。

我们先定义了一个函数func1,并在func1中定义了func2,并在func1的内部调用了func2

def func1():

    print(f'函数 {func1.__name__} 正在执行')

    def func2():

        print(f'内部函数 {func2.__name__} 正在执行')

    func2()

现在执行func1func2看看会发生什么

>>> func1()
函数 func1 正在执行
内部函数 func2 正在执行

>>> func2()
------------------------------------------------
Traceback (most recent call last)

----> 1 func2()

NameError: name 'func2' is not defined

可以看到,当执行func1时,会自动执行func2,但是如果单独执行func2,则提示未定义,说明func2只能在func1中被调用!

1.3 函数返回函数

最后是一个函数可以将另一个函数作为返回值返回的简单示例,在下面的代码中,我们先定义了一个外部函数func1(接受一个参数a),之后定义了一个内部函数func2(接受一个参数b)并返回a + b,最后将func2作为func1的返回值返回

def func1(a):

    print(f'函数 {func1.__name__} 正在执行')

    def func2(b):

        print(f'函数 {func2.__name__} 正在执行')

        return a + b

    return func2

需要注意的是,这里返回的func2没有括号,代表返回的是func2的地址!

>>> func3 = func1(1)
>>> func3
函数 func1 正在执行
<function __main__.func1.<locals>.func2(b)>

>>> func3(2)
函数 func2 正在执行
3

从上面的运行结果可以看到,当执行func1(1)后,返回的是func2的地址,并赋给func3,之后执行func3(2)才真正执行了内部函数func2

现在我们就解决了上一小节的问题「将内部函数func2单独拿出来用」!

1.4 函数内省

函数内省是相对来说比较好理解的一个概念,在Python中的意思就是我们可以访问函数的部分属性,例如print函数,可以使用dir函数来查看其全部属性

>>> dir(print)['__call__', '__class__', '__delattr__',

  ··· ···

 '__subclasshook__', '__text_signature__']

现在可以查看其对应的属性

>>> print.__name__
'print'
>>> print.__call__
<method-wrapper '__call__' of builtin_function_or_method object at 0x7fddb8056b80>
>>> print.__doc__
'print(value, ..., sep=' ', end='\\n', file=sys.stdout, flush=False)\n\nPrints the values to a stream, or to sys.stdout by default.\nOptional keyword arguments:\nfile:  a file-like object (stream); defaults to the current sys.stdout.\nsep:   string inserted between values, default a space.\nend:   string appended after the last value, default a newline.\nflush: whether to forcibly flush the stream.'

函数内省了解到这个程度即可,我们会在2.3节再次提到这里的知识!

至此,我已经将接触装饰器之前必须要吃透的知识点介绍完毕,如果你觉得我讲解的不够清晰,可以查看任何其他教程或者书籍弄懂后再继续阅读。

02

初识装饰器

现在终于可以来说说装饰器了,当然绝对不是直接告诉你一个写好的装饰器,而是我们一点一点去写一个简单的装饰器。

2.1 第一个装饰器

在下面的代码中,我们先定义了一个函数first_decorator,该函数接受函数为参数(如果不理解请查看本文 1.1 节),之后在内部定义了一个名为name_wrapper的内部函数(如果不理解请查看本文 1.2 节),最后返回以name_wrapper作为返回值(如果不理解请查看本文 1.3 节)

def first_decorator(func):    def name_wrapper():

        print(f'被装饰的函数 {func.__name__} 即将执行')        func()        print(f'被装饰的函数 {func.__name__} 执行完毕')

    return name_wrapper

这个函数的功能是,在执行被接收函数前后分别打印一段话,所以我们要再定义一个函数来测试效果

def add():
    
    print('函数 add 正在执行 ')

这个fun1没有什么好说的,打印一段话。下面需要仔细看了,我们来执行这两段代码

>>> add = first_decorator(add)>>> add()被装饰的函数 add 即将执行函数 add 正在执行 被装饰的函数 add 执行完毕

正如我们预料的一样,在执行add前后都有一段提示,但是如果每次使用first_decorator功能都需要先将add传递,之后再调用,来回写好几遍,实在太麻烦了!

因此这完全有更Pythonic的写法,也就是我们常见的装饰器形式,使用语法糖@,例如上面的例子和下面的写法等价

@first_decorator
def add():

print('函数 add 正在执行 ')

@+装饰器函数名字放在需要被装饰函数的上方即可,现在直接调用add即可实现装饰器的功能!

>>> add()被装饰的函数 add 即将执行函数 add 正在执行 被装饰的函数 add 执行完毕

相信看到这里,你应该明白装饰器@如何工作的,至少你在未来看到@时需要想到类似等价于add = first_decorator(add)一样的作用!

2.2 装饰器传参

上面仅是最简单的装饰器示例,在实际使用时

很自然的想法那就是加上参数呗,改起来也不难

@first_decorator
def add(x,y):
    
    
    print('函数 add 正在执行 ')
    print(f'{x} + {y} 的结果为{x+y}')

让我们来测试一下

>>> add(1,2)-----------------------------------------------Traceback (most recent call last)<ipython-input-144-944f4051a32c> in <module>----> 1 add(1,2)

TypeError: name_wrapper() takes 0 positional arguments but 2 were given

不出意外的报错了,虽然我们给被装饰的函数加上了参数,但是在装饰器的内部函数name_wrapper()执行时并没有参数!

因此我们之前的代码可以这么改,使用*args, **kwargs也是非常常见的用法

def first_decorator(func):
    def name_wrapper(*args, **kwargs):
        
        print(f'被装饰的函数 {func.__name__} 即将执行')
        func(*args, **kwargs)
        print(f'被装饰的函数 {func.__name__} 执行完毕')
        
    return name_wrapper

现在我们再次使用这个装饰器即可返回我们预测的结果!

@first_decoratordef add(x,y):

    print('函数 add 正在执行 ')    print(f'{x} + {y} 的结果为 {x+y}')

>>> add(1,2)被装饰的函数 add 即将执行函数 add 正在执行 1 + 2 的结果为 3被装饰的函数 add 执行完毕

03

functools

本文的最后,还需要简单介绍一下在写装饰器时常用的functools模块。

还记得1.4节的函数内省相关知识吗?我们可以打印一个函数指向的内存地址或者名字等其他属性。

还是上面用到的add函数,我们都知道虽然被装饰了,但是功能上没有任何变化,依旧是计算两个数字的和,但是真的没有任何变化吗?下面让我们来观察一下

def add(x,y):
    
    print('函数 add 正在执行 ')
    print(f'{x} + {y} 的结果为{x+y}')
    
>>> print(add)
<function add at 0x7fddb9dd41f0>
>>>print(add.__name__)
add

@first_decorator
def add(x,y):

print('函数 add 正在执行 ')
    print(f'{x} + {y} 的结果为{x+y}')
    
>>> print(add)
<function first_decorator.<locals>.name_wrapper at 0x7fddb9dd4e50>
>>> print(add.__name__)
name_wrapper

可以看到,被装饰后,虽然功能上没有变化,但是它指向的是装饰器所定义的内部函数!这并不是我们希望看到的,比如若不同函数被两个装饰器装饰时则会出现一样的函数名!

幸运的是Python中的functools库可以轻松解决这个问题,只需要加上一行简单的代码就可以搞定!

import functools

def first_decorator(func):    @functools.wraps(func)    def name_wrapper(*args, **kwargs):

        print(f'被装饰的函数 {func.__name__} 即将执行')        func(*args, **kwargs)        print(f'被装饰的函数 {func.__name__} 执行完毕')

    return name_wrapper

@first_decoratordef add(x,y):

    print('函数 add 正在执行 ')    print(f'{x} + {y} 的结果为{x+y}')

>>> print(add)<function add at 0x7fddb9dd4e50>>>> print(add.__name__)add

可以看到此时函数名等函数属性均保留下来了,事实上@functools.wraps(func)通过functools.update_wrapper()将原函数中的部分内省属性固定,只传递部分关键参数来实现这个功能,感兴趣的读者可以自行进一步研究。

至此,我想你应该明白为什么需要现装饰器、什么是装饰器、以及如何写一个简单的装饰器,当你再次看到装饰器时,脑海中浮现的概念应该不仅仅是@

- EOF -

(0)

相关推荐

  • 【编程课堂】装饰器浅析

    Python 拥有丰富强大的功能和表达特性,其中之一便是装饰器,装饰器能够在不改变函数.方法.类本身的情况下丰富他们的功能. 比如,我们有一个函数 func ,我们希望在不改变函数的前提下记录函数运行 ...

  • 一文看懂Python系列之装饰器(decorator)(工作面试必读)

    Python的装饰器(decorator)可以说是Python的一个神器,它可以在不改变一个函数代码和调用方式的情况下给函数添加新的功能.Python的装饰器同时也是Python学习从入门到精通过程中 ...

  • P百科:如何理解 Python 中的装饰器

    昨天 [P百科]:Python 大白话百科全书   每周,痴海会用大白话,讲解一个 Python 中那些难以理解的知识点.   希望通过这样的白话讲解,可以让大家更好的学会 Python.   今天这 ...

  • Python学习—装饰器

    学习Python已经有一段时间了,陆续学了一些基础部分,但是理解的不是很深刻,每过一段时间就会忘记,所以不得不写一些博客进行记录,加深自己的理解.这两个星期一直在研究装饰器,开始觉得很简单,但是只知其 ...

  • python笔记36-装饰器之wraps

    前言 前面一篇对python装饰器有了初步的了解了,但是还不够完美,领导看了后又提出了新的需求,希望运行的日志能显示出具体运行的哪个函数. name和doc __name__用于获取函数的名称,__d ...

  • python笔记35-装饰器

    前言 python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象. 很多python初学者学到面向对象类和方法是一道大坎,那么p ...

  • 一文搞懂Python装饰器

    一.前言 本不打算专门写文来讲装饰器的,但有不少粉丝问到了,自己查阅了一些网上的装饰器教程,发现讲的通俗易懂的不多,也有不少照搬的文章.所以我这里专门来讲一讲它. 个人在用的人工智能学习网站推荐给大家 ...

  • 从听说到上手,人人都能看懂的软路由入门指南

    在互联网时代,家庭网络带宽速度飞速地提升,家庭中的智能移动设备越来越多,智能家居设备也开始逐渐地普及,于是家里的路由器需承载的负荷也越来越大.不过,普通的路由器越来越难满足现阶段的家庭需求,软路由因此 ...

  • 一张图,人人都能看懂的全国产业分布格局

    以往我们看统计数据,面对密密麻麻的数字,对于大多数非专业人士来说是不是一脸懵呀.即使有图表,往往也仅展示单项指标,无法综合展现出各地区的差异.为了能更直观地读懂统计数据,了解全国地区间产业结构与分布情 ...

  • 风水学入门,人人都能看懂

    家居室内风水是人与环境二者相融.相宜.相合.它可以对家居的外部环境和室内环境进行细致分析.观形察势. 在方位上来讲,家居风水跟我们风水学上是一致:左(青龙).右(白虎).前(朱雀).后(玄武).北(子 ...

  • 人人都能看懂的Matplotlib绘图原理讲解!

    前言 大家好,我是早起,这篇文章没有代码,只做一件事:尝试讲明白Matplotlib的工作机制,先说说我刚开始是怎样学习Matplotlib的吧(可能也是你的)

  • 一篇人人都能看懂的防疫期心理调节,身体和心理同样重要

    春节假期期间,新型冠状病毒肺炎来势汹汹,为了防护不便出门,却每天坐在家里看着电视手机媒体的实时疫情更新,容易出现焦虑.恐惧的情绪. 人们对世界认知的主观性很强,即使看到同一个现象或信息,每个人关注的重 ...

  • 让手机秒变显微镜、望远镜!人人都能看得懂系列

    原来手机 既能变成显微镜 还能成为智能望远镜? 你信吗? 小科本来是不信的华中科技大学 "原创光学成像"项目团队 正是借此斩获 "互联网+"大赛 湖北省赛高教主 ...

  • 李煜的这首千古名作,人人都能读懂,却无人能模仿

    王国维在<人间词话>中曾言"词至李后主而眼界始大,感慨遂深,遂变伶工之词而为士大夫之词". 在李煜之前,最负盛名的是"花间词",多描写男女离别相思, ...

  • 一位职业操盘手首次摆出BOV阵法,新散户都能看懂,堪称无价之宝!

    OBV指标是投资人进行继续分析时,常用的技术指标之一.OBV指标的中文名称为能量潮指标,其形成的方法就是股价和成交量之间的配比. OBV能量潮指标是由两条线组成的,第一条线为MAOBV线,一般情况下, ...