一个例子解释python装饰器

引出装饰器

首先通过下面的代码引出今天的话题:

import time

def print_odds():
    '''
    输出0~100之间的所有奇数,并统计执行时间
    '''
    start_time = time.clock()# 起始时间
    # 查找并输出所有奇数
    for i in range(100):
        if i % 2 ==1:
            print(i)
    end_time = time.clock()# 结束时间
    print("it takes {}s to find all the olds".format(end_time - start_time))

if __name__ == '__main__':
    print_odds()

这段代码的作用是输出0~100之间的所有奇数,并统计执行时间。但是其有一个很大的缺点,就是主要函数逻辑(查找奇数)和辅助功能(记录时间)耦合在一起,不方便修改,容易引起bug,那么我们能不能将辅助功能从主要功能函数中抽离出来?如是有了下面的修正:

import time

def count_time(func):
    '''
    统计某个函数的运行时间
    '''
    start_time = time.clock()# 起始时间
    func()# 执行函数
    end_time = time.clock()# 结束时间
    print("it takes {}s to find all the olds".format(end_time - start_time))

def print_odds():
    '''
    输出0~100之间的所有奇数
    '''
    # 查找并输出所有奇数
    for i in range(100):
        if i % 2 ==1:
            print(i)

if __name__ == '__main__':
    count_time(print_odds)

上面的代码中,我们将辅助功能(记录时间)抽离出成为一个辅助函数count_time,在count_time中调用主要函数print_odds,这样做的优点是实现了解耦,使函数职责分离。但是还是有一个不符合逻辑的缺点,就是必须通过辅助函数来调用主要功能函数,这显然不合理,我们应该想办法通过调用主要功能函数来自动实现时间记录,而不是反过来。如是再次更改:

import time

def print_odds():
    '''
    输出0~100之间的所有奇数
    '''
    # 查找并输出所有奇数
    for i in range(100):
        if i % 2 ==1:
            print(i)

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func():
        start_time = time.clock()# 起始时间
        func()# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
    return improved_func

if __name__ == '__main__':
    # 调用count_time_wrapper增强函数
    print_odds = count_time_wrapper(print_odds)
    print_odds()

上面的代码中,我们通过一个闭包来增强主要功能函数print_odds,给它增加一个统计时间功能。

闭包函数:是一个函数,其参数和返回值都是函数

  • 用于增强函数功能

  • 面向切面编程(AOP)

通过增强后,我们就可以通过调用主要函数来“顺带”实现辅助功能。但是我们还是发现了一个问题,就是我们需要显示进行闭包增强,即通过print_odds = count_time_wrapper(print_odds)来实现增强,这样显得代码还是比较冗余。那么我们有什么办法“偷偷”地在调用主要函数时自动实现函数增强吗?装饰器顺应而生。

import time

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func():
        start_time = time.clock()# 起始时间
        func()# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
    return improved_func

@count_time_wrapper
def print_odds():
    '''
    输出0~100之间的所有奇数
    '''
    # 查找并输出所有奇数
    for i in range(100):
        if i % 2 ==1:
            print(i)

if __name__ == '__main__':
    # 装饰器等价于在第一次调用函数时执行以下数据
    # print_odds = count_time_wrapper(print_odds)
    print_odds()

通过装饰器进行函数增强,只是一种语法糖,本质上跟上个程序的闭包增强完全一致,只是在代码上显得更加可观。

语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用

  • 语法糖没有增加新功能,只是一种更方便的写法

  • 语法糖可以完全等价地转换为原本非语法糖的代码

装饰器在第一次调用时被装饰函数增强

  • 增强时机?第一次调用前

  • 增强次数?只增强一次,一次增强后可多次使用

装饰器进阶

装饰器的原理并不复杂,但是有两个问题必须注意。首先是返回值问题,我们看下面的代码:

import time

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func():
        start_time = time.clock()# 起始时间
        func()# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
    return improved_func

def count_odds(lim=100):
    '''
    统计0~lim之间所有奇数个数
    '''
    cnt = 0
    for i in range(lim):
        if i % 2 == 1:
            cnt += 1
    return cnt

if __name__ == "__main__":
    print('增强前:')
    print(count_odds())# 装饰前函数能正常返回,能接收参数
    print('------------------------')
    print('增强后:')
    count_odds = count_time_wrapper(count_odds)
    print(count_odds())

这段程序的主函数是统计0~lim(参数)之间所有奇数个数,装饰函数还是统计运行时间。与上面那个打印奇数的案例不同的是,这次的主函数有一个返回值cnt。我们先看一下运行结果:

增强前:
50
------------------------
增强后:
it takes 7.6e-06s to find all the olds
None

主要函数确实得到了装饰,但是那个返回值好像并没有返回出来,为什么?查看一下代码就知道了,我们在装饰时调用了一次主函数func()# 执行函数,它本身是有一个返回值的,但是我们没有将他保存下来,更没有返回。对improved_func()稍加修改:

import time

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func():
        start_time = time.clock()# 起始时间
        ret = func()# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
        return ret  # 增强函数的返回值应该是主要功能函数的返回值
    return improved_func

def count_odds(lim=100):
    '''
    统计0~lim之间所有奇数个数
    '''
    cnt = 0
    for i in range(lim):
        if i % 2 == 1:
            cnt += 1
    return cnt

if __name__ == "__main__":
    print('增强前:')
    print(count_odds())# 装饰前函数能正常返回,能接收参数
    print('------------------------')
    print('增强后:')
    count_odds = count_time_wrapper(count_odds)
    print(count_odds())

修改后运行结果如下,成功获得了返回值:

增强前:
50
------------------------
增强后:
it takes 8.7e-06s to find all the olds
50

其次是含参问题,还是上面那个程序,我们可以看到在定义主函数时是有一个参数lim的,它默认为100,我们可以在传参时给他赋其他值,但是会由此引发一个问题,我们先看下列代码:

import time

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func():
        start_time = time.clock()# 起始时间
        ret = func()# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
        return ret  # 增强函数的返回值应该是主要功能函数的返回值
    return improved_func

def count_odds(lim=100):
    '''
    统计0~lim之间所有奇数个数
    '''
    cnt = 0
    for i in range(lim):
        if i % 2 == 1:
            cnt += 1
    return cnt

if __name__ == "__main__":
    print('增强前:')
    print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
    print('------------------------')
    print('增强后:')
    count_odds = count_time_wrapper(count_odds)
    print(count_odds(lim=1000))

它的执行结果是增强后的函数报错了,错误如下:

TypeError: improved_func() got an unexpected keyword argument 'lim'

系统说improved_func()函数接收了一个不曾期待的参数lim,但是我们不禁思考为什么说是improved_func()接收到了这个lim参数,我们不是把它传给count_odds()了吗?我们不妨修改修改一下if __name__ == "__main__":下的内容:

if __name__ == "__main__":
    print('增强前:')
    print(count_odds.__name__)
    print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
    print('------------------------')
    print('增强后:')
    count_odds = count_time_wrapper(count_odds)
    print(count_odds.__name__)
    print(count_odds(lim=1000))

看一下程序运行结果(报错前的部分):

# import time...
增强前:
count_odds
500
------------------------
增强后:
improved_func

原来增强后的函数看似还是叫count_odds,但是其本质已经是improved_func了,这就解释得通为什么报错说improved_func()函数接收了参数lim,而我们本身定义的函数improved_func()并没有接收任何参数的能力,如是给它加上万能参数,问题解决。代码如下:

import time

def count_time_wrapper(func):
    '''
    闭包,用于增强函数func:给func增加统计时间的功能
    '''
    def improved_func(*args, **kwargs):
        start_time = time.clock()# 起始时间
        ret = func(*args,**kwargs)# 执行函数
        end_time = time.clock()# 结束时间
        print("it takes {}s to find all the olds".format(end_time - start_time))
        return ret  # 增强函数的返回值应该是主要功能函数的返回值
    return improved_func

def count_odds(lim=100):
    '''
    统计0~lim之间所有奇数个数
    '''
    cnt = 0
    for i in range(lim):
        if i % 2 == 1:
            cnt += 1
    return cnt

if __name__ == "__main__":
    print('增强前:')
    print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
    print('------------------------')
    print('增强后:')
    count_odds = count_time_wrapper(count_odds)
    print(count_odds(lim=1000))

运行结果如下:

# import time...
增强前:
500
------------------------
增强后:
it takes 0.0001065s to find all the olds
500

这就是装饰器必须注意的两个问题,上面的代码为了方便解释采用的闭包形式,装饰器的原理是一样的。

多层装饰器

下面通过一个面试题简单解释一下多层装饰器的使用:

def wrapper1(func1):
    print('set func1')# 在wrapper1装饰函数时输出

    def improved_func1():
        print('call func1')# 在wrapper1装饰过的函数被调用时输出
        func1()

    return improved_func1

def wrapper2(func2):
    print('set func2')# 在wrapper2装饰函数时输出

    def improved_func2():
        print('call func2')# 在wrapper2装饰过的函数被调用时输出
        func2()

    return improved_func2

@wrapper1
@wrapper2
def original_func():
    pass

if __name__ == '__main__':
    original_func()

这是一个两层的装饰器,我们先看一下运行结果:

set func2
set func1
call func1
call func2

为什么它是先装饰2再装饰1,而又先调用1再调用2?我们对下面三行代码进行解释:

@wrapper1
@wrapper2
def original_func():

等价于original_func = wrapper1(wrapper2(original_func))

original_func = wrapper1(wrapper2(original_func))又可以分解成
- 1.original_func = wrapper2(original_func)
- 2.original_func = wrapper1(original_func)

我们将if __name__ == '__main__':的内容进行修改:

if __name__ == '__main__':
    original_func = wrapper2(original_func)
    print(original_func.__name__)
    original_func = wrapper1(original_func)
    print(original_func.__name__)
    original_func()

查看一下执行结果:

set func2            # 1.装饰2前输出set func2 
improved_func2       # 2.装饰2后函数变为improved_func2
set func1            # 3.装饰1前输出set func1
improved_func1       # 4.装饰1后函数变为improved_func1
call func1           # 5.装饰1在外层,先调用
call func2           # 6.装饰2在内层,后调用

也就是说装饰器是一个穿衣脱衣的过程,后穿上的衣服在外面,先脱;先穿的衣服在里面,后脱。

最后一个tip

我们将面试题的if __name__ == '__main__':的内容稍加修改,会得到一个很重要的结论,代码如下:

if __name__ == '__main__':
    original_func()
    print('----------------')
    original_func()

运行一下:

set func2
set func1
call func1
call func2
----------------
call func1
call func2

由此可知,装饰器只在第一次被调用时被装饰函数增强,这一点非常重要。

你可能感兴趣的:(一个例子解释python,python)