进阶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. antd源码分析之——标签页(tabs 1.组件结构)

    由于ant Tabs组件结构较复杂,共分三部分叙述,本文为目录中第一部分(高亮) 目录 一.组件结构 antd代码结构 rc-ant代码结构 1.组件树状结构 2.Context使用说明 3.rc-t ...

  2. mysql:启动服务时遇到的问题

    1.cmd命令: 在切换路径时,如果要切到另外一个磁盘,比如从C盘切到E盘,命令如下: cd /d 你要切换的路径 2.错误:“服务名无效” 问题原因:mysql服务没有安装.(参考:https:// ...

  3. Flume-几种拓扑结构

    一.串联 Flume Agent 连接 这种模式是将多个 flume 顺序连接起来了,从最初的 source 开始到最终 sink 传送的目的存储系统.此模式不建议桥接过多的 flume 数量,flu ...

  4. koa 允许跨域

    1.安装模块 npm install koa2-cors --save 2.引用 const cors = require('koa2-cors'); ... // 允许跨域访问 app.use(co ...

  5. Nginx在Linux安装详解及问题处理

    Linux编译安装 1.nginx 依赖于prce库,要先安装pcre. #yum install prce pcre-devel 2.下载解压nginx #cd /usr/local/src/ #w ...

  6. 企业SOA架构案例分析

    面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和契约联系起来.接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台. ...

  7. 修改Visual Studio的默认模板

    如果我在Visual Studio创建的项目中每次新建一个文件,自动生成注释或者是结构的话,那么就需要改下默认的模板了.下面以vs2013为例 我们添加的文件有很多种,这里就举例3种,CSharp类文 ...

  8. 多目标优化算法(一)NSGA-Ⅱ(NSGA2)(转载)

    多目标优化算法(一)NSGA-Ⅱ(NSGA2) 本文链接:https://blog.csdn.net/qq_40434430/article/details/82876572多目标优化算法(一)NSG ...

  9. 计算机组成原理 — FPGA 现场可编程门阵列

    目录 文章目录 目录 FPGA FPGA 的应用场景 FPGA 的技术难点 FPGA 的工作原理 FPGA 的体系结构 FPGA 的开发 FPGA 的使用 FPGA 的优缺点 参考文档 FPGA FP ...

  10. 利用百度智能云结合Python体验图像识别(转载来自qylruirui)

    https://blog.csdn.net/qylruirui/article/details/94992917 利用百度智能云结合Python体验图像识别只要注册了百度账号就可以轻松体验百度智能云中 ...