装饰器提供了简单的语法来调用高阶函数。官方是这样解释的

A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.

首先,装饰器也是一个函数,它可以扩展另一个函数的行为,而不会直接修改它本身。

听起来很困惑?下面用例子来详细说明装饰器是如何工作的。

函数

理解装饰器之前,先简单了解下函数。函数是基于给定参数确定返回值。

1
2
3
4
5
def add_one(number):
    return number + 1

>>> add_one(1)
2

一般情况下,Python中的函数可能会有副作用,这里不做详细介绍。

注:函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。

第一类对象

在Python中,函数是第一类对象。意味着函数可以赋值给其它变量,也可做为其它函数的参数传入。看下以下3个例子:

1
2
3
4
5
6
7
8
def say_hello(name):
    return f"Hello {name}"

def be_friend(name):
    return f"Hi {name}, you are my best friend!"

def hi_lam(be_friend):
    return be_friend("Lam")

以上例子中,say_hello()和be_friend()都是常规函数,接受一个字符串类型的参数。而hi_lam()函数接受函数类型的参数,可以将say_hello()和be_friend()传入。

1
2
3
4
>>> hi_lam(say_hello)
Hello Lam
>>> hi_lam(be_friend)
Hi Lam, you are my best friend!

注意到hi_lam(say_hello)涉及到两个函数hi_lam()和say_hello,但调用方式不一样。 其中say_hello没有括号,意味着只是将函数的引用传入,并未执行函数。

内部函数

Python允许在函数内定义函数。这些函数称为内部函数。

1
2
3
4
5
6
7
def outer_func():
    print('calling outer function')

    def inner_func():
        print('calling inner function')

    inner_func()

调用outer_func()函数

1
2
3
>>> outer_func()
calling outer function
calling inner function

调用inner_func()函数

1
2
3
4
>>> inner_func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'inner_func' is not defined

由此可见,内部函数只有在父函数调用的时候才被定义。它仅在父函数outer_func()中有效,因此无法外部直接调用。

从函数中返回函数

Python允许将函数作为返回值。我们改造下上面的例子:

1
2
3
4
5
def outer_func():
    def inner_func():
        print('calling inner function')
    
    return inner_func	# 注意这里没有括号

测试一下

1
2
3
4
5
>>> inner_func = outer_func()
>>> inner_func
<function outer_func.<locals>.inner_func at 0x101f6a9d0>
>>> inner_func()
calling inner function

讲了很多函数的基础,接下来进入正题。

一个简单的装饰器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def my_decorator(func):
    def wrapper():
        print('call func before')
        func()
        print('call func after')
    return wrapper

def say_hi():
    print('Hi!')

say_hi = my_decorator(say_hi)

经过上面函数的讲解,看是否能猜出调用say_hi()会打印哪些数据?Try:

1
2
3
4
>>> say_hi()
call func before
Hi!
call func after

如果对打印结果有疑惑,再回过头去看看前面的例子。

say_hi现在指向的是wrapper()内部函数,因为调用my_decorator(say_hi)的时候将wrapper做为函数返回值。

再看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass
     return wrapper

def speak_loud():
    print("Ah!")
    
speak_loud = not_during_the_night(speak_loud)

当7点之前或22点之后调用speak_loud(),不会有任何打印:

1
2
>>> speak_loud()
>>> 

语法糖

上面在say_hi()函数上使用装饰器的方式有点过时了,Python可以通过简单的 @ 符号来使用装饰器。将上面的例子改造下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def my_decorator(func):
    def wrapper():
        print('call func before')
        func()
        print('call func after')
    return wrapper

@my_decorator
def say_hi():
    print('Hi!')

复用装饰器

创建一个decorators.py的文件:

1
2
3
4
5
def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

引用使用:

1
2
3
4
5
from decorators import do_twice

@do_twice
def say_hi():
    print("Hi!")

结果:

1
2
3
4

>>> say_hi()
Hi!
Hi!

带参数的装饰器

如果函数需要参数,那是否仍可以应用装饰器?

1
2
3
4
5
from decorators import do_twice

@do_twice
def greet(name):
    print(f'Hello {name}')

运行上面代码:

1
2
3
4
>>> greet('World')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

从上面报错信息得知,因为内部函数wrapper_do_twice()无须参数,但是name=“World"传入引起。可以通过调整wrapper_do_twice()接受一个参数,但是之前say_hi()函数无须参数,亦会报错。

终极解决方案是在内部函数中使用 *args and **kwargs,这样wrapper_do_twice()可接受任意可选的参数。

调整decorators.py代码:

1
2
3
4
5
def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

再次运行上面例子正常。

被装饰函数返回值

1
2
3
4
5
6
from decorators import do_twice

@do_twice
def return_sample(name):
    print('returning sample')
    return f"Hi {name}"

测试

1
2
3
4
5
>>> hi_lam = return_sample("lam")
returning sample
returning sample
>>> print(hi_lam)
None

因为wrapper_do_twice()没有返回值,所以打印返回值为None。

将decorators.py再次调整如下:

1
2
3
4
5
def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)		# 这里增加return
    return wrapper_do_twice

再次执行上述代码

1
2
3
4
>>> return_sample("lam")
returning sample
returning sample
Hi lam

functools

Python的自省能力提供了极大的灵活性和控制力,特别是在交互式shell中。通过自省能力可能了解运行时的属性。比如可以知道一个函数的名字和文档说明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
	<full help message>

自省同样适合应用在自定义的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> say_hi
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_hi.__name__
'wrapper_do_twice'

>>> help(say_hi)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()

然而,应用了装饰器后,say_hi()的指向变得有点困惑,它目前被指向do_twice()装饰器中的内部函数wrapper_do_twice()。

因此,需要使用 @functools.wraps 装饰器保留原始函数的信息。

1
2
3
4
5
6
7
8
9
# decorators.py
import functools		# add import

def do_twice(func):
    @functools.wraps(func)	# add functools decorator
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

再次运行上面代码正常指向。

到目前为止装饰器的基本概念已经基本了解,来看下一些实际应用的例子。

应用

timer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 函数运行前的时间
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 函数运行后的时间
        run_time = end_time - start_time    # 耗时
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def run_a_long_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

timer()装饰器用于打印被装饰函数运行时间,Try下:

1
2
3
4
5
>>> run_a_long_time(1)
Finished 'run_a_long_time' in 0.0010 secs

>>> run_a_long_time(999)
Finished 'run_a_long_time' in 0.2330 secs

debug

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import functools

def debug(func):
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # args参数列表
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # kwargs参数列表
        signature = ", ".join(args_repr + kwargs_repr)           # 组装签名
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 打印返回值
        return value
    return wrapper_debug

以上的 @debug 装饰器会把调用函数的参数和返回值全部打印出来。

1
2
3
4
5
6
@debug
def test_debug(name, age=None):
    if age is None:
        return f"Hi {name}!"
    else:
        return f"Hi {name}! You are {age} years old!"

运行函数

1
2
3
4
5
6
7
8
9
>>> test_debug('lam')
Calling test_debug('lam')
'test_debug' returned 'Hi lam!'
'Hi lam!'

>>> test_debug('lam', age=100)
Calling test_debug('lam', age=100)
'test_debug' returned 'Hi lam! You are 100 years old!'
'Hi lam! You are 100 years old!'

高级装饰器

到目前为止,我们已经大致了解什么是装饰器,以及它们是如何动作的。也学会了创建简单的装饰器。接下来我们要学习装饰器的一些高级特性。

类装饰器

在类上使用装饰器有两种方式:

1. 应用于类的方法上

我们使用上面写的两个例子来装饰类方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from decorators import debug, timer

class TimeWaster:
    @debug
    def __init__(self, max_num):
        self.max_num = max_num

    @timer
    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])

执行代码

1
2
3
4
5
6
>>> tw = TimeWaster(1000)
Calling __init__(<time_waster.TimeWaster object at 0x7efccce03908>, 1000)
'__init__' returned None

>>> tw.waste_time(999)
Finished 'waste_time' in 0.3376 secs

2. 直接应用于类上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from decorators import timer

@timer
class TimeWaster:
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])

该装饰器只应用于类,不会应用到函数上。在这里,@timer 只计算初始化类的时间。

1
2
3
4
5
>>> tw = TimeWaster(1000)
Finished 'TimeWaster' in 0.0000 secs

>>> tw.waste_time(999)
>>> 

嵌套装饰器

装饰器支持嵌套应用在函数上。

1
2
3
4
5
6
from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")

执行代码,注意打印顺序

1
2
3
4
5
>>> greet('lam')
Calling greet('lam')
Hello lam
Hello lam
'greet' returned None

@debug调用@do_twice,@do_twice调用greet(),类似于

1
>>> debug(do_twice(greet()))

把嵌套的装饰器调整下顺序

1
2
3
4
5
6
from decorators import debug, do_twice

@do_twice
@debug
def greet(name):
    print(f"Hello {name}")

再次执行代码

1
2
3
4
5
6
7
>>> greet("lam")
Calling greet('lam')
Hello lam
'greet' returned None
Calling greet('lam')
Hello lam
'greet' returned None

带参数的装饰器

我们把@do_twice改造为支持输入参数的@repeat(times)装饰器。参数times为repeat次数

1
2
3
4
5
6
7
8
9
def repeat(times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

测试下

1
2
3
@repeat(times =4)
def greet(name):
    print(f"Hello {name}")
1
2
3
4
5
>>> greet('lam')
Hello lam
Hello lam
Hello lam
Hello lam

未完,待续…