入手装饰器

在学习装饰器前,我们先来了解两个函数概念。

函数中定义函数

在 Python 中,函数内部是可以嵌套地定义函数的。如:

def print_twice(word):
    def repeat(times):
        return word * times

    print(repeat(2))
>>> print_twice('go ')
gogo

内层函数只能在包裹它的外层函数中使用,而不能在外层函数外使用。比如上面的 repeat() 可以在 print_twice() 中使用,但是不能在 print_twice() 的外部使用。

另外,内层函数中可以使用外层函数的参数或其它变量。如上面的参数 word

函数返回函数

之前我们学习过,函数可以作为另一个函数的参数。类似的,函数的返回值也可以是一个函数。

如:

def print_words(word):
    def repeat(times):
        return word * times

    return repeat
>>> f = print_words(‘go’)
>>> f
<function print_words..repeat at 0x10befe620>

我们调用 print_words() 并用变量 f 接收其返回值,f 是个函数,是 print_words 下的 repeat 函数。

既然 f 是个函数,自然可以被调用,这也就相当于调用 repeat()

>>> f(2)
‘gogo’

扩展:我们直接调用 f(也就是 repeat())时,repeat() 内部会使用变量 word,而这个变量时定义在外层函数 print_words() 中的,却会一直伴随 repeat() 而存在,这在 Python 中叫作闭包。

装饰器是什么

好了,回到正题,来看看什么是装饰器。我们在《类进阶》章节中介绍过类方法和静态方法的定义方式,还记得吗,定义它们时需要用到 @classmethod@staticmethod,它们就是装饰器。写法为 @装饰器名称

装饰器用来增强一个现有函数的功能,并且不改变这个函数的调用方式。这种增强是非侵入式的,也就是说无需直接修改函数内部的代码,而是在函数的外部做文章。

举个例子,假设我们有这样一个函数:

def say_hello():
    print('Hello!')
>>> say_hello()
Hello!

这个函数非常简单,每次调用会输出「Hello!」,假如我们想在每次输出「Hello!」的同时附带上当前的时间,像这样:

>>> say_hello()
[ 2019-09-14 16:38:10.942802 ]
Hello!
>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!

如果想具备上面的功能,但又不想修改 say_hello() 函数的内部实现,该怎么做?

这就是装饰器的典型使用场景了——非侵入的情况下让函数具备更多的功能。

假设我们已经有了一个能满足该需求的装饰器 @time ,只要像这样来装饰 say_hello() 即可:

@time
def say_hello():
    print('Hello!')

函数的调用方式依然不变:

>>> say_hello()

当然,虽然 Python 中内置有一些装饰器,如 @classmethod@staticmethod,但并没 @time,所以我们需要自己来定义它。

自定义装饰器

我们来自定义之前所说的装饰器 @time,要求是使用它可以在函数调用时输出调用时间。

这里直接给出 @time 的实现:

import datetime    # 日期时间相关库,用于后续获取当前时间

def time(func):
    def wrapper(*args, **kw):
        print('[', datetime.datetime.now(), ']')
        return func(*args, **kw)
    return wrapper

我们暂且不关注具体的实现细节,先使用一下看看:

@time
def say_hello():
    print('Hello!')
>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!
>>> say_hello()
[ 2019-09-15 09:44:06.155869 ]
Hello!

没有问题,效果和预期相同!那这是什么原理呢?

装饰器原理

其实,

@time
def say_hello():
    print('Hello!')

等效于:

def say_hello():
    print('Hello!')

say_hello = time(say_hello)

也就是说,我们用 @time 装饰 say_hello() 时,Python 会在背后做了这样一个操作(重点):

say_hello = time(say_hello)

@time(包括所有装饰器)本质上是个以函数作为参数,并返回函数的函数。不妨回过头来观察下 @time 实现:

import datetime    # 日期时间相关库,用于后续获取当前时间

def time(func):
    def wrapper(*args, **kw):
        print('[', datetime.datetime.now(), ']')
        return func(*args, **kw)
    return wrapper

say_hello = time(say_hello) 这句代码将函数 say_hello 作为参数来调用 time()time() 将其内部定义的函数返回了出来,并替换了函数 say_hello。结合装饰器实现来看, say_hello() 其实变成了 time() 中的 wrapper()

>>> say_hello
<function time..wrapper at 0x10befea60>

那就来具体看下 wrapper()

def wrapper(*args, **kw):
    print('[', datetime.datetime.now(), ']')
    return func(*args, **kw)

wrapper() 其实也非常简单,其内部的 print('[', datetime.datetime.now(), ']') 以 [ 时间 ] 的格式将当前时间输出出来,达成了「输出函数调用时间」的目的。其中 datetime.datetime.now()用于获取当前的时间。

最后一句 return func(*args, **kw) 比较关键,这里调用函数 func() 并将其结果返回出去。func() 是什么?它就是 say_hello()。最初 say_hello 作为参数被传入 time() 中,其参数名便是 func

参数 *args**kw 是什么?还记得我们在《函数进阶》中的内容吗,*args 可以接收一切非关键字参数,而 **kw 可以接收一切关键字参数,两个结合起来一起使用就可以接收一切参数了。用在这里的作用是,接收调用 say_hello() 时的所有参数,并悉数传给 func()

稍作梳理我们就能明白,装饰器之所以能够增强一个函数的功能,其实就是将被装饰函数用新函数替换,虽然还是同一个函数名,但函数内部实现已经变了。而这个新函数的内部在添加了一些功能的后,还会调用之前被装饰的函数。这样就相当于对被装饰的函数做了非侵入的扩展。

functools.wraps 装饰器

当一个函数不被装饰器装饰时,其函数名称就是自己。如:

>>> def say_hello():
…     print(‘Hello!’)
…
>>> say_hello
<function say_hello at 0x10efbb1e0>

>>> say_hello.__name__
‘say_hello’

在解释器中直接输入 sayhello,显示其为 function say_hello。使用 `sayhello.__name,可以直接获取到其函数名称,此处显示为say_hello`。

如果我们用装饰器 @time 来修饰这个函数,那结果就不同了:

>>> @time
… def say_hello():
…     print(‘Hello!’)
…
>>> say_hello
<function time..wrapper at 0x10efbb048>

>>> say_hello.__name__
‘wrapper’

可以看到其名字信息被装饰器中的函数 wrapper 覆盖了。

是的,由于装饰器本质上是用一个新的函数来替换被装饰的函数,所以函数的元信息会被覆盖。

那有没有什么方式保留被装饰函数的元信息呢?有的,可以在定义装饰器时使用 @functools.wraps 装饰器。使用如下:

import datetime
import functools

def time(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('[', datetime.datetime.now(), ']')
        return func(*args, **kw)
    return wrapper
>>> say_hello
<function say_hello at 0x10ef5c378>

>>> say_hello.__name__
‘say_hello’

可以看到使用 @functools.wraps 后,元信息恢复如初,不留痕迹。

带参数的装饰器

既然装饰器本质上是个函数,那这个函数能不能有参数呢?答案是可以有。

举个例子,刚才我们输出的时间格式是 [ 2019-09-14 16:42:58.409742 ],如果我们想要自行指定这个格式,可以考虑用装饰器参数的形式来设置。

带时间格式的装饰器如下:

import datetime
import functools

def time(format):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print(datetime.datetime.now().strftime(format))
            return func(*args, **kw)
        return wrapper
    return decorator

可以看到,这回装饰器变成了三层函数嵌套的形式。是的,如果需要指定装饰器的参数,那么就需要在原来装饰器的基础上在再加一层函数。

wrapper() 中原本的 print('[', datetime.datetime.now(), ']') 被修改为 print(datetime.datetime.now().strftime(format)),其中的 format 便是装饰器的参数,也就是时间格式。

使用时,在装饰器 @time 后添加括号并写上参数:

@time('%Y/%m/%d %H:%M:%S')
def say_hello():
    print('Hello!')
>>> say_hello()
2019/09/15 10:00:24
Hello!

可以看到时间格式已经根据我们的设置而生效。

扩展:

'%Y/%m/%d %H:%M'datetime 包中用于指定时间格式的字符串,其中:

%Y 表示年
%m 表示月
%d 表示天
%H 表示小时
%M 表示分钟
%S 表示秒。

带参数的装饰器原理

带参数的装饰器的实现为什么要三层函数嵌套?看了下面的等效代码你就明白了!

@time('%Y/%m/%d %H:%M:%S')
def say_hello():
    print('Hello!')

等效于:

def say_hello():
    print('Hello!')

say_hello = time('%Y/%m/%d %H:%M:%S')(say_hello)

而不带参数的装饰器的等效代码是 say_hello = time(say_hello)。对比可以看出,带参数的装饰器的等效代码多了一次函数调用,通过这种方式将装饰器参数传递到内部的两层函数中,这之后便回到了不带参数的装饰器的情形。

装饰器是接受一个函数返回包装后的函数的东西。然后他要接受自己参数的话,就得在外面多套一层,time(A)返回的也是一个函数,后面接(B)就是不带参数的装饰器了

results matching ""

    No results matching ""