浅入深出 Python 装饰器 【超详细内容+丰富示例代码】

当我们使用类似 flask 框架的时候,一定接触过这样的代码:

@app.route('/')
def index():
	return '<h1>Hello World!</h1>'

这里的 @app.route() 就是修饰器的语法糖。对于 Python 的修饰器,我相信有很多人都向我一样,知道它的存在,大概知道它的用法,但却又不太深入的理解。

这篇文章是我结合多篇教程、技术书籍加以总结归纳,从浅入深的去了解 Python 修饰器。本文将从以下要来研究、分析及实践 Python 修饰器的内涵:

基础篇:

  • 装饰器的实质原理 —— 函数中调用函数

  • @ 语法糖的使用

  • 使用带参数、自定义参数的装饰器

  • 利用 @functools.wrap() 保留原函数的元信息

  • 类装饰器

  • 多个装饰器的嵌套

进阶:

  • Python 程序是如何计算装饰器的语法

  • 从装饰器看闭包函数

运用:

  • 身份验证

  • 日志

  • 缓存

通过上述的内容,应能能对 Python 的装饰器有更深入的理解。本文借鉴了若干优秀的教程和书籍,文末会有响应的参考资料链接,对应文中参考借鉴的代码,也会有响应的说明。本文尽可能使用简单、容易理解的语言讲述,但鉴于本人水平局限,可能会有阐述不清或者有疏漏的地方,欢迎交流指正。望本文能对你有所帮助。

基础篇

装饰器的实质

先看一段代码:

def sayhi_to(name):
	print('Hi, %s' % name)

say = sayhi_to

say('speculatecat')

>> 
Hi, speculatecat

我们首先定义一个 sayhi_to 的函数,然后将 sayhi_to 赋值给 say ,我们最后可以通过 say 调用,实际输出和 sayhi_to 的结果一样。这是 Python 的一个特性,在 Python 中,函数也是对象

那么,我们就可以把函数本身当作参数传给函数。这样的说法可能有些绕口,我们接着看代码:

def sayhello_to(name):
	print('Hello, %s' %s)

def talk_to_speculatecat(func):
	func('speculatecat')

talk_to_speculatecat(sayhi_to)
>>
Hi, speculatecat
---------------------------------
talk_to_speculatecat(sayhello_to)
>>
Hello, speculatecat

可以看到,我们定义两个打招呼函数: sayhi_tosayhello_to ,然后将这两个函数作为参数传给 talk_to_speculatecat ,就可以得到不同的输出。

除了函数对象可以作为参数传递,函数本身还可以嵌套函数,请看下面的代码:

def meet_speculatecat(action):
	print('Meet speculatecet!\n')
	def talk_to_speculatecat(action):
		action('speculatecat')
	return talk_to_speculatecat(action)

meet_speculatecat(sayhello_to)
>>
Meet speculatecat!

Hello speculatecat

上面的代码功能也很简单,就是调用 meet_speculatecat 函数,会先打印一个看到 speculatecat,然后再调用 talk 功能。这里就是演示了 Python 怎样在函数内调用函数的。

这可能会产生疑问,为什么函数的结尾要使用 return talk_to_speculatecat(action) ?术语上,这种将函数对象作为函数返回值的操作,叫做闭包,关于闭包的详细,后文会有专门篇幅讲述。这里可以简单的这样理解:函数声明之后需要调用才会生效,可以参考下面的代码,其效果与上面的代码相同:

def meet_speculatecat2(action):
	print('Meet speculatecat, again!')
	def talk(action):
		action('speculatecat')
	talk(action)

meet_speculatecat2(sayhi_to)
>>
Meet speculatecat, again!
Hi, speculatecat

到目前为止,我们尝试并演示了 Python 函数的四种特性:

  • 函数也是对象,函数对象可以赋值到一个变量上,并且可以用过调用这个变量来调用函数

  • 函数对象可以作为参数传入另外一个函数

  • 函数的内部可以再定义函数

  • 函数对象本身也可以作为函数的返回值,将函数对象作为函数返回值的操作,叫做闭包

我们了解了上面的特性之后,我们就可以来编写我们的第一个装饰器:

def deco(func):
	def wrapper():
		print('wrapper of decorator')
		func()
	return wrapper

def sayHello():
	print('Hello speculatecat!')

sayHello = deco(sayHello)
sayHello()
>>
wrapper of decorator
Hello speculatecat!

上面的代码就是最基本的装饰器。这里的 sayHello 会被 wrapper() 调用,而最外层的 deco 就是一个装饰器,它可以改变 sayHello 的最终行为,但又不需要对 sayHello 函数本身进行修改。

@ 语法糖的使用

在我们的实际使用用,我们不用将装饰器再赋值给一个变量再调用这么麻烦。Python 给我们提供了 @decorator 的语法糖,我们还是沿用上面的代码:

def deco(func):
	def wrapper():
		print('wrapper of decorator')
		func()
	return wrapper

@deco
def sayHello():
	print('Hello speculatecat!')

sayHello()
>>
wrapper of decorator
Hello speculatecat

我们通过 @ + 装饰器的函数名,然后接着写我们需要用到装饰器的函数,即可。上面的代码运行结果和我们之前没有使用 @语法糖 的代码运行结果一样。

用带参数、自定义参数的装饰器

上面的代码例子我们的函数并没有使用参数,但是实际上我们的装饰器是可以带参数的。我们接着在上面代码功能的基础上加上传入参数,具体代码如下:

def deco(func):
	def wrapper(name):
		print('wrapper of decorator')
		func(name)
	return wrapper

@deco
def sayHello_to(name):
	print('Hello %s' % name)

sayHello_to('speculatecat')
>>
wrapper of decorator
Hello speculatecat

如上所示例,如果想传递参数,我只需要在内装饰器的内部函数 wrapper 添加一个参数,并且在 func 调用时加上参数即可。这里是只有一个参数的使用方法,如果我们被装饰的函数有多个参数呢?具体代码参考下面代码:

def mult_deco(func):
	def wrapper(*args, **kwargs):
		print('wrapper of mult decorator')
		func(*args, **kwargs)
	return wrapper

@mult_deco
def send_message(name, message):
	print('Send message: %s to %s' % (message, name))

send_message('speculatecat', 'How are you?')
>>
wrapper of mult decorator
Send message: How are you? to speculatecat

可以看到,使用多个参数的方法和上面单一参数的用法类似,不过是把参数换成 *args, **kwargs 表示接受任意数量和类型的参数形式。

这两个谈到参数的示例,都是在被修饰函数上做文章,但是回想以下文章开头,我们提及到的 flask 框架,那种实现是 @app.route('/') ,它的参数是在装饰器上传递的,以下将演示以下如何使用自定义参数的装饰器。

这里我们仿照 flask 的路由来演示使用自定义参数装饰器,但是由于篇幅,我们不会实现完整的功能,我们仅仅实现使用修饰器将路径绑定到一个 PATH 变量当中。具体代码如下:

Path = dict()

def route(path):
    def deco(func):
        if path not in Path:
                Path[path] = func
        def wrapper():
            print('Route %s' % path)
            func()
        return wrapper
    return deco

@route('/')
def home():
    print('home pages')

@route('/admin')
def admin():
    print('admin pages')

def browser(path):
    if path not in Path:
        print('404 - Not found pages')
    else:
        return Path[path]()


if __name__ == '__main__':
    print(Path)
    browser('/')
    browser('/admin')
	browser('user')

>>
{'/': <function home at 0x10105a8b0>, '/admin': <function admin at 0x10105a9d0>}
home pages
admin pages
404 - Not found pages

上面的代码是先定义一个 route 的修饰器,功能是将不存在与路径字典中的路径及其函数绑定到 Path 字典中。然后就是定义两个显示函数 adminhome ,最后是一个 browser 函数模拟我们正常访问网页。然后我们运行程序,就可以看到 browser 函数会根据不同的路径显示不同的内容,当接受到一个不存在的路径,就会返回一个 404 找不到页面的输出。

这里的代码会有一个 trick,就是代码中,我们并没有执行一个绑定动作的操作,甚至没有调用 home admin 或者 route ,为什么我们的 Path 中已经会绑定了 /admin 呢?这个是 Python 是如何计算装饰器的问题,我们会在后面有篇幅专门针对这一点说明。

利用 @functools.wrap() 保留原函数的元信息

我们在上面的例子中使用的修饰器,有个缺点,就是被修饰函数的 __name__ __doc__ 都会被覆盖。请看下面的例子:

def deco(func):
    def wrapper(name):
        print('wrapper of decorator')
        func(name)
    return wrapper

@deco
def sayHello_to(name):
    print('Hello %s' % name)

print(sayHello_to.__name__)

>>
wrapper

可以看到,我们的 sayHello 函数的 __name__ 已经被 wrapper 取代了。如果我想保留原函数的元信息,我们可以用 @functools.wrap 这个内置装饰器。具体代码如下:

import functools

def deco(func):
    @functools.wraps(func)
    def wrapper(name):
        print('wrapper of decorator')
        func(name)
    return wrapper

@deco
def sayHello_to(name):
    print('Hello %s' % name)

print(sayHello_to.__name__)

>>
sayHello_to

我们可以从代码的结果看到,使用了 @functools.wraps 内置装饰器后,sayHello_to 的元信息已经被完整复制了。

类装饰器

装饰器除了可以用在函数上,还可以用在类上。类装饰器是要是利用了 __call__() ,每当调用类实例,__call__() 就会被执行。下面看代码:

class Count:
    def __init__(self, func):
        print('execute __init__')
        self.func = func
        self.num_calls = 0
    
    def __call__(self, *args, **kwargs):
        print('execute __call__')
        self.num_calls += 1
        print('num of calls is : %s' % self.num_calls)
        return self.func(*args, **kwargs)
    
    def print_num_call(self):
        self.func()
        print('print num of calls is : %s' % self.num_calls)
        self.num_calls += 1

@Count
def example():
    print('Hello example func')

def example2():
    print('Hello example func2')


if __name__ == "__main__":
    print('*' * 40 )
    print('Script running...')
    example()
    c = Count(example2)
    c.print_num_call()
    example()
    c.print_num_call()

>>
execute __init__
****************************************
Script running...
execute __call__
num of calls is : 1
Hello example func
execute __init__
Hello example func2
print num of calls is : 0
execute __call__
num of calls is : 2
Hello example func
Hello example func2
print num of calls is : 1

使用类装饰器时,有一个 trick,就是被装饰代码调用是,会调用 __call__ ,但是类并不会被实例化。可以从代码中看到,当我们第一次调用 example() 后,再创建一个 Count 对象 c ,然后调用 c 的打印计数函数,这里会看到实例化后的 c 计数为 0 。然后接着再调用一次 example() ,可以看到计数会增加。这就印证了被装饰函数不会实例化类,而是直接执行调用了 __call__ 来修改类变量 num_calls

多个装饰器的嵌套

上面的例子都是使用单独一个装饰器,装饰器可以多个叠加使用,请看以下例子:

import functools

def deco1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('running deco1...')
        func(*args, **kwargs)
    return wrapper

def deco2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('running deco2...')
    return wrapper

@deco1
@deco2
def say_hello_to(name):
    print('Hello, %s' % name)

say_hello_to('speculatecat')

>>
running deco1...
Hello, speculatecat
running deco2...

使用多个装饰器,会被按顺序依次应用到被装饰的函数上,上面的代码实质等价于 say_hello_to = deco1(deco2(say_hello_to)) ,所以可以看到输出结果中 wraps 的内容会依次出现,且 say_hello_to 只被执行了一次。

进阶篇

上面基础篇我们介绍了装饰器的最基本使用方法,包括了介绍了如何使用 @ 语法糖,如何使用带参数装饰器,还介绍了用 @functools.wraps 来保留被袖装饰函数的元信息,还有不是很常用的类装饰器以及多个装饰器嵌套使用。

本篇进阶篇主要来介绍以下 Python 是如何计算装饰器的以及更进一步了解闭包函数的特性。

Python 装饰器的两大特性

我们在之前的篇幅中,就已经知道了,Python 装饰器的第一大特性——能把被装饰的函数替换成其他函数。

但是,我们在上面模仿 flask 的路由示例中,代码中并没有主动调用,但是 Path 中就已经自动绑定好路径的显示内容的函数,这个就是 Python 装饰器的第二个特性—— 装饰器在加载模块时就会被立即执行。

为了方便理解,请看下面代码:

registry = []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

@register
def func1():
    print('This is func1()!')

@register
def func2():
    print('This is func2()')

def func_no_deco():
    print('This is func without decoator!')

def main():
    print('*' * 10 + 'Main Running!' + '*' * 10)
    print('registry -> ', register)
    func1()
    func2()
    func_no_deco()


if __name__ == '__main__':
    main()

>>
running register(<function func1 at 0x105369820>)
running register(<function func2 at 0x1053698b0>)
**********Main Running!**********
registry ->  <function register at 0x1052cc040>
This is func1()!
This is func2()
This is func without decoator!

我们可以从上面的代码中看到,在程序运行的开始,装饰器就会先运行,所以输出中就可以看到在打印出 **********Main Running!********** 之前,register 就被已经被运行。这就解释了为什么我们在之前的仿写路由的示例用,Path 中的内中能被自动绑定了。

这就是 Python 装饰器的第二个特性:饰器在加载模块时就会被立即执行

从装饰器再回到闭包

在我们的基础篇示例中,就有提到过闭包。由于我们的装饰器通常都会有嵌套函数的出现,因此闭包的问题也会一直伴随左右。相信初学的朋友都听过闭包,但是却对其了解不深。以下将尝试以一个比较简单的方式来尝试介绍以下闭包的内涵。由于本人水平有限,因此下面内容会借鉴了《流畅的Python》一书的代码示例,这本书是一本非常优秀的Python技术书籍,它里面深入讲解了很多平时日常使用 Python 不怎么会深究但确又很关键的内容和知识点,但是该书的内容可能对初学者不那么友好,但如果对 Python 有一定了解的读者,读这本书应该能得到很大的收获。

作用域

在开始讨论闭包之前,先来看一下函数的变量作用域。我们先来看代码:

def func_print_a(a):
    print(a)
    print(b)

func_print_a(42)

>>
42
Traceback (most recent call last):
  File "...", line 5, in <module>
    func_print_a(42)
  File "...", line 3, in func_print_a
    print(b)
NameError: name 'b' is not defined

这段代码很简单,就是打印输入的变量 a ,因为没有定义 b 变量,所以会报错。这里如果我们在函数之外定义一个 b 变量,那结果会怎样呢?

def func_print_a(a):
    print(a)
    print(b)


b = 0
func_print_a(42)

>>
42
0

我们可以看到,我们在函数在外定义了一个 b 变量,函数内部没有 b 变量,但函数能自动获取函数外的全局变量 b 。接下来我们再看一个例子:

def func_print_a(a):
    print(a)
    print(b)
	b = 100

b = 0
func_print_a(42)

>>
42
Traceback (most recent call last):
  File "...", line 7, in <module>
    func_print_a(42)
  File "...", line 3, in func_print_a
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

在之前的 func_print_a 函数末尾加上一句给 b 赋值 100,再次调用函数,发现我们的函数又再次报错了。这是因为当函数内部声明了 b 变量,函数就会从函数的内部(局部变量)中获取,而不会再获取全局变量,但是因为我们的 print 语句在 b 赋值之前,所以就会看到上面代码的报错。如果我们想实现先打印全局变量,再对这个变量进行从新赋值,操作也很简单,只要加上 global 关键字即可。

def func_print_a(a):
    global b
    print(a)
    print(b)
    b = 100

b = 0
func_print_a(42)
print(b)

>>
42
0
100

到这里,关于 Python 函数的作用域问题,我们大概已经了解。如果想更加深入的了解这个课题,可以参考《麻省理工 MIT 6.0001 计算机导论》这个视频公开课,其中第四课后半部分就又详细讲解函数作用域的问题 (课程中副标题为:Scope Detials),里面有非常好的图文并茂的讲解。

闭包的使用和进阶

闭包函数在一定程度上,可以实现某些类实现的功能。以下将以《流畅的Python》中的计算移动平均值的闭包实现方法来演示,该书中还演示的面向对象类的实现方法,但本文就不再列举,感兴趣的可以从该书中了解。

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    
    return averager

avg = make_averager()
print(avg(50))
print(avg(100))
print(avg(200))

>>
50.0
75.0
116.66666666666667

可以看到,我们三次调用 avg , 我们新加入的值都能保存在 series 中,为什么可以这样呢?正常函数调用完,函数内的变量不是应该自动销毁了吗?

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    
    return averager

avg = make_averager()
print(avg.__code__.co_varnames) #1
print(avg.__code__.co_freevars) #2
print(avg.__closure__) #3
print(dir(avg.__closure__[0])) #4

print(avg.__closure__[0].cell_contents) #5
avg(50)
print(avg.__closure__[0].cell_contents) #6
avg(100)
print(avg.__closure__[0].cell_contents) #7

>>
('new_value', 'total') #1
('series',) #2
'__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents'] #3
(<cell at 0x100e87eb0: list object at 0x100e2c7c0>,) #4
[] #5
[50] #6
[50, 100] #7

从上面的代码中我们可以得知,我们可以从函数的 __code__ 属性中,保存了局部变量 new_value total 以及*变量 series 的名称。同时,在 __closure__ 中绑定了对应 __code__.co_freevars 的值。当我们每次调用 avg() 的时候,new_value 都会把新值添加到 series 这个*变量中,而 series 这个*变量的值实际会存放在 __closure__[0].cell_contents 中。

那么我们能不能把闭包当作是类一样用呢?答案是可以,但也不完全可以。请看下面的代码:

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    
    return averager

avg = make_averager()
print(avg(50))
print(avg(100))
print(avg(200))

>>
Traceback (most recent call last):
  File "...", line 13, in <module>
    print(avg(50))
  File "...", line 6, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

为什么刚才能正常运行的代码,现在只是修改了计算方式,就会报错呢?我们像之前代码一样,来看一下现在函数绑定的局部变量和*变量的情况:

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    
    return averager

avg = make_averager()

print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
print(avg.__closure__)

>>
('new_value', 'count', 'total')
()
None

我们可以通过获取 __code____closure__ 属性来查看变量的情况。从输出的结果我们可以得知,这一次我们的函数中 counttotal 并不是*变量,而是变成了局部变量,所以很显然,运行代码就会出现报错。

造成这个问题的原因,是因为我们在先前的代码中,使用的列表是可变类型对象,而在 Python 中,数字(int float),字符(str)、元组(tupe)这些属于不可变对象。使用了不可变对象,Python 就会将其创建为局部变量,而不是*变量,自然就不会保存zai闭包当中。

如果非要实现上面的功能,怎么办呢?办法也很简单,通过 nonlocal 关键字就可以。具体代码如下:

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    
    return averager

avg = make_averager()
print(avg(50)) #1
print(avg(100)) #2
print(avg(200)) #3
print(avg.__code__.co_varnames) #4
print(avg.__code__.co_freevars) #5
print(avg.__closure__) #6
print(avg.__closure__[0].cell_contents) #7
print(avg.__closure__[1].cell_contents) #8

>>
50.0 #1
75.0 #2
116.66666666666667 #3
('new_value',) #4
('count', 'total') #5
(<cell at 0x104887ee0: int object at 0x10460e970>, <cell at 0x104887eb0: int object at 0x1046b6850>) #6
3 #7
350 #8

我们加入关键字 nonlocal 之后,变量 counttotal 就成为了*变量,在 __closure__ 属性中也对应出现的这两个变量的值,函数功能也可以正常实现。

运用篇

经过了上面基础篇和进阶篇的介绍,相信我们对装饰器已经有一定的了解,接下来我们再通过几个实际的案例代码来巩固以下装饰器的运用。

身份验证

下面以一个简单的管理员获取信息的代码为例演示如何使用装饰器完成权限管理的功能。需要说明的是,为了代码的简单易懂,演示的代码并不严谨,只是展示了如何通过装饰器根据不同的用户权限进行操作。代码如下:

import functools

def auth_admin(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        role = args[0]['role']
        if role == 'admin':
            return func(*args, **kwargs)
        else:
            raise Exception('Permission Denied')
    return wrapper
    

@auth_admin
def get_admin_message(user):
    print('Get Message from:Username: %s, Role: %s' % (user['name'], user['role']))


if __name__ == "__main__":
    user = {'role': 'admin', 'name': 'speculatecat'}
    user_nomal = {'role': 'user', 'name': 'nomal_user'}
    get_admin_message(user)
    get_admin_message(user_nomal)

>>
Get Message from:Username: speculatecat, Role: admin
Traceback (most recent call last):
  File "...", line 23, in <module>
    get_admin_message(user_nomal)
  File "...", line 10, in wrapper
    raise Exception('Permission Denied')
Exception: Permission Denied

我们在示例代码中,在验证权限的装饰器中硬编码了 role 参数,实际代码中不会这样做,不过这里为了演示方便,就这样设计。可以看到,我们使用了装饰器后,并不需要在 get_admin_message 函数内再做任何的用户权限逻辑判断,这样让我们的代码变得更加简洁易懂。

日志

除了权限验证,日志记录也是装饰器的一个常用之处,以下将演示一个记录函数运行时间的日志记录功能。代码如下:

import time
import functools

def log(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('(%0.8fs) %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

@log
def test_func(sleep_time):
    time.sleep(sleep_time)
    return True

test_func(3.14)
>>
corator/article_009_log_demo.py
(3.14534538s) test_func(3.14) -> True

上面的代码中装饰器完成了获取被装饰函数的函数名、传入参数、返回值等信息,并且计算了函数运行的总时间,并且最终格式化输出结果。通过这个日志的装饰器,我们只需要在需要日志的函数上利用 @ 语法糖即可。

缓存

这一部分的最后,我们来介绍以下 lru_cache 的运用。在介绍之前,我们先来看以下我们电脑运行一个计算斐波那契数所需要的时间:

import time
import functools

def log(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('(%0.8fs) %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

@log
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
    print(fibonacci(25))

>>
0.00000013s) fibonacci(0) -> 0
(0.00000013s) fibonacci(1) -> 1
(0.00000412s) fibonacci(2) -> 1
(0.00000013s) fibonacci(1) -> 1
(0.00000013s) fibonacci(0) -> 0
(0.00000013s) fibonacci(1) -> 1
(0.00000371s) fibonacci(2) -> 1
(0.00000750s) fibonacci(3) -> 2
(0.00001504s) fibonacci(4) -> 3
(0.00000012s) fibonacci(1) -> 1
(0.00000012s) fibonacci(0) -> 0
(0.00000013s) fibonacci(1) -> 1
...
(0.01913079s) fibonacci(15) -> 610
(0.02132400s) fibonacci(16) -> 987
(0.04015479s) fibonacci(17) -> 1597
(0.06254508s) fibonacci(18) -> 2584
(0.08707908s) fibonacci(19) -> 4181
(0.16693542s) fibonacci(20) -> 6765
(0.27160862s) fibonacci(21) -> 10946
(0.41941350s) fibonacci(22) -> 17711
(0.71603742s) fibonacci(23) -> 28657
(1.20284050s) fibonacci(24) -> 46368
(2.04485879s) fibonacci(25) -> 75025
75025

可以看到,计算完成需要时间为2秒多,而且输出的过程中,能看到很多重复的调用。这时,我们的 lru_cache 就可以用来实现缓存,从而提高性能。

@functools.lru_cache()
@log
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

>>
(0.00000042s) fibonacci(1) -> 1
(0.00000033s) fibonacci(0) -> 0
(0.00000892s) fibonacci(2) -> 1
(0.00005983s) fibonacci(3) -> 2
(0.00000067s) fibonacci(4) -> 3
(0.00006875s) fibonacci(5) -> 5
(0.00000058s) fibonacci(6) -> 8
(0.00007733s) fibonacci(7) -> 13
(0.00000046s) fibonacci(8) -> 21
(0.00008596s) fibonacci(9) -> 34
(0.00000050s) fibonacci(10) -> 55
(0.00009442s) fibonacci(11) -> 89
(0.00000046s) fibonacci(12) -> 144
(0.00010279s) fibonacci(13) -> 233
(0.00000054s) fibonacci(14) -> 377
(0.00011092s) fibonacci(15) -> 610
(0.00000046s) fibonacci(16) -> 987
(0.00012158s) fibonacci(17) -> 1597
(0.00000046s) fibonacci(18) -> 2584
(0.00012967s) fibonacci(19) -> 4181
(0.00000046s) fibonacci(20) -> 6765
(0.00013713s) fibonacci(21) -> 10946
(0.00000046s) fibonacci(22) -> 17711
(0.00014613s) fibonacci(23) -> 28657
(0.00000046s) fibonacci(24) -> 46368
(0.00015433s) fibonacci(25) -> 75025
75025

其他的代码不变,我只需要添加上 @functools.lru_cache() 即可。从输出结果上看,运行完成时间只需要0.00015秒,比刚才的代码快了很多。而且这次的代码并不会出现重复的调用。

总结

本文从基础、进阶以及运用三个纬度介绍了Python装饰器。虽然内容比较长,但应该是比较全面深入的介绍了装饰器的内容。本文参考了许多优秀的教程和文章,其中《极客时间-Python核心技术与实践》是非常优秀的入门教程,这个系列中的内容通熟易懂且又具有相当的启发性,无论是新手还是有一定编程经验的程序员都值得一读。另外一个重要的参考资料是 《流畅的Python》,这本书我已经买了很久,但是开始的时候因为根基不扎实,这本书的内容又较为深入细节和原理,导致很多次读了没有多久就放弃了。最近再次拿起来阅读,发现很多内容犹如瑰宝。所以才有结合一些基础以及这本书的内容来深入学习。

本文即使技术分享文章,也是我的学习笔记。可能因为自身水平有限,认知不足,会有错漏之处,还望读者能多多包涵谅解。同时还欢迎大家指正错误及一起深入探讨。

开始以为完成本文还算比较简单,但实际上从开始写作到完成文章,耗费了大量的时间,深感创作不易,希望文章能带给你帮助及启发。如果你觉得文章对你有用,请不要吝啬点赞、收藏、关注、分享。你的举手之劳是我继续创作的动力源泉。

参考资料

【MIT 6.0001】

【流畅的Python ISBN: 9787115454157】

【极客时间-Python核心技术与实践】

【深入理解Python特性 ISBN: 9787115511546】

上一篇:分治 - 1 (C++描述)


下一篇:图解青蛙跳台阶和汉诺塔问题(C语言+Java语言实现)