首页 > 编程笔记 > Python笔记 阅读:11

Python装饰器用法详解(附带实例)

装饰器是 Python 的重要特性之一,为了更好地描述装饰器,下面将通过实例一步一步地讲解。

首先实现一个简单的延时函数,并在程序中调用它:
import time

def timeTest():
    print('timeTest start')
    print('sleep 1 second...')
    time.sleep(1)
    print('timeTest end')

timeTest()
运行结果为:
timeTest start
sleep 1 second...
timeTest end
定义一个时间测试函数,函数功能非常简单:延时 1 秒,在延时前和延时后先分别打印开始和结束,再在程序中调用该函数。

假设此时想添加一个新功能:计算 timeTest() 函数的运行时间,则首先添加一个新的函数,在该函数中调用 timeTest() 函数,然后分别在调用前和调用后获取系统时间并求时间差,代码为:
import time

def calcTime(func):
    startTime = time.time()
    func()
    endTime = time.time()
    interval = endTime - startTime
    print('Time interval: %f secs' % interval)

def timeTest():
    print('timeTest start')
    print('sleep 1 second...')
    time.sleep(1)
    print('timeTest end')

calcTime(timeTest)
运行结果为:
timeTest start
sleep 1 second...
timeTest end
Time interval: 1.001065 secs
通过以上程序成功计算了 timeTest() 函数的运行时间。仔细观察程序可以发现,为了实现新功能,程序不仅新增了代码,还更改了原程序的代码:将 timeTest() 修改为 calcTime(timeTest)。

很显然,这样的方式虽然实现了新的功能,但是更改了原程序的代码。有没有一种方式,既可以实现新的功能,又不需要更改原程序的代码呢?也就是只增不改。

答案是肯定的。Python 提供的装饰器可以满足这样的需求。

所谓装饰器,就是在不修改程序代码(包括函数的实现和调用方式)的基础上动态添加新的功能。就好比一个人打扮好(写好代码)准备出门了,突然想添加一个装饰品(新的功能),如想戴一顶帽子,那么最直接的方式就是在不改变当前打扮的情况下戴上帽子即可,而不是为了戴帽子还需要把已经穿好的衣服扒掉(修改写好的代码)。

使用装饰器可以重新实现计时功能:
import time

def calcTime(func):
    def wrapper():
        startTime = time.time()
        func()
        endTime = time.time()
        interval = endTime - startTime
        print('Time interval: %f secs' % interval)
    return wrapper

@calcTime
def timeTest():
    print('timeTest start')
    print('sleep 1 second...')
    time.sleep(1)
    print('timeTest end')

timeTest()
运行结果为:
timeTest start
sleep 1 second...
timeTest end
Time interval: 1.001107 secs
在代码中,黑体部分是新增代码,即只新增代码就能新增功能,并未修改原程序的代码。其中,@calcTime 中的 @ 是 Python 装饰器的语法糖。@calcTime 放在 timeTest() 函数的定义处,相当于执行了语句:timeTest=calcTime(timeTest)。

Python被装饰的函数带参数

在以上程序中,被装饰的函数 timeTest() 没带参数,如果带参数,则装饰器该如何构建呢?

基于以上程序,修改代码如下:
import time

def calcTime(func):
    def wrapper(num):
        startTime = time.time()
        func(num)
        endTime = time.time()
        interval = endTime - startTime
        print('Time interval: %f secs' % interval)
    return wrapper

@calcTime
def timeTest(num):
    print('timeTest start')
    print('sleep %d second...' % num)
    time.sleep(num)
    print('timeTest end')

timeTest(1)
运行结果为:
timeTest start
sleep 1 second...
timeTest end
Time interval: 1.001097 secs
可以看到,当被装饰的函数 timeTest() 带参数时,可将装饰器的内嵌函数修改为被装饰函数的形式。此时,timeTest(1) 相当于 calcTime(timeTest(1))。

Python带参数的装饰器

假设想要使用共同的装饰器来修饰多个不同的函数,不同的函数有形式不同的参数,则装饰器可以通过可变参数(*args,**kwargs)来实现内嵌函数。

目前,针对被装饰函数带参数已经有了应对方法,对于装饰器本身带参数如何应对呢?下面通过实例进行介绍。

基于前文的实例,为装饰器添加一个 bool 变量,通过变量的真假判断是否调用计时功能:
import time

def calcTime(flag=False):
    if flag:
        def _calcTime(func):
            def wrapper(*args, **kwargs):
                startTime = time.time()
                func(*args, **kwargs)
                endTime = time.time()
                interval = endTime - startTime
                print("Time interval: %f secs" % interval)
            return wrapper
    else:
        def _calcTime(func):
            return func
    return _calcTime

@calcTime(False)
def timeTest1():
    print('timeTest1 start')
    print('sleep 1 second...')
    time.sleep(1)
    print('timeTest1 end')

@calcTime(True)
def timeTest2(num):
    print('timeTest2 start')
    print('sleep %d second...' % num)
    time.sleep(num)
    print('timeTest2 end')

timeTest1()
print()
timeTest2(2)
运行结果为:
timeTest1 start
sleep 1 second...
timeTest1 end

timeTest2 start
sleep 2 second...
timeTest2 end
Time interval: 2.007000 secs
由程序和运行结果可知,将同一个装饰器 calcTime 用于两个不同的函数,分别是 timeTest1() 和 timeTest2(num),一个没带参数,一个带参数,通过装饰器的参数可以为装饰过程添加判断,@calcTime(True) 表示进行计时,@calcTime(False) 表示不会进行计时,程序运行结果也证明了这一点。

Python装饰器调用顺序

在以上程序中,使用一个装饰器修饰了一个函数,如果使用多个装饰器修饰同一个函数,则装饰器的调用顺序怎样呢?下面依然通过实例进行介绍。
def dec1(func):
    print('dec1')
    def wrapper():
        print('dec1_wrapper')
        func()
    return wrapper

def dec2(func):
    print('dec2')
    def wrapper():
        print('dec2_wrapper')
        func()
    return wrapper

@dec1
@dec2
def testFunc():
    print('testFunc')

testFunc()
运行结果为:
dec1
dec2
dec2_wrapper
dec1_wrapper
testFunc
由运行结果可知,装饰器的调用顺序与语法糖 @ 的声明顺序相反。在程序中,testFunc 相当于 dec1(dec2(testFunc))。

装饰器的威力在于不需要修改原程序即可添加新的功能。比如,在Web的开发过程中,任何人都可以看网络新闻,如果想要评论,则需要先登录账号,此时就可以通过装饰器添加登录功能来修饰现有的程序。

相关文章