对于每一个学习 Python 的同学,想必对 @ 符号一定不陌生了,正如你所知, @ 符号是装饰器的语法糖,@符号后面的函数就是我们本文的主角:装饰器。

装饰器放在一个函数开始定义的地方,它就像一顶帽子一样戴在这个函数的头上。和这个函数绑定在一起。在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶上这顶帽子,这顶帽子我们称之为 装饰器 。

曾经我在刚转行做程序员时的一次的面试中,被面试官问过这样的两个问题:

1、你都用过装饰器实现过什么样的功能?

2、如何写一个可以传参的装饰器?

对于当时实战经验非常有限的我,第一个问题只能回答一些非常简单的用法,而第二个问题却没能回答上来。

当时带着这两个问题,我就开始系统的学习装饰器的所有内容。这些一直整理在自己的博客中,今天对其进行了大量的补充和勘误,发表在这里分享给大家。希望对刚入门以及进阶的朋友可以提供一些参考。

01. Hello,装饰器

装饰器的使用方法很固定

先定义一个装饰器(帽子)

再定义你的业务函数或者类(人)

最后把这装饰器(帽子)扣在这个函数(人)头上

就像下面这样子

def decorator(func):def wrapper(*args, **kw):return func()return wrapper@decorator
def function():print("hello, decorator")

实际上,装饰器并不是编码必须性,意思就是说,你不使用装饰器完全可以,它的出现,应该是使我们的代码

更加优雅,代码结构更加清晰

将实现特定的功能代码封装成装饰器,提高代码复用率,增强代码可读性

接下来,我将以实例讲解,如何编写出各种简单及复杂的装饰器。

02. 入门:日志打印器

首先是日志打印器。

实现的功能:

在函数执行前,先打印一行日志告知一下主人,我要执行函数了。

在函数执行完,也不能拍拍屁股就走人了,咱可是有礼貌的代码,再打印一行日志告知下主人,我执行完啦。

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
# 这是装饰器函数,参数 func 是被装饰的函数
def logger(func):def wrapper(*args, **kw):print('主人,我准备开始执行:{} 函数了:'.format(func.__name__))# 真正执行的是这行。func(*args, **kw)print('主人,我执行完啦。')return wrapper

假如,我的业务函数是,计算两个数之和。写好后,直接给它带上帽子。

@logger
def add(x, y):print('{} + {} = {}'.format(x, y, x+y))

然后执行一下 add 函数。

add(200, 50)

来看看输出了什么?

主人,我准备开始执行:add 函数了:
200 + 50 = 250
主人,我执行完啦。

03. 入门:时间计时器

再来看看 时间计时器
实现功能:顾名思义,就是计算一个函数的执行时长。

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
# 这是装饰函数
def timer(func):def wrapper(*args, **kw):t1=time.time()# 这是函数真正执行的地方func(*args, **kw)t2=time.time()# 计算下时长cost_time = t2-t1 print("花费时间:{}秒".format(cost_time))return wrapper

假如,我们的函数是要睡眠10秒。这样也能更好的看出这个计算时长到底靠不靠谱。

import time@timer
def want_sleep(sleep_time):time.sleep(sleep_time)want_sleep(10)

来看看输出,如预期一样,输出10秒。

花费时间:10.0073800086975098秒

04. 进阶:带参数的函数装饰器

通过上面两个简单的入门示例,你应该能体会到装饰器的工作原理了。

不过,装饰器的用法还远不止如此,深究下去,还大有文章。今天就一起来把这个知识点学透。

回过头去看看上面的例子,装饰器是不能接收参数的。其用法,只能适用于一些简单的场景。不传参的装饰器,只能对被装饰函数,执行固定逻辑。

装饰器本身是一个函数,做为一个函数,如果不能传参,那这个函数的功能就会很受限,只能执行固定的逻辑。这意味着,如果装饰器的逻辑代码的执行需要根据不同场景进行调整,若不能传参的话,我们就要写两个装饰器,这显然是不合理的。

比如我们要实现一个可以定时发送邮件的任务(一分钟发送一封),定时进行时间同步的任务(一天同步一次),就可以自己实现一个 periodic_task (定时任务)的装饰器,这个装饰器可以接收一个时间间隔的参数,间隔多长时间执行一次任务。

可以这样像下面这样写,由于这个功能代码比较复杂,不利于学习,这里就不贴了。

@periodic_task(spacing=60)
def send_mail():pass@periodic_task(spacing=86400)
def ntp()pass

那我们来自己创造一个伪场景,可以在装饰器里传入一个参数,指明国籍,并在函数执行前,用自己国家的母语打一个招呼。

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
# 小明,中国人
@say_hello("china")
def xiaoming():pass# jack,美国人
@say_hello("america")
def jack():pass

那我们如果实现这个装饰器,让其可以实现 传参 呢?

会比较复杂,需要两层嵌套。

def say_hello(contry):def wrapper(func):def deco(*args, **kwargs):if contry == "china":print("你好!")elif contry == "america":print('hello.')else:return# 真正执行函数的地方func(*args, **kwargs)return decoreturn wrapper

来执行一下

xiaoming()
print("------------")
jack()

看看输出结果。

你好!
------------
hello.

05. 高阶:不带参数的类装饰器

以上都是基于函数实现的装饰器,在阅读别人代码时,还可以时常发现还有基于类实现的装饰器。

基于类装饰器的实现,必须实现 __ call__ 和 __init__两个内置函数。
__ init __ :接收被装饰函数
__ call __ :实现装饰逻辑。

还是以日志打印这个简单的例子为例

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
class logger(object):def __init__(self, func):self.func = funcdef __call__(self, *args, **kwargs):print("[INFO]: the function {func}() is running...".format(func=self.func.__name__))return self.func(*args, **kwargs)@logger
def say(something):print("say {}!".format(something))say("hello")

执行一下,看看输出

[INFO]: the function say() is running...
say hello!

06. 高阶:带参数的类装饰器
上面不带参数的例子,你发现没有,只能打印INFO级别的日志,正常情况下,我们还需要打印DEBUG WARNING等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。

__ init __ :不再接收被装饰函数,而是接收传入参数。
__ call __ :接收被装饰函数,实现装饰逻辑。

class logger(object):def __init__(self, level='INFO'):self.level = leveldef __call__(self, func): # 接受函数def wrapper(*args, **kwargs):print("[{level}]: the function {func}() is running...".format(level=self.level, func=func.__name__))func(*args, **kwargs)return wrapper  #返回函数@logger(level='WARNING')
def say(something):print("say {}!".format(something))say("hello")

我们指定WARNING级别,运行一下,来看看输出。

[WARNING]: the function say() is running...
say hello!

07. 使用偏函数与类实现装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。

事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象。

对于这个 callable 对象,我们最熟悉的就是函数了。

除函数之外,类也可以是 callable 对象,只要实现了__ call__ 函数(上面几个例子已经接触过了)。

还有容易被人忽略的偏函数其实也是 callable 对象。

接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。

如下所示,DelayFunc 是一个实现了 __ call__ 的类,delay 返回一个偏函数,在这里 delay 就可以做为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
import time
import functoolsclass DelayFunc:def __init__(self,  duration, func):self.duration = durationself.func = funcdef __call__(self, *args, **kwargs):print(f'Wait for {self.duration} seconds...')time.sleep(self.duration)return self.func(*args, **kwargs)def eager_call(self, *args, **kwargs):print('Call without delay')return self.func(*args, **kwargs)def delay(duration):"""装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行"""# 此处为了避免定义额外函数,# 直接使用 functools.partial 帮助构造 DelayFunc 实例return functools.partial(DelayFunc, duration)
我们的业务函数很简单,就是相加@delay(duration=2)
def add(a, b):return a+b

来看一下执行过程

>>> add    # 可见 add 变成了 Delay 的实例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5)  # 直接调用实例,进入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 实现实例方法
<function add at 0x107bef1e0>

08. 如何写能装饰类的装饰器?

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

以下便是我自己写的装饰器版的单例写法。

instances = {}def singleton(cls):def get_instance(*args, **kw):cls_name = cls.__name__print('===== 1 ====')if not cls_name in instances:print('===== 2 ====')instance = cls(*args, **kw)instances[cls_name] = instancereturn instances[cls_name]return get_instance@singleton
class User:_instance = Nonedef __init__(self, name):print('===== 3 ====')self.name = name

可以看到我们用singleton 这个装饰函数来装饰 User 这个类。装饰器用在类上,并不是很常见,但只要熟悉装饰器的实现过程,就不难以实现对类的装饰。在上面这个例子中,装饰器就只是实现对类实例的生成的控制而已。

其实例化的过程,你可以参考我这里的调试过程,加以理解。

09. wraps 装饰器有啥用?
在 functools 标准库中有提供一个 wraps 装饰器,你应该也经常见过,那他有啥用呢?

先来看一个例子

def wrapper(func):def inner_function():passreturn inner_function@wrapper
def wrapped():passprint(wrapped.__name__)
#inner_function

为什么会这样子?不是应该返回 func 吗?

这也不难理解,因为上边执行func 和下边 decorator(func) 是等价的,所以上面 func.__ name__ 是等价于下面decorator(func).__ name__ 的,那当然名字是 inner_function

def wrapper(func):def inner_function():passreturn inner_functiondef wrapped():passprint(wrapper(wrapped).__name__)
#inner_function

那如何避免这种情况的产生?方法是使用 functools .wraps 装饰器,它的作用就是将 被修饰的函数(wrapped) 的一些属性值赋值给 修饰器函数(wrapper) ,最终让属性的显示更符合我们的直觉。

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
from functools import wrapsdef wrapper(func):@wraps(func)def inner_function():passreturn inner_function@wrapper
def wrapped():passprint(wrapped.__name__)
# wrapped

准确点说,wraps 其实是一个偏函数对象(partial),源码如下

def wraps(wrapped,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES):return partial(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)

可以看到wraps其实就是调用了一个函数update_wrapper,知道原理后,我们改写上面的代码,在不使用 wraps的情况下,也可以让 wrapped.__ name__ 打印出 wrapped,代码如下:

from functools import update_wrapperWRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')def wrapper(func):def inner_function():passupdate_wrapper(inner_function, func, assigned=WRAPPER_ASSIGNMENTS)return inner_function@wrapper
def wrapped():passprint(wrapped.__name__)

10. 内置装饰器:property

以上,我们介绍的都是自定义的装饰器。

其实Python语言本身也有一些装饰器。比如property这个内建装饰器,我们再熟悉不过了。

它通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。

通常我们给实例绑定属性是这样的

class Student(object):def __init__(self, name, age=None):self.name = nameself.age = age# 实例化
xiaoming = Student("小明")# 添加属性
xiaoming.age=25# 查询属性
xiaoming.age# 删除属性
del xiaoming.age

但是稍有经验的开发人员,一下就可以看出,这样直接把属性暴露出去,虽然写起来很简单,但是并不能对属性的值做合法性限制。为了实现这个功能,我们可以这样写。

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
class Student(object):def __init__(self, name):self.name = nameself.name = Nonedef set_age(self, age):if not isinstance(age, int):raise ValueError('输入不合法:年龄必须为数值!')if not 0 < age < 100:raise ValueError('输入不合法:年龄范围必须0-100')self._age=agedef get_age(self):return self._agedef del_age(self):self._age = Nonexiaoming = Student("小明")# 添加属性
xiaoming.set_age(25)# 查询属性
xiaoming.get_age()# 删除属性
xiaoming.del_age()

上面的代码设计虽然可以变量的定义,但是可以发现不管是获取还是赋值(通过函数)都和我们平时见到的不一样。
按照我们思维习惯应该是这样的。

# 赋值
xiaoming.age = 25# 获取
xiaoming.age

那么这样的方式我们如何实现呢。请看下面的代码。

class Student(object):def __init__(self, name):self.name = nameself.name = None@propertydef age(self):return self._age@age.setterdef age(self, value):if not isinstance(value, int):raise ValueError('输入不合法:年龄必须为数值!')if not 0 < value < 100:raise ValueError('输入不合法:年龄范围必须0-100')self._age=value@age.deleterdef age(self):del self._agexiaoming = Student("小明")# 设置属性
xiaoming.age = 25# 查询属性
xiaoming.age# 删除属性
del xiaoming.age

用@property装饰过的函数,会将一个函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像后面我们使用的@age.setter和@age.deleter。

  • @age.setter 使得我们可以使用XiaoMing.age = 25这样的方式直接赋值。
  • @age.deleter 使得我们可以使用del XiaoMing.age这样的方式来删除属性。

property 的底层实现机制是「描述符」,为此我还写过一篇文章。

这里也介绍一下吧,正好将这些看似零散的文章全部串起来。

如下,我写了一个类,里面使用了 property 将 math 变成了类实例的属性

class Student:def __init__(self, name):self.name = name@propertydef math(self):return self._math@math.setterdef math(self, value):if 0 <= value <= 100:self._math = valueelse:raise ValueError("Valid value must be in [0, 100]")

为什么说 property 底层是基于描述符协议的呢?通过 PyCharm 点击进入 property 的源码,很可惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。

不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。

这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类 property 特性。

代码如下:

class TestProperty(object):def __init__(self, fget=None, fset=None, fdel=None, doc=None):self.fget = fgetself.fset = fsetself.fdel = fdelself.__doc__ = docdef __get__(self, obj, objtype=None):print("in __get__")if obj is None:return selfif self.fget is None:raise AttributeErrorreturn self.fget(obj)def __set__(self, obj, value):print("in __set__")if self.fset is None:raise AttributeErrorself.fset(obj, value)def __delete__(self, obj):print("in __delete__")if self.fdel is None:raise AttributeErrorself.fdel(obj)def getter(self, fget):print("in getter")return type(self)(fget, self.fset, self.fdel, self.__doc__)def setter(self, fset):print("in setter")return type(self)(self.fget, fset, self.fdel, self.__doc__)def deleter(self, fdel):print("in deleter")return type(self)(self.fget, self.fset, fdel, self.__doc__)

然后 Student 类,我们也相应改成如下

class Student:def __init__(self, name):self.name = name# 其实只有这里改变@TestPropertydef math(self):return self._math@math.setterdef math(self, value):if 0 <= value <= 100:self._math = valueelse:raise ValueError("Valid value must be in [0, 100]")

为了尽量让你少产生一点疑惑,我这里做两点说明:

使用TestProperty装饰后,math 不再是一个函数,而是TestProperty类的一个实例。所以第二个math函数可以使用 math.setter 来装饰,本质是调用TestProperty.setter 来产生一个新的 TestProperty 实例赋值给第二个math。

第一个 math 和第二个 math 是两个不同 TestProperty 实例。但他们都属于同一个描述符类(TestProperty),当对 math 对于赋值时,就会进入 TestProperty.__ set__,当对math 进行取值里,就会进入 TestProperty.__ get__。仔细一看,其实最终访问的还是Student实例的 _math 属性。

说了这么多,还是运行一下,更加直观一点。

# 运行后,会直接打印这一行,这是在实例化 TestProperty 并赋值给第二个math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90

如对上面代码的运行原理,有疑问的同学,请务必结合上面两点说明加以理解,那两点相当关键。

11. 其他装饰器:装饰器实战

读完并理解了上面的内容,你可以说是Python高手了。别怀疑,自信点,因为很多人都不知道装饰器有这么多用法呢。

在我看来,使用装饰器,可以达到如下目的:

使代码可读性更高,逼格更高;

代码结构更加清晰,代码冗余度更低;

刚好我在最近也有一个场景,可以用装饰器很好的实现,暂且放上来看看。

这是一个实现控制函数运行超时的装饰器。如果超时,则会抛出超时异常。

有兴趣的可以看看。

import signalclass TimeoutException(Exception):def __init__(self, error='Timeout waiting for response from Cloud'):Exception.__init__(self, error)def timeout_limit(timeout_time):def wraps(func):def handler(signum, frame):raise TimeoutException()def deco(*args, **kwargs):signal.signal(signal.SIGALRM, handler)signal.alarm(timeout_time)func(*args, **kwargs)signal.alarm(0)return decoreturn wraps

以上,便是我对装饰器的所有分享。

没看完这11 条,别说你精通 Python 装饰器相关推荐

  1. 收藏!最详细的Python全栈开发指南 看完这篇你还不会Python全栈开发 你来打我!!!

    Python Web全栈开发入门实战教程教程    大家好,我叫亓官劼(qí guān jié ),这个<Python Web全栈开发入门实战教程教程>是一个零基础的实战教程,手把手带你开 ...

  2. python装饰器原理-看完这篇文章还不懂Python装饰器?

    原标题:看完这篇文章还不懂Python装饰器? 1.必备 2.需求来了 初创公司有N个业务部门,1个基础平台部门,基础平台负责提供底层的功能,如:数据库操作.redis调用.监控API等功能.业务部门 ...

  3. python装饰器带参数函数二阶导数公式_一文搞定Python装饰器,看完面试不再慌

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第12篇文章,我们来看看Python装饰器. 一段囧事 差不多五年前面试的时候,我就领教过它的重要性.那时候我Pyt ...

  4. 看完这篇文章,你的Python基础就差不多了(附571集精品教程)

    学一门语言贵在坚持用它,不用就淡忘了,而记录下一篇文章也有助于日后快速回忆.全文分为两大部分,分别是Python基础语法和面向对象. 入门Python其实很容易,但是我们要去坚持学习,每一天坚持很困难 ...

  5. 同样是技术出身,深夜看完张一鸣200条微博,让我越想越后怕

    来 源:仟语仟寻(ID:huoqian2014)  作 者:霍仟 这几天抽空把张一鸣的所有微博看了一遍,发现2010年的微博最好,就是他30岁左右的时候,那时候刚创业没多久,在微博上认真分享自己的思考 ...

  6. 程序员如何装逼看完最后一条我也是醉了

    点击上面 免费订阅本账号! 本公众号主要推送javaweb开发相关技术,基础知识点,同时会深入剖析复杂的问题,分享一些优秀的框架,大型项目经验,当今最流行的Javaweb技术,热点科技新闻,招聘信息, ...

  7. python装饰器功能是冒泡排序怎么做_传说中Python最难理解的点|看这完篇就够了(装饰器)...

    https://mp.weixin.qq.com/s/B6pEZLrayqzJfMtLqiAfpQ 1.什么是装饰器 网上有人是这么评价装饰器的,我觉得写的很有趣,比喻的很形象 每个人都有的内裤主要是 ...

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

    大家好,我是菜鸟哥! 之前的文章中提到,很多人认为理解了装饰器 的概念和用法后,会觉得自己的 Python 水平有一个明显的提高. 但很多教程在一上来就会给出装饰器的定义以及基本用法,例如你一定会在很 ...

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

    大家好,我是萱萱! 之前的文章中提到,很多人认为理解了装饰器的概念和用法后,会觉得自己的 Python 水平有一个明显的提高. 但很多教程在一上来就会给出装饰器的定义以及基本用法,例如你一定会在很多文 ...

最新文章

  1. solr的安装配置与helloworld
  2. BugKuCTF 杂项 come_game
  3. 大数据WEB阶段Spring框架 AOP面向切面编程(一)
  4. composer安装thinkphp
  5. python测试脚本截图_Python+selenium实现截图图片并保存截取的图片
  6. Python的is和==区别
  7. java数据集合总结_java集合总结 - 雷开你的门的个人空间 - OSCHINA - 中文开源技术交流社区...
  8. (转)贝莱德,从0到6万亿
  9. 快速掌握粒子编辑器 —— onebyonedesign网页版
  10. 数学建模算法与应用 数理统计
  11. 简约商业计划书PPT模板
  12. 分享一个USB转杜邦线 5V取电的3D打印件,附3D打印文件
  13. tensorflow2.X tf.data.Dataset详解
  14. CODEVS 1069 关押罪犯
  15. I3D【Inflated 3D ConvNet】——膨胀卷积网络用于行为识别
  16. Linux - 一次性计划任务之at命令使用
  17. php 数字 字母组合,PHP生成数字字母组合或纯数字的唯一订单号
  18. 粒子系统(二):绘制精美几何图案
  19. 生活杂感: 理性与感性
  20. 董明珠的权力危机:半年两位元老出局,格力进入动荡时刻

热门文章

  1. LintCode_173 链表插入排序
  2. 【MM】 基于收货的发票校验
  3. 【学习笔记】node.js基础介绍
  4. 【增强】FI行项目报表增强任意字段
  5. 定义利润中心(Profit Center)
  6. MSEG和EKBE的区别在哪里
  7. 物料BOM和生产订单BOM的区别
  8. BDC创建物料主数据各个视图
  9. sap转换成基本订单单位
  10. 会计记忆总结之九:会计档案