Skip to content

Latest commit

 

History

History
226 lines (184 loc) · 7.48 KB

20-装饰器.md

File metadata and controls

226 lines (184 loc) · 7.48 KB

装饰器

本文主要介绍装饰器的使用,难点在于理解装饰器的工作原理。我尽量多举例子,少废话。看完本文之后,大家应该能够模仿文中范例,独立写出一个装饰器。

软件开发领域中最经典的口头禅就是“don’t repeat yourself”。 也就是说,任何时候当你的程序中存在高度重复(或者是通过剪切复制)的代码时,都应该想想是否有更好的解决方案。装饰器就是python中一种可以有效避免重复的工具。相对于其它方式,装饰器语法简单,代码可读性高。因此,装饰器在Python项目中有广泛的应用。

装饰器是做什么的

python官方文档写到:“装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。”

简单点说,装饰器是一种特殊的函数,它接受的参数中必须要有一个函数,它的返回值也必须是函数。是的,你没看错,在python中函数也可以是参数。还记得聪哥之前说过,在python中,一切都是对象。从这个角度看,函数和列表元组等没有本质上的区别。

# 给函数countdown加上装饰器timethis
@timethis
def countdown(n):
    pass

# 等价于先定义函数countdown,然后用函数timethis处理它 
def countdown(n):
    pass
countdown = timethis(countdown)

如何定义一个装饰器

装饰器本质上是函数,因而定义装饰器类似于定义一个函数。定义装饰器需要用到functools,这是一个操作函数的模块,其中的wraps方法就是用来定义装饰器的。下面是一个定义装饰器的示例:

# 引入time模块,它是一个时间显示和处理模块
import time
# 引入wraps,利用它可以制作装饰器
from functools import wraps
# 定义装饰器timethis,该装饰器的功能是记录函数运行时间
def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 函数开始运行时间
        start = time.time()
        result = func(*args, **kwargs)
        # 函数运行结束时间
        end = time.time()
        # __name__属性指的是函数的名称
        print(func.__name__, end-start)
        return result
    return wrapper

装饰器的使用方法很简单,只要在定义的函数之前加上装饰器的名称即可。

# 使用上面定义的timethis装饰器来修饰函数countdown
@timethis
def countdown(n):
    '''
    Counts down
    '''
    while n > 0:
        n -= 1
# countdown函数的作用就是将一个数不断减一,最终减至0或负数

>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown(10000000)
countdown 0.87188299392912

不难发现,函数countdown的每一次运行时间都被打印到了控制台,这个功能就是timethis装饰器提供的。

通过上面的例子,可以归纳出装饰定义的一般规则:

from functools import wraps
def 装饰器名(func):
    @wraps(func)
    def wrapper(参数):
        代码块
    return wrapper

其中:装饰器名称是自定义的,func是需要用装饰器修饰的函数名称,wrapper是被装饰器修饰后返回的新函数名称。func和wrapper都是自定义的,换成其它名称也不影响使用。代码块实现的装饰器的功能。

wraps装饰器

前面,我们定义装饰器的时候,总是引入wraps装饰器,那么使用这个装饰器有什么作用呢?请看下面的代码。

# 针对之前定义的countdown函数
>>> countdown.__name__
'countdown'
>>> countdown.__doc__
'\n\tCounts down\n\t'
>>> countdown.__annotations__
{'n': <class 'int'>}

如果我们定义timethis装饰器时不使用wraps装饰器修饰wrapper,此时countdown的属性:

>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}

很显然,__doc____annotations属性值都丢失了。所以,wraps装饰器是用来保持被修饰函数的元信息的。

带参数的装饰器

有的时候,我们不希望每个函数的运行时间都被记录下来,这个时候可以用一个参数来控制装饰器的执行。这个参数很显然是加在装饰器上的,示例代码如下:

import time
from functools import wraps
# 定义装饰器timethis,该装饰器的功能是记录函数运行时间
def timethis(arg = True):
    '''
    Decorator that reports the execution time.
    '''
    # 嵌套一层_timethis函数
    if arg:
        # 当装饰器传入的参数是True的时候记录函数运行时间
        def _timethis(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                start = time.time()
                result = func(*args, **kwargs)
                end = time.time()
                print(func.__name__, end-start)
                return result
            return wrapper
    else:
        # False的时候,直接返回原函数
        def _timethis(func):
            return func
    return _timethis

上面定义了两个_timethis装饰器,timethis装饰器的参数决定了哪一种_timethis装饰器被返回。

多个装饰器

类似于形容词修饰名词,装饰器也可以修饰函数。一个名词可以有多个形容词修饰,那么一个函数是否可以用多有装饰器修饰?答案是可以的。那么问题来了,如果有多个装饰器同时修饰一个函数,到底谁先修饰?换句话说,哪一个装饰器的代码优先被执行?

请看下面的两个装饰器:

import time
from functools import wraps
def decorator_1(func):
    print("Enter into decorator_1")
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Enter into decorator_1_wrapper")
        func(*args, **kwargs)
    return wrapper
def decorator_2(func):
    print("Enter into decorator_2")
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Enter into decorator_2_wrapper")
        func(*args, **kwargs)
    return wrapper

函数hello被这样两个装饰器修饰,之后调用hello函数:

>>> @decorator_1
··· @decorator_2
··· def hello(name):
···     '''
···     Welcome somebody
···     '''
···     print("Hello, %s" % name)
···
>>> hello("jack")
Enter into decorator_2
Enter into decorator_1
Enter into decorator_1_wrapper
Enter into decorator_2_wrapper
Hello, jack

发现装饰器的调用顺序是:先调用靠近函数名的装饰器,后调用远离函数名的装饰器,等到运行装饰器中的代码时,顺序恰好相反。最后实现函数本身的功能。

装饰器的作用

  • 引入日志

  • 函数执行时间统计

  • 预处理

  • 后处理

  • 权限校验

  • 缓存

总结

  1. 装饰器时用来修饰函数的,它可以实现优雅地复用代码,简化编程
  2. 装饰器的定义过程实质上是:引入旧函数生成新函数的过程
  3. wraps装饰器可以保留被修饰函数的元信息
  4. 装饰器可以添加参数,实现更高级的功能
  5. 定义多个装饰器,需要注意它们的调用顺序

思考

定义一个装饰器,打印出函数所有输入的参数和返回值

from functools import wraps
def print__(func):
    '''
    Decorator that print the input and output.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(*args, **kwargs)
        result = func(*args, **kwargs)
        print(result)
        return result
    return wrapper