Python教程

上篇:Python装饰器教程(一)

三. 装饰器的概念及原理

1. 什么是装饰器?

装饰器,是一种“语法糖”,其本质上就是个函数。

2. 装饰器的作用

它是一个装饰其他函数的函数,用来为其他函数添加一些额外的功能。

3. 装饰器原则

装饰器对被装饰的函数应该是完全透明的,即

不能修改被装饰的函数的源代码

不能修改被装饰的函数的调用方式

4. 什么样的函数才是装饰器?

高阶函数 + 嵌套函数 => 装饰器

这里的高阶函数需要同时满足以下两个条件:

接收函数名作为参数 -- 可以实现在不修改被装饰函数源代码的情况下为其添加新的功能

返回内部嵌套函数的函数名 -- 可以实现不用修改函数的调用方式

5. 装饰器实现实例

再来看下上面写的实现代码:

def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

def func2():
    time.sleep(2)
    print('func2')
    
def print_run_time(f):
    def inner(*args, **kwargs):
        time_start = time.time()
        ret = f(*args, **kwargs)
        time_end = time.time()
        func_name = f.__name__
        print('%s run time is: %s' % (func_name, (time_end - time_start)))
        return ret
    return inner

对于这段代码来讲,print_run_time(f)就已经是一个装饰器函数。为了不改变原函数的调用方式,我们需要把print_run_time(f)函数的返回值重新赋值给原来的函数名:

func1 = print_run_time(func1)
func2 = print_run_time(func2)

但是我们上面说过,装饰器除了是一个函数之外,还是一个“语法糖”。“语法糖”应该是可以简化某些操作的,事实上确实是这样。上面的过程其实可以这样来写:

def print_run_time(f):
    def warpper(*args, **kwargs):
        time_start = time.time()
        ret = f(*args, **kwargs)
        time_end = time.time()
        func_name = f.__name__
        print('%s run time is: %s' % (func_name, (time_end - time_start)))
        return ret
    return wrapper

@print_run_time
def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

@print_run_time
def func2():
    time.sleep(2)
    print('func2')

是的,就是这么简单。

@print_run_time
def func1(string):
    ...

就相当于:

func1 = print_run_time(func1)

不要问为什么,因为Python解释器就是这样执行的。但是需要注意,此时print_run_time(f)函数必须定义在被修改的函数定义之前,这个很容易理解,只要捋一下代码执行过程就明白了。另外,我们把print_run_time的内部函数名改成了wrapper,这个是装饰器函数的惯用名称(当然,也可以继续使用inner或使用任意名称)。

那么,现在我们可以像原来那样调用函数了(上面的修改这对函数的调用方是完全无感知的):

func1('Decorator Test...')
func2()

输出结果:

Decorator Test...
func1
func1 run time is: 1.0006635189056396
func2
func2 run time is: 2.0003299713134766

四、 回马枪

上面的“情景模拟”部分对于print_run_time函数的改进其实还没有完成。出于内容衔接和便于对概念理解的目的,才将这个改进放到了这里。现在我们返回来,看下这个print_run_time(f)函数还有什么不足。

1. 一个新需求

现在要求在完成统计函数运行时间的基础上,如果函数运行时间超过指定的秒数则打印提示信息,且该秒数允许自定义。

存在的问题:

print_run_time(f)作为一个函数,目前对可而接收的参数限制太大--只能接受一个函数作为参数,要接收一个时间参数就必须为该函数定义新的参数。

解决思路:

这个看起来很容易解决,只要给print_run_time()函数定义一个用于接收超时时间的参数就可以了。

2. 代码初步实现

def print_run_time(f, timeout=1):
    def inner(*args, **kwargs):
        time_start = time.time()
        ret = f(*args, **kwargs)
        time_end = time.time()
        func_name = f.__name__
        run_time = time_end - time_start
        print('%s run time is: %s' % (func_name, run_time))
        if run_time > timeout:
            print('PROBLEM'.rjust(30, '>'))
        return ret
    return inner

def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

def func2():
    time.sleep(2)
    print('func2')

那么此时,我们可以这样来重新给func1和func2函数赋值并指定超时时间:

func1 = print_run_time(func1, timeout=2)
func2 = print_run_time(func2, timeout=2)

函数调用:

func1('Decorator Test...')
func2()

输出结果:

Decorator Test...
func1
func1 run time is: 1.0006680488586426
func2
func2 run time is: 2.0003504753112793
>>>>>>>>>>>>>>>>>>>>>>>PROBLEM

可以看到,我们设置的超时时间为2秒,func1函数的运行时间为1秒多,小于2秒,因此没有输出错误信息;而func2函数的运行时间为2秒多,大于2秒,因此输出了一个PROBLEM错误信息。Great! It works!

3. 代码改进

来看下这时候“装饰器语法糖”能正常工作吗?

因为print_run_time(f, timeout=1)的两个参数都是位置参数,要指定timeout的值就要先指定f的值,因此写法是这样的:

@print_run_time(func1, timeout=2)
def func1(string):
    ...

很抱歉,这种方式行不通,会报NameError: name 'func1' is not defined

结合这个例子和前面可以正常执行的代码,我们可以得出一个结论:装饰器函数只能有一个接受函数的参数;或者说以“装饰器语法糖”格式使用的函数,必须本身是一个装饰器函数或者其返回值是一个装饰器函数。

解决思路:

既然f和timeout不能同时存在,但是又必须都存在,那我们就只能在print_run_time函数中定义一个内部函数来把它们分开了。也就是说print_run_time函数需要返回一个定义在它内部的函数,且这个内部函数需要满足一个正常的装饰器函数。那么参数f必然要定义在这个要被返回内部函数中,而参数timeout只能定义在print_run_time()这个外部函数中了。此时print_run_time函数的定义应该是这样的:

import time

def print_run_time(timeout=1):
    def decorator(f):
        def wrapper(*args, **kwargs):
            time_start = time.time()
            ret = f()
            time_end = time.time()
            func_name = f.__name__
            run_time = time_end - time_start
            print('%s run time is: %s' % (func_name, run_time))
            if run_time > timeout:
                print('PROBLEM'.rjust(30, '>'))
        return wrapper
    return decorator

此时的print_run_time已经不再是一个装饰器函数,而是一个返回装饰器函数的函数。func1使用这个函数语法糖应该是这样的:

@print_run_time(timeout=2)
def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

这个过程是这样的:

decorator_func = print_run_time(timeout=2)

@decortator_func
def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

函数调用:

func1('Decorator Test...')

输出结果:

Decorator Test...
func1
func1 run time is: 1.0006623268127441

由于timeout是个默认参数(默认值为1),因此也可以不给它传递值,但是那对小括号不能省略:

@print_run_time()
def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

此时的输出结果中就会多打印一条错误信息:

Decorator Test...
func1
func1 run time is: 1.0006630420684814
>>>>>>>>>>>>>>>>>>>>>>>PROBLEM

4. 最终实现

看上去,上面已经把所有需求都实现了。不要担心,还差一小步而已。此时我们可以打印一下被装饰函数的__name__属性看下:

print(func1.__name__)
print(func2.__name__)

输出结果:

wrapper
wrapper

发现函数func1和func2的名字都变成了wrapper,当然其实还有一些其他属性都变成了print_run_name的内部函数wrapper的属性值。这显然是不合适的,比如一些依赖函数签名的代码就可能会出现错误。解决这个问题也很简单,就是给print_run_name的内部函数warpper应用一个装饰器functools.wrapper就可以了(关于该装饰器的说明请自行翻阅python文档的funcstools模块):

def print_run_time(timeout=1):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            time_start = time.time()
            ret = f(*args, **kwargs)
            time_end = time.time()
            func_name = f.__name__
            run_time = time_end - time_start
            print('%s run time is: %s' % (func_name, run_time))
            if run_time > timeout:
                print('PROBLEM'.rjust(30, '>'))
        return wrapper
    return decorator

此时再打印func1和func2的__name__属性值看看:

print(func1.__name__)
print(func2.__name__)

输出结果:

func1
func2

以上,希望对大家有所帮助,学习更多Python教程,上我学院在线教育平台。关注我学院网微信:woxueyuan_com。即可开始你的学习旅程。