大话python装饰器

由于装饰器的结构和使用形式,相信很多python的初学者在学习的过程中有很多困惑,本文尽量站在初学者的角度,用大白话和简单的代码对装饰器进行讲解,绕开闭包和对象引用的概念,希望尽可能减少初学者在学习装饰器时的困惑。

1 什么是装饰器

其实对于初学者来说,最大的疑惑可能是装饰器是干什么用的?为什么我在编程的过程中基本上用不到,我在什么场合下必须用它呢?

其实装饰器很简单,从名字上就可以看出它的功能,它就是装饰用的。而它的装饰对象就是函数,而所谓的“装饰”就是给函数添加功能的意思。

这个时候你可能会想,给函数添加功能,直接修改函数不就行了,为啥要用装饰器呢。事实上确实是这样,用装饰器能完成的功能,直接修改函数也能达到同样的效果,这也是为什么初学者基本上用不到装饰器的原因。但是在实际的项目的某个阶段,你可能不允许改动测试好的函数,而想增加一些功能,比如统计这个函数运行的时间,那么这个时候,装饰器就派上用场了。这也恰恰是装饰器的本质所在,在不改变函数和它的引用的情况下,增加函数的功能。

那么基于以上的分析,假设现在我们有如下的函数,其作用是在两秒后,输出打印的内容。在这个函数的基础上,我们提出以下需求,后文将在解决这个需求的过程中,逐步深入装饰器的原理。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

print_function() #1

1、给该函数增加一个运行耗时统计的功能

2、不改动这个函数的内部代码

3、不改动这个函数的引用

好,如果你没有思考过这个问题,可以在这里停下来想一想再继续下面的内容。

2 问题的解决

现在我们想要增加一个统计这个函数运行时间的功能,而不改动函数的源码,首先我们想到的是,在这个函数引用(#1 处)的外部,添加这些计时功能的代码,就像下面这样:

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))

可以看到,通过在 #1处的上下增加代码,我们实现了函数的计数功能,而且没有改变函数的内部代码。那么这些增加的计时所用的代码就是所谓的“装饰”,其实装饰器的原理就是这么简单。但是这种方式增加的代码,让函数的引用变得更加复杂。现在我们换一种方式,让代码看起来更加优雅。很自然的一个想法是,我们把最后四行再封装成一个函数,去引用它。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

def print_function_time():
    time_start=time.time()
    print_function() #1
    time_end=time.time()
    print("print function uses %f seconds"%(time_end-time_start))

print_function_time()

很显然,print_function_time() 就是我们要的函数,但是由于它的函数名改变了,不符合我们一开始提出的问题。那有没有什么办法不改名函数名,也能实现同样的功能呢,答案就是高阶函数。其实不要被高阶函数这个名字唬住了,它和普通函数唯一的区别就在于它返回的不是数值、字符串、列表等这样普通的对象,它返回的是一个函数。我们知道,当我们使用下面这条语句之后,f() 和print_function() 其实是等价的。

f=print_function()

那么我们自然想到,把我们要“装饰”的函数(print_function)传到一个函数里面,增加计时功能之后,再把它当做返回值返回,废话不多说,上代码。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

def timer(func): #1
    def deco():
        time_start=time.time()
        func() #2
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
    return deco

print_function=timer(print_function) #3
print_function()

#1 处我们定义了一个装饰函数,#2 处为传入的被装饰的函数,最外层的timer函数返回的是 deco函数,也就是“装饰后”的 func 函数。

因此我们在 #3处 引用 timer(print_function) 其实就是引用了deco(),也就是被装饰后的print_function 函数,最后一行中print_function()的引用虽然和之前定义的函数名相同,但实际上,他已经在原来的基础上增加了计时功能。

至此,我们已经实现了开头提出来的问题,在不改变函数内部代码和函数引用的情况下,增加计时功能。这就是一个装饰器的工作原理和过程,其中print_function 我们可以称之为被装饰的对象(函数),外部的 timer 就是装饰器,当函数被当成参数传入到装饰器中,装饰器返回一个被装饰之后的函数,这个过程我们可以称之为装饰。

当然python 提供了一种比较直观的装饰器语法,来代替上面这种朴素的写法,使代码更加优雅。

3 python 中装饰器的写法

在我们上面的写法中,需要在每个被装饰的函数前面加上一句

print_function=timer(print_function)

python 提供了一种更加简洁的写法。在需要装饰的函数定义(#1 处)上面加 @timer 语句,就表示该函数被timer函数装饰,注意,在引用装饰函数(@timer)之前,需要先定义装饰函数,不然会报错。下面这段代码是python 装饰器的标准写法,但是它和第2小节中的那段代码是等效的。

import time 

def timer(func):
    def deco():
        time_start=time.time()
        func()
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
    return deco

@timer # 1
def print_function():
    time.sleep(2)
    print("this is a print function")

    
print_function()

4 装饰有返回值的函数和带有参数的函数

相信看到这,你已经对装饰器的原理和作用有了一个初步的理解。在上面我们被装饰的函数是一个没有返回值和没有参数的函数,那么假如要装饰有返回值和有参数的函数,该怎么做呢,首先,我们来看有返回值的函数。

4.1 装饰有返回值的函数

装饰有返回值的函数其实很简单,在上面一段代码中,其实我们最后引用的print_function() 其实就是 deco()函数,显然目前deco 函数是没有返回值的,所以需要在deco函数中添加一个 return 项,那么要return 什么呢?因为我们要的是被装饰函数(print_function)的返回值,其实就是要返回其内部 func()的返回值,所以需要把func()的返回值记录下来,然后在deco 函数中返回:

import time 

def timer(func):
    def deco():
        time_start=time.time()
        value=func()
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer # 1
def print_function():
    time.sleep(2)
    print("this is a print function")
    return "run over"

print_function() #1  输出 'run over'

4.2 返回带有参数的函数

实际中的函数往往是有参数的,如果将上面被装饰的print_function 函数定义处直接改成有参函数,如下面的代码所示,显然这段代码是会报错的,因为在装饰函数中传入的函数是没有参数的函数。

import time 

def timer(func):
    def deco():
        time_start=time.time()
        value=func() 
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer 
def print_function(parameter): #2 增加形参
    time.sleep(2)
    print("this is a print function")
    return "run over"

print_function() 

为了装饰带有参数的函数,需要给装饰器中的deco()和func()也加上参数,考虑到参数数量的不确定性和不同类型(普通参数和关键字参数),参数采用可变参数(**args)和可变关键字参数(**args)两者的组合,这样就可以适应有各种参数的函数。

import time 

def timer(func):
    def deco(*args,**kwargs):
        time_start=time.time()
        value=func(*args,**kwargs) 
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer 
def print_function(parameter): #2 增加形参
    time.sleep(2)
    print("this is a print function",parameter)
    return "run over"

print_function('with parameter')  # 输出 this is a print function with parameter

5 带有参数的装饰器

有时候,同一个装饰器,针对不同的输入函数要采取不同的装饰方式。因此就需要有参数来标记对哪个函数采取哪种装饰措施,这种带有参数的装饰器的使用如下:

@timer(parameter=parmeter_value)

比如我们有两个print_function,分别是print_function1 和 print_function2,如下所示:

def print_function1(): 
    time.sleep(1)
    print("this is the print function 1")

def print_function2()
    time_sleep(2)
    print(this is the print function 2) 

针对不同的打印函数,除了都需要计时外,装饰器还要针对传入的两个不同函数输出“in print_function1”和“in print_function1/print_function2”,这个时候装饰器就需要有参数来区分传入的函数为print_function1还是print_function2,不废话,上代码:

import time 

def timer(parameter):
    def outer(func): #1 增加一层函数
        def deco():
            print("in",parameter) #1 针对不同的输出不同的语句
            time_start=time.time()
            func() 
            time_end=time.time()
            print("print function uses %f seconds"%(time_end-time_start))
        return deco
    return outer

@timer (parameter="print_function1")
def print_function1():
    time.sleep(1)
    print("this is the print function1")

@timer(parameter="print_function2")
def print_function2():
    time.sleep(2)
    print("this is the print function2")

print_function1()  # 输出 in print_function1
print_function2()  # 输出 in print_function2

由于要把paramter 参数传递到装饰器中,我们以往都是将函数名传入装饰器,现在多了一个参数,要怎么解决呢。一个自然的想法就是直接在原来timer(func)中直接增加一个参数,变为time(func, parameter),但是这样直接将两个参数放到一起会引起后面引用的错误。

实际的做法就是在 #1 处增加一层函数来接受参数,其产生的效果如下所示:

timer=timer(parameter)
print_function=timer(print_function)

其运行就和一般的装饰器一样了。

6、进一步阅读

其实装饰器是闭包思想的一个应用,关于闭包的介绍,请参考谈谈自己的理解:python中闭包,闭包的实质

 

参考文章:

1、Python装饰器的通俗理解

2、12步轻松搞定python装饰器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值