Python学习笔记24:函数装饰器进阶
函数装饰器,也称为函数修饰器或函数修饰符。
之前在Python学习笔记11:函数修饰符和Python学习笔记12:函数修饰符的应用中我们有所讨论,但学习的并不深入。
在本文中我们就一些函数装饰器的进阶知识进行探讨。
方便起见,这里使用《Fluent Python》中的称呼,统一称为函数装饰器。
装饰器基本概念
在之前的文章中我们讨论过,装饰器就是一种设计模式,其目的是为了给一个功能附加上其它功能。具体到Python的函数装饰器,顾名思义,就是给指定函数附加上其它功能,让其“改头换面”。
那么解释器是何时开始执行函数装饰器对目标函数进行“装饰”的呢?
何时执行装饰器
这里用一个注册装饰器进行说明。
注册装饰器
registed = []
def register(func):
registed.append(func)
print(str(func),"is registed")
return func
@register
def test1():
print('this is test1 function')
@register
def test2():
print('this is test2 function')
print("main function begin")
print(registed)
test1()
test2()
# <function test1 at 0x000002196EF73A60> is registed
# <function test2 at 0x000002196EF73B80> is registed
# main function begin
# [<function test1 at 0x000002196EF73A60>, <function test2 at 0x000002196EF73B80>]
# this is test1 function
# this is test2 function
我们可以看到,Python解释器调用函数装饰器对目标函数处理是在脚本的程序执行之前,理所应当地,也在被装饰的目标函数被调用之前。
事实上,在函数装饰器后紧跟着的目标函数完成定义后,Python解释器就会执行装饰器。
这里的“注册装饰器”极为简单,对目标函数没有做任何修改直接返回,和我们之前说过的装饰器模式定义不符。事实上,装饰器不一定会附加功能后返回新的函数,虽然大多数装饰器都是那样的功能,但也会有这里展示的这种"简化装饰器"。
其实我们完全可以不用@
符号,不依赖Python解释器,手动完成调用装饰器处理目标函数的工作。
registed = []
def register(func):
registed.append(func)
print(str(func),"is registed")
return func
def test1():
print('this is test1 function')
test1 = register(test1)
def test2():
print('this is test2 function')
test2 = register(test2)
print("main function begin")
print(registed)
test1()
test2()
# <function test1 at 0x000002196EF73A60> is registed
# <function test2 at 0x000002196EF73B80> is registed
# main function begin
# [<function test1 at 0x000002196EF73A60>, <function test2 at 0x000002196EF73B80>]
# this is test1 function
# this is test2 function
结果完全一致。
事实上@register(test1)
和test1=register(test1)
的效果是完全相同的,这也正式Python解释器的工作。
现在是不是对函数装饰器有了更深一层的认识?
使用装饰器改写“策略模式”
在Python学习笔记23:Python设计模式中我们介绍了如何用Python的方式实现策略模式,其中提到最佳策略的Python实现。
在那篇文章中我们使用了内省的方式来实现“自动注册”策略,除此之外,我们还可以用刚介绍过的装饰器来实现。
新建一个模块promo_register.py
,实现一个简单的注册装饰器。
from .Order import Order
promotions = []
def regist_promo(func):
promotions.append(func)
return func
在促销策略模块promotion_func.py
中使用@
符号进行注册,并实现一个最佳策略方法:
@regist_promo
def large_order_promo(order: Order):
if len(order.cart.items) >= 10:
return order.total()*0.07
else:
return 0
def best_promo(order: Order):
if len(promotions) == 0:
return 0
return max(promo(order) for promo in promotions)
这里有个坑,我一开始是把
best_promo
和注册装饰器写在一个模块里的,导致测试程序不需要导入promotion_func.py
,自然也就不会把促销策略注册到promotions
变量内,怎么试也不会使用促销。要避免这种情况出现,就要把最佳策略写在promotion_func.py
中。这里是不能在
promo_register.py
中加入import .promotion_func
来避免此问题的,因为会构成循环引用。我有点怀念PHP的require_once
了,怎么循环引用都没事。
测试代码相关修改这里就不做演示了,我把修改后的整体工程代码上传到百度云:
链接: https://pan.baidu.com/s/1qLv8dlYAL_6dokbBqIwG4Q
提取码: irfj
在继续深入了解函数装饰器前,我们先要讨论一个概念:闭包。
闭包
我记得Java中也有闭包的概念,简单地说就是一个当前作用域范围之外的变量在这个作用域中的可见性。
老实说我在PHP中很少细究这其中的差异,毕竟很少写像Java中的匿名类。
全局变量与局部变量
我们先看下面的例子:
numbers = []
def per(num: int) -> float:
numbers.append(num)
return sum(numbers)/len(numbers)
print(per(1))
print(per(5))
print(per(10))
# 1.0
# 3.0
# 5.333333333333333
per
函数接受一个int值,将其加入全局变量numbers
后再求numbers中的所有元素的平均值并返回。
看起来运行的不错,但这里面有一个细节性的问题:我们每次调用per
都需要便利numbers
后重新计算平均值,其实大可不必,就目前的功能来说,完全可以不用保存历史数据,我们只要保留已经输入的数字个数和数字之和就行了,对于新数字,只要累加后除以个数即可。
total = 0
nums = 0
def per(num: int) -> float:
total +=