什么是Python装饰器?

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

1.什么是Python装饰器?

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

2.为什么用装饰器?

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

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

3.在哪里用装饰器?

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

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

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

4.简单示例

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

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

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行左右的代码量。

5.带参数的装饰器

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

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

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

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

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

6.自定义属性的装饰器

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

以输出日志为例,初学Python的同学都习惯用print打印输出信息,其实这不是一个好习惯,当开发商业工程时,你很用意把一些信息暴露给用户。在开发过程中,我更加鼓励使用日志进行输出,通过定义WARNING、DEBUG、INFO等不同等级来控制信息的输出,比如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,那么它的灵活度太差了,因为如果我们要输出DEBUG、WARING等级的日志,还需要重新写一个装饰器。

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

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

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

先看一下代码,

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_message和set_level,分别用于改变输出日志的内容和改变输出日志的等级。

看一下输出结果,

main(3, 3)

# 输出
# WARNING:Test:main
# 6

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

main.set_level(logging.ERROR)
main(5, 5)

# 输出
# ERROR:Test:main
# 10

输出日志等级改成了ERROR。

7.保留元信息的装饰器

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

什么是函数的元信息?

就是函数携带的一些基本信息,例如函数名、函数文档等,我们可以通过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

#学习中遇到问题没人解答?小编创建了一个Python学习交流群:711312441
@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装饰器? - Python技术站

(0)
上一篇 2023年3月31日 下午9:02
下一篇 2023年3月31日

相关文章

  • python中可以处理word文档的模块:docx模块

    一.docx模块 Python可以利用python-docx模块处理word文档,处理方式是面向对象的。也就是说python-docx模块会把word文档,文档中的段落、文本、字体等都看做对象,对对象进行处理就是对word文档的内容处理。 二.相关概念 如果需要读取word文档中的文字(一般来说,程序也只需要认识word文档中的文字信息),需要先了解pyth…

    Python开发 2023年3月31日
    00
  • python中的mysql操作教程及实例

    一. 数据库在自动化测试中的应用 存测试数据 有的时候大批量的数据,我们需要存到数据库中,在测试的时候才能用到,测试的时候就从数据库中读取出来。这点是非常重要的! 存测试结果 二. python中的数据库之必备前提条件 1. 安装:pyMysql python2中使用mysqldb,python3中不再支持,换成pyMysql 2. 安装步骤 安装pyMys…

    Python开发 2023年4月2日
    00
  • Python3教程:多线程的使用教程

    一、简介 Python中使用线程有两种方式:函数或者用类来包装线程对象。 函数式:调用 _thread 模块中的start_new_thread()函数来产生新线程。语法如下: 参数说明: function – 线程函数。 args – 传递给线程函数的参数,他必须是个tuple类型。 kwargs – 可选参数。 实例: import _thread im…

    Python开发 2023年4月2日
    00
  • Python学习:构造函数与析构函数

    1.构造函数: __init__(self), 这个方法就是构造函数,在实例化的时候自动调用。 所有如果这个函数内有打印的方法,当实例出来的时候会打印里面的信息。 __init__方法的第一个参数永远都是self,表示创建实例本身,在__init__方法内部,可以把各种属性绑定到self,因为self指向创建的实例本身。 有了__init__方法,在创建实例…

    Python开发 2023年4月2日
    00
  • Python中 re.compile 函数的使用

    以下介绍在python的re模块中怎样应用正则表达式 1. 使用re.compile re模块中包含一个重要函数是compile(pattern [, flags]) ,该函数根据包含的正则表达式的字符串创建模式对象。 可以实现更有效率的匹配。在直接使用字符串表示的正则表达式进行search,match和findall操作时,python会将字符串转换为正则…

    Python开发 2023年3月31日
    00
  • Python学习:转义字符及用法、数据类型转换函数

    ASCII编码为每个字符都分配了唯一的编号,成为编码值。在Python中,一个ASCII字符除了可以用它的实体(真正的字符)表示,还可以用它的编码值表示。这种使用编码值来间接地表示字符的方式成为转义字符(Escape Character)。 转义字符以\0或者\x开头、以\0开头表示后跟八进制形式的编码值,以\x开头表示后跟十六进制形式的编码值。Python…

    Python开发 2023年4月2日
    00
  • Python类的封装教程

    一、什么是封装 封装的本身意思其实就和闭包函数一样,就是把一个函数和变量全都包在一起,但其实这样的说法不是很具体,就是一种很片面的解释 二、为什么要封装 封装数据的主要原因是:保护隐私 封装方法的主要原因是:隔离复杂度(快门就是傻瓜相机为傻瓜们提供的方法,该方法将内部复杂的照相功能都隐藏起来了,只提供了一个快门键,就可以直接拍照) 提示:在编程语言里,对外提…

    Python开发 2023年3月31日
    00
  • python进程池中的回调函数

    什么是回调函数 指定一个任务后、并且指定一个回调函数后,当指定的进程池执行的任务结束后,会将该任务的返回值作为回调函数的参数传递到回调函数中,并且回调函数得以执行 回调函数在主进程中被执行 import os from multiprocessing import Pool def func1(n): print(‘in func1’, os.getpid(…

    python 2023年4月17日
    00
合作推广
合作推广
分享本页
返回顶部