Python装饰器学习笔记

装饰器

装饰器,也可以理解为修饰函数的函数,比如我们需要记录一下一些重要函数的执行时间,以便于后续的数据分析,这个时候就可以通过装饰器来实现,此外也可以用它来注册一系列的函数,定义对一批数据的处理流程(文末会有一个定义一个数据处理的pipeline的例子)。

简单的装饰器示例

先通过一个简单的例子,来认识一下装饰器,比如,需要打印一下某些函数的在创建的时候,名称中是否包含指定的字符串;

def format(func):
    if "test" in func.__name__: # 这个装饰器要求我们的函数名称必须包含test,否则就返回一个错误
        return func # 满足条件的,就将函数返回
    else:
        raise Exception("函数名称必须包含 test ")

定义一个函数,该函数的名称不包括test

@format
def myfunc():
    print("hello world")

这个时候,函数定义报错了,因为它的名称不包含test
在这里插入图片描述
若是函数名称包含test,则可以正常运行,代码如下:

@format
def testfunc(): # 这个是满足条件的
    print("hello world")
testfunc()
  • 上面的装饰器,是在函数创建时进行判断的,那么,有没有办法在函数运行时进行判断呢?比如,我们需要记录一下函数的开始执行时间
  • 思考:在上面的例子当中,首先是对函数名称进行了判断,如果满足条件,则返回函数,不满足则报错,那么是否可以再在装饰器内部封装一个函数,然后把这个封装的函数返回,这样就相当于把装饰器内部封装的函数返回给当前定义的函数了,这样就可以在内部封装的函数中,执行一些其他操作,进而为当前定义的函数提供更多的功能。


    下面来尝试需要记录函数每次运行的开始时间。
from datetime import datetime
def log_start_time(func):

    def wrapped(func):
        print("现在开始执行函数,时间是:",datetime.now())

        return func
    
    return wrapped
@log_start_time
def myfunc():
    print("hello world")
@log_start_time
def myfunc():
    print("hello world")
print(myfunc.__name__)

myfunc() 

上面的代码报错了, 思考一下,通过 @log_start_time 装饰器,在定义函数的时候返回的wrapped,但是返回的warpped是需要参数的,这个时候myfunc = wrapped,而wrapped是有参数的…myfunc并没有…所以去掉一下wrapped中的参数。
更改后如下:

from datetime import datetime
def log_start_time(func):

    def wrapped():
        print("现在开始执行函数,时间是:",datetime.now())

        return func
    
    return wrapped
@log_start_time
def myfunc():
    print("hello world")
myfunc() 
# out: 现在开始执行函数,时间是: 2023-05-12 11:20:01.085597  <function __main__.myfunc()>
print(myfunc.__name__)
# out : 现在开始执行函数,时间是: 2023-05-12 11:19:52.811810   wrapped


  • 上面的例子中,myfunc并没有像预期那样输出hello world,而是输出了一个函数定义。
  • 思考一下,() 表示函数的调用,而当前myfunc = wrapped,调用myfunc 相当于执行了 wrapped,而wrapped的返回就是之前定义的函数myfunc
  • 这里有点儿绕,原因是,在通过装饰器来定义myfunc的时候,装饰器的入参是myfunc,但是装饰器返回的却是内部的wrapped函数,造成了我们定义的myfunc函数变成了指向了wrapped函数,
  • 这就相当于我们定义了一个函数 def foor():pass,然后将其赋值给 a,即:a = foo,这个时候,a的函数名也是foo,即
    a.__name__ 也为 foo,这个问题后面再解决,下面继续让myfunc可以正常的执行。

这个时候,就需要修改装饰器内部的封装函数,让其在内部调用myfunc,这样就能够让myfunc正常执行。、

from datetime import datetime
def log_start_time(func):

    def wrapped():
        print("现在开始执行函数,时间是:",datetime.now())

        return func() # 注意,这个时候带括号了,说明要执行这个函数,并且返回其执行结果
    
    return wrapped
@log_start_time
def myfunc():
    print("hello world")
myfunc() # 下面就执行正常了

下面解决前面的,需要函数名称变化的问题,我们可以通过python提供的类库,声明某些函数只是装饰器内部的处理函数,同时告诉他正确的函数是谁。

from functools import wraps

# 这个时候,就需要修改装饰器内部的封装函数,让其在内部调用myfunc,这样就能够让myfunc正常执行
from datetime import datetime
def log_start_time(func):

    @wraps(func) # 通过python提供的类库,以装饰器的形式来声明,下面的函数只是装饰器内部的处理函数,正确的函数信息,应该是func中的
    def wrapped():
        print("现在开始执行函数,时间是:",datetime.now())

        return func() # 注意,这个时候带括号了,说明要执行这个函数,并且返回其执行结果
    
    return wrapped
@log_start_time
def myfunc():
    print("hello world")
print("此时的函数名称为:",myfunc.__name__)
# out  :myfunc

总结:

  1. 从上面的例子中可以看出,装饰器本身就是一种函数,在使用装饰器的时候,装饰器的参数就是其下声明的函数;
  2. 那么问题来了,若是我们需要为装饰器增加一些参数该怎么办?比如:我们要知道某些函数的开始运行时间,并且将其写入到某个日志文件当中,然后不同的类型的函数需要写入到的日志文件是不同的
  3. 强大的python是支持这样的特性的,我们可以为装饰器再增加一层函数,在函数中封装日志文件的参数,之前,返回在返回装饰器,这样子,装饰器所在的上下文就有了关于日志文件的信息。

为装饰器增加参数

下面修改上面的装饰器,将其封装到一个函数当中,这个函数的入参为日志文件的地址。

from functools import wraps
from datetime import datetime
def log_start_func(log_file="out.log"):

    # 这里是真正的装饰器
    def log_start_time(func):
        @wraps(func) # 通过装饰器声明,下面的函数只是装饰器内部的处理函数,正确的函数信息,应该是func中的
        def wrapped():
            print("现在开始执行函数,时间是:",datetime.now())
            # 打开这个日志文件,并且写入
            with open(log_file,"a+",encoding="utf-8") as f:
                f.write(
                    "现在开始执行函数"+func.__name__+",时间是:"+str(datetime.now())+"\n"
                )

            return func() # 注意,这个时候带括号了,说明要执行这个函数,并且返回其执行结果
        return wrapped
    
    #通过调用这个函数,返回这个装饰器
    return log_start_time
@log_start_func() # 首先这里通过调用这个函数,会反正真正的装饰器
def myfunc():
    print("hello world")
myfunc()

# out :现在开始执行函数,时间是: 2023-05-12 11:29:02.092102  hello world

这个时候,我们在装饰器外部增加了一个函数,该函数会记录一些与日志有关的参数,进而可以在装饰器内部的上下文中访问到这些参数。

总结:目前为止,已经了解到了装饰器以及需要一些其他参数的装饰器的定义和使用方法,下面来继续了解带参数的函数结合装饰器的使用方法,此外,还有一点需要谨记,装饰器的调用形式上是不需要括号的,其参数就是它所修饰的函数,而我们在代码中看到的某个装饰器后面有括号,其实际上是通过调用了一个函数,然后返回的装饰器。

为函数增加参数

  1. 考虑一个函数,函数输入是一个名字,输出是 “hello , {name}”
    思考,在上面的例子中,装饰器的入参是函数的名称,装饰器返回的是经过装饰器处理(处理一词不是很恰当,因为这种处理还可能放在返回的函数的内部)后的函数(本文称为:装饰后函数),也就是说,我们只需要在装饰后函数中,获取相关参数即可。
  2. python对于参数的获取提供了两个类型,一个是 *args,表示的是一个可迭代的元组对象,用于传递未命名参数,一个是**kwargs,表示一个字典对象,用于传递命名参数。

下面,来修改装饰器的定义

from datetime import datetime
def log_start_func(log_file="out.log"):

    # 这里是真正的装饰器
    def log_start_time(func):
        @wraps(func) # 通过装饰器声明,下面的函数只是装饰器内部的处理函数,正确的函数信息,应该是func中的
        def wrapped(*args,**kwargs): # 在装饰器中增加这两个参数,分别用来获取未命名的以元组传递的参数和已命名的以字典传递的参数
            print("现在开始执行函数,时间是:",datetime.now())
            print("封装到元组中的参数有:",args)
            print("封装到字典中的参数有:",kwargs)
            # 打开这个日志文件,并且函数的开始执行时间
            with open(log_file,"a+",encoding="utf-8") as f:
                f.write(
                    "现在开始执行函数"+func.__name__+",时间是:"+str(datetime.now())+"\n"
                )

            return func(*args,**kwargs) # 这里将参数传递给函数,执行并返回结果
        return wrapped
    
    #通过调用这个函数,返回这个装饰器
    return log_start_time
@log_start_func() # 首先这里通过调用这个函数,会反正真正的装饰器
def myfunc(name):
    print("hello {}".format(name))
@log_start_func() # 首先这里通过调用这个函数,会反正真正的装饰器
def myfunc(name):
    print("hello {}".format(name))
myfunc("小明") # 不声明关键字,参数会被传递到元组当中

输出为:
在这里插入图片描述

myfunc(name="小明") # 声明关键字,参数会被传递到字典当中

在这里插入图片描述

案例:使用装饰器来配置一个数据处理的管道

目前为止,已经对python的装饰器的内容有了基本的了解,接下来完成一个例子
在数据处理当中,通常会通过管道的方式对数据进行清洗或者转换,比如,首先要对数据的合法性进行校验,然后对数据进行一些运算(例如归一化,取对数等等),然后执行一些其他操作(比如数据填充)
下面通过pipeline的方式,来对一波简单的数据进行处理,步骤如下:

  1. 首先,判断保留仅大于0的数字
  2. 对该批数据进行softmax归一化
  3. 支持扩展的其他操作…

此外,还需要在日志当中,记录下每一次处理步骤的开始时间和结束时间,以及其对应的数据的输入和输出。

  1. 定义一个管道。
  # 1. 首先定义一个管道,管道是一个列表,用于存储对数据处理的函数
pipeline = []
  1. 定义装饰器,装饰器中,会注册该函数到管道当中,并且将函数的执行开始时间、结束时间、输入输出及函数名写入日志当中。
from datetime import datetime
from functools import wraps
# 下面开始定义该装饰器
def pipe_log(log_file="out.log"):

    # 这里是真正的装饰器
    def wrapped(func):

        @wraps(func)
        def para_handler(*args,**kwargs):
            start = datetime.now()
            result = func(*args,**kwargs)
            end = datetime.now()
            with open(log_file,"a+",encoding="utf-8") as f:
                f.write(
                    "函数:" +func.__name__+ \
                    "\t" + \
                    "输入为:" +str(kwargs["input"])+ \
                    "\t" + \
                    "输出为:" + str(result) + \
                    "\t" + \
                    "执行开始时间:"+str(start) +\
                    "\t" + \
                    "执行结束时间:"+str(end) +"\n"
                )
            # 最后,返回函数执行结果
            return result
        # 注册函数
        pipeline.append(para_handler)
        print(para_handler.__name__,"已注册到pipeline")
        return para_handler # 返回函数

    return wrapped
  1. 处理函数注册。
# 定义处理函数1,该函数仅仅保留列表中大于0的数字
@pipe_log()
def number_filter(input:list):
    tmp_list = []
    for i in input:
        if i >= 0:
            tmp_list.append(i)
    return tmp_list
    # out : number_filter 已注册到pipeline
# 定义处理函数2,计算softmax
import math
@pipe_log()
def norm_with_soft(input:list):
    exp_list = [math.exp(i) for i in input]
    sum_exp = sum(exp_list)
    result = [i/sum_exp for i in exp_list]
    return result
# out : norm_with_soft 已注册到pipeline
  1. 验证
arr = [0,1,2,3,4,5,6,7,8,9,-1]
for handler in pipeline:
    arr = handler(input = arr)
print("处理结果为:")
arr

在这里插入图片描述

打开日志文件:
在这里插入图片描述

注:若转载本文章,请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值