进阶Python:装饰器

前言

前段时间我发了一篇讲解Python调试工具PySnooper的文章,在那篇文章开始一部分我简单的介绍了一下装饰器,文章发出之后有几位同学说"终于了解装饰器的用法了",可见有不少同学对装饰器感兴趣。但是那篇文章主要的目的是在介绍PySnooper,所以没有太深入的展开讲解装饰器,于是在这里就详细的介绍一些装饰器的使用。

装饰器是Python中非常重要的一个概念,如果你会Python的基本语法,你可以写出能够跑通的代码,但是如果你想写出高效、简洁的代码,我认为离不开这些高级用法,当然也包括本文要讲解的装饰器,就如同前面提到的代码调试神器PySnooper一样,它就是主要通过装饰器调用的方式对Python代码进行调试。

什么是Python装饰器?

顾名思义,从字面意思就可以理解,它是用来"装饰"Python的工具,使得代码更具有Python简洁的风格。换句话说,它是一种函数的函数,因为装饰器传入的参数就是一个函数,然后通过实现各种功能来对这个函数的功能进行增强。

为什么用装饰器?

前面提到了,装饰器是通过某种方式来增强函数的功能。当然,我们可以通过很多方式来增强函数的功能,只是装饰器有一个无法替代的优势--简洁。

你只需要在每个函数上方加一个@就可以对这个函数进行增强。

在哪里用装饰器?

装饰器最大的优势是用于解决重复性的操作,其主要使用的场景有如下几个:

  • 计算函数运行时间
  • 给函数打日志
  • 类型检查

当然,如果遇到其他重复操作的场景也可以类比使用装饰器。

简单示例

前面都是文字描述,不管说的怎么天花烂坠,可能都无法体会到它的价值,下面就以一个简单的例子来看一下它的作用。

如果你要对多个函数进行统计运行时间,不使用装饰器会是这样的,

from time import time, sleep

def fun_one():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func one run time {}".format(cost_time)) def fun_two():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func two run time {}".format(cost_time)) def fun_three():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))

在每个函数里都需要获取开始时间start、结束时间end、计算耗费时间cost_time、加上一个输出语句。

使用装饰器的方法是这样的,

def run_time(func):
def wrapper():
start = time()
func() # 函数在这里运行
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))
return wrapper @run_time
def fun_one():
sleep(1) @run_time
def fun_two():
sleep(1) @run_time
def fun_three():
sleep(1)

通过编写一个统计时间的装饰器run_time,函数的作为装饰器的参数,然后返回一个统计时间的函数wrapper,这就是装饰器的写法,用专业属于来说这叫闭包,简单来说就是函数内嵌套函数。然后再每个函数上面加上@run_time来调用这个装饰器对不同的函数进行统计时间。

可见,统计时间这4行代码是重复的,一个函数需要4行,如果100个函数就需要400行,而使用装饰器,只需要几行代码实现一个装饰器,然后每个函数前面加一句命令即可,如果是100个函数,能少300行左右的代码量。

带参数的装饰器

通过前面简单的例子应该已经明白装饰器的价值和它的简单用法:通过闭包来实现装饰器,函数作为外层函数的传入参数,然后在内层函数中运行、附加功能,随后把内层函数作为结果返回。

除了上述简单的用法还有一些更高级的用法,比如用装饰器进行类型检查、添加带参数的的装饰器等。它们的用法大同小异,关于高级用法,这里以带参数的装饰器为例进行介绍。

不要把问题想的太复杂,带参数的装饰器其实就是在上述基本的装饰器的基础上在外面套一层接收参数的函数,下面通过一个例子说明一下。

以上述例子为基础,前面的简单示例输出的信息是,

func three run time 1.0003271102905273
func three run time 1.0006263256072998
func three run time 1.000312328338623

现在我认为这样的信息太单薄,需要它携带更多的信息,例如函数名称、日志等级等,这时候可以把函数名称和日志等级作为装饰器的参数,下面来时实现以下。

def logger(msg=None):
def run_time(func):
def wrapper(*args, **kwargs):
start = time()
func() # 函数在这里运行
end = time()
cost_time = end - start
print("[{}] func three run time {}".format(msg, cost_time))
return wrapper
return run_time @logger(msg="One")
def fun_one():
sleep(1) @logger(msg="Two")
def fun_two():
sleep(1) @logger(msg="Three")
def fun_three():
sleep(1) fun_one()
fun_two()
fun_three()

可以看出,我在示例基本用法里编写的装饰器外层又嵌套了一层函数用来接收参数msg,这样的话在每个函数(func_one、func_two、func_three)前面调用时可以给装饰器传入参数,这样的输出结果是,

[One] func three run time 1.0013229846954346
[Two] func three run time 1.000720500946045
[Three] func three run time 1.0001459121704102

自定义属性的装饰器

上述介绍的几种用法中其实有一个问题,就是装饰器不够灵活,我们预先定义了装饰器run_time,它就会按照我们定义的流程去工作,只具备这固定的一种功能,当然,我们前面介绍的通过带参数的装饰器让它具备了一定的灵活性,但是依然不够灵活。其实,我们还可以对装饰器添加一些属性,就如同给一个类定义实现不同功能的方法那样。

以输出日志为例,初学Python的同学都习惯用print打印输出信息,其实这不是一个好习惯,当开发商业工程时,你很用意把一些信息暴露给用户。在开发过程中,我更加鼓励使用日志进行输出,通过定义WARNINGDEBUGINFO等不同等级来控制信息的输出,比如INFO是可以给用户看到的,让用户直到当前程序跑到哪一个阶段了。DEBUG是用于开发人员调试和定位问题时使用。WARING是用于告警和提示。

那么问题来了,如果我们预先定义一个打印日志的装饰器,

def logger_info(func):
logmsg = func.__name__
def wrapper():
func()
log.log(logging.INFO, "{} if over.".format(logmsg))
return wrapper

http://logging.INFO是打印日志的等级,如果我们仅仅写一个基本的日志装饰器logger_info,那么它的灵活度太差了,因为如果我们要输出DEBUGWARING等级的日志,还需要重新写一个装饰器。

解决这个问题,有两个解决方法:

  • 利用前面所讲的带参数装饰器,把日志等级传入装饰器
  • 利用自定义属性来修改日志等级

由于第一种已经以统计函数运行时间的方式进行讲解,这里主要讲解第二种方法。

先看一下代码,

import logging
from functools import partial def wrapper_property(obj, func=None):
if func is None:
return partial(attach_wrapper, obj)
setattr(obj, func.__name__, func)
return func def logger_info(level, name=None, message=None):
def decorate(func): logmsg = message if message else func.__name__ def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs) @wrapper_property(wrapper)
def set_level(newlevel):
nonlocal level
level = newlevel @wrapper_property(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg return wrapper return decorate @logger_info(logging.WARNING)
def main(x, y):
return x + y

这里面最重要的是wrapper_property这个函数,它的功能是把一个函数func编程一个对象obj的属性,然后通过调用wrapper_property,给装饰器添加了两个属性set_messageset_level,分别用于改变输出日志的内容和改变输出日志的等级。

看一下输出结果,

main(3, 3)

# 输出
# WARNING:Test:main
# 6

来改改变一下输出日志等级,

main.set_level(logging.ERROR)
main(5, 5) # 输出
# ERROR:Test:main
# 10

输出日志等级改成了ERROR

保留元信息的装饰器

很多教程中都会介绍装饰器,但是大多数都是千篇一律的围绕基本用法在展开,少部分会讲一下带参数的装饰器,但是有一个细节很少有教程提及,那就是保留元信息的装饰器

什么是函数的元信息?

就是函数携带的一些基本信息,例如函数名、函数文档等,我们可以通过func.name获取函数名、可以通过func.doc获取函数的文档信息,用户也可以通过注解等方式为函数添加元信息。

例如下面代码,

from time import time

def run_time(func):
def wrapper(*args, **kwargs):
start = time()
func() # 函数在这里运行
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))
return wrapper @run_time
def fun_one():
'''
func one doc.
'''
sleep(1) fun_one() print(fun_one.__name__)
print(fun_one.__doc__) # 输出
# wrapper
# None

可以看出,通过使用装饰器,函数fun_one的元信息都丢失了,那怎么样才能保留装饰器的元信息呢?

可以通过使用Python自带模块functools中的wraps来保留函数的元信息,

from time import time
from functools import wraps def run_time(func):
@wraps(func) # <- 这里加 wraps(func) 即可
def wrapper(*args, **kwargs):
start = time()
func() # 函数在这里运行
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))
return wrapper @run_time
def fun_one():
'''
func one doc.
'''
sleep(1) fun_one() print(fun_one.__name__)
print(fun_one.__doc__) # 输出
# fun_one
# func one doc.

只需要在代码中加入箭头所指的一行即可保留函数的元信息。

进阶Python:装饰器 全面详解的更多相关文章

  1. python装饰器大详解

    1.作用域 在python中,作用域分为两种:全局作用域和局部作用域. 全局作用域是定义在文件级别的变量,函数名.而局部作用域,则是定义函数内部. 关于作用域,我要理解两点:a.在全局不能访问到局部定 ...

  2. python装饰器学习详解-函数部分

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 最近阅读<流畅的python>看见其用函数写装饰器部分写的很好,想写一些自己的读书笔记. ...

  3. python装饰器使用详解

    装饰器 '''装饰器:就是闭包(闭包的一个应用场景) -- 把要被装饰的函数作为外层函数的参数通过闭包操作后返回一个替代版函数 优点: -- 丰富了原有函数的功能 -- 提高了程序的可拓展性''' 开 ...

  4. python 函数及变量作用域及装饰器decorator @详解

    一.函数及变量的作用   在python程序中,函数都会创建一个新的作用域,又称为命名空间,当函数遇到变量时,Python就会到该函数的命名空间来寻找变量,因为Python一切都是对象,而在命名空间中 ...

  5. (十)装饰器模式详解(与IO不解的情缘)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. LZ到目前已经写了九个设计模 ...

  6. 涉及模式之 装饰器模式详解(与IO不解的情缘)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. LZ到目前已经写了九个设计模 ...

  7. Java 装饰器模式详解

    转载请注明出处:http://blog.csdn.net/zhaoyanjun6/article/details/56488020 前言 在上面的几篇文章中,着重介绍了Java 中常见的 IO 相关知 ...

  8. Python装饰器详解

    python中的装饰器是一个用得非常多的东西,我们可以把一些特定的方法.通用的方法写成一个个装饰器,这就为调用这些方法提供一个非常大的便利,如此提高我们代码的可读性以及简洁性,以及可扩展性. 在学习p ...

  9. python装饰器(docorator)详解

    引言: 装饰器是python面向对象编程三大器之一,另外两个迭代器.生成器只是我现在还没有遇到必须使用的场景,等确实需要用到的时候,在补充资料:装饰器在某些场景真的是必要的,比如定义了一个类或者一个函 ...

随机推荐

  1. [vbs]脚本启动

    Set ws = CreateObject("Wscript.Shell") ws.run "cmd.exe /c start tool.exe config_tence ...

  2. Spring Cloud Eureka(一): 开篇说明及目录汇总

    开篇简述 基于Spring Boot 和 Spring Cloud 的微服务应用,本人在工作中已经使用两年有余了,伴随着个人学习计划的实施,希望借助博文的方式,将工作中使用到的技术点体系化的总结出来, ...

  3. Visual Studio Code(VS code)介绍

    一.日常安利 VS code VS vode特点: 开源,免费: 自定义配置 集成git 智能提示强大 支持各种文件格式(html/jade/css/less/sass/xml) 调试功能强大 各种方 ...

  4. Django1.6 + jQuery Ajax + JSON 实现页面局部实时刷新

    最近微信公众帐号要扩展做一个签到系统,签到结果在一个网页上实时更新,即页面局部刷新.我想用Ajax来实现,之前公众帐号是用的Django搭的,我查找了Django的官方文档,没有封装Ajax.网上有各 ...

  5. 我的zshrc文件设置备份

    # If you come from bash you might have to change your $PATH. # export PATH=$HOME/bin:/usr/local/bin: ...

  6. 2个最好的JavaScript编辑器 必须要知道

    JavaScript程序员有许多很好的工具可供选择,几乎太多了.在这篇文章中,介绍2个最好用的文本编辑器,也是顶级的.并且很好地支持使用JavaScript,HTML5和CSS进行开发,并用Markd ...

  7. FFmpeg之Linux下编译与调试

    注:下面的一切都是在 root 模式下进行的,可以不再 root 模式下进行 1. 安装linux的基础环境 基础环境就是编译代码的基础库,Ubuntu联网安装软件很简单,一个语句即可搞定,这里列出语 ...

  8. fastcgi代理

    一.fastcgi代理 1.示意图 2.fastcgi 代理配置语法 a.设置PHP服务代理地址 Syntax: fastcgi_pass address; Default: — Context: l ...

  9. k8s-helm02-----helm常用命令

    查看在存储库中可用的所有 Helm charts helm search helm status 查询一个特定的 Release 的状态. helm status XXX  

  10. 处理输入为非对角阵的Clustering by fast search and find of density peak代码

    Clustering by fast search and find of density peak. Alex Rodriguez, Alessandro Laio 是发表在Science上的一篇很 ...