你所需要的,不仅仅是一个好用的代理。
装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
假如有个名为 decorate 的装饰器:
@decorate
def target():
pprint('running target()')
上述代码的效果与下述写法一样:
def target():
print('running target()')
target = decorate(target)
两种写法的最终结果一样:上述两个代码片段执行完毕后得到的target 不一定是原来那个 target 函数,而是 decorate(target) 返回的函数
举个:chestnut: 装饰器通常把函数替换成另一个函数
1 def deco(func):
2 def inner():
3 print('running in inner()')
4 return inner
5
6 @deco
7 def target():
8 print('running in target()')
9
10 target()
以上代码执行的结果为:
running in inner()
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是, 装饰器在加载模块时立即执行 。
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时),如 :chestnut: 的registration.py 模块所示。
1 registry = []
2
3 def register(func):
4 '''
5 :param func:被装饰器的函数
6 :return: 返回被装饰的函数func
7 '''
8 print('running register(%s)' % func) #获取形参func的的引用
9 registry.append(func) #获取的引用地址放入到类表中
10 return func #执行装饰器的时候返回func
11
12 @register
13 def f1():
14 print('running f1()')
15
16 @register
17 def f2():
18 print('running f2()')
19
20 def f3():
21 print('running f3()')
22
23 def main():
24 print('running main()')
25 print('registry ->', registry)
26 f1()
27 f2()
28 f3()
29
30 if __name__ == "__main__":
31 main()
以上代码执行的结果为:
running register(<function f1 at 0x101c7bf28>)
running register(<function f2 at 0x101c83048>)
running main()
registry -> [<function f1 at 0x101c7bf28>, <function f2 at 0x101c83048>]
running f1()
running f2()
running f3()
注意,register 在模块中其他函数之前运行(两次)。调用register 时,传给它的参数是被装饰的函数,例如 <function f1 at 0x101c7bf28>。
加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及 f3,只在 main 明确调用它们时才执行。
使用装饰器改进“策略”模式
使用注册装饰器可以改进的电商促销折扣 :chestnut: 回顾一下,函数策略主要问题是,定义体中有函数的名称,但是best_promo 用来判断哪个折扣幅度最大的 promos 列表中也有函数名称。这种重复是个问题,因为新增策略函数后可能会忘记把它添加到promos 列表中,导致 best_promo 忽略新策略,而且不报错,为系统引入了不易察觉的缺陷。下面的 :chestnut: 使用注册装饰器解决了这个问题。
:chestnut: promos 列表中的值使用 promotion 装饰器填充
1 promos = []
2
3 def promotion(promo_func):
4 promos.append(promo_func)
5 return promo_func
6
7 @promotion
8 def fidelity(order):
9 """为积分为1000或以上的顾客提供5%折扣"""
10 return order.total() * .05 if order.customer.fidelity >= 1000 else 0
11
12 @promotion
13 def bulk_item(order):
14 """单个商品为20个或以上时提供10%折扣"""
15 discount = 0
16 for item in order.cart:
17 if item.quantity >= 20:
18 discount += item.total() * .1
19 return discount
20
21 @promotion
22 def large_order(order):
23 """订单中的不同商品达到10个或以上时提供7%折扣"""
24 distinct_items = {item.product for item in order.cart}
25 if len(distinct_items) >= 10:
26 return order.total() * .07
27 return 0
28
29 def best_promo(order):
30 """选择可用的最佳折扣"""
31 return max(promo(order) for promo in promos)
与函数策略给出的方案相比,这个方案有几个优点:
@promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略,只需要把装饰器注释掉
促销折扣策略可以在其他的模块中定义,在系统中的任何地方都行,只要使用@promotion装即可
变量作用域规则
举个 :chestnut: 来说明函数作用域的问题,我们定义并测试一个函数,它读取两个两个变量的值:一个是局部变量a,是函数的参数;另外一个变量b,这个函数没有定义它!
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: name 'b' is not defined
如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:
>>> b = 10
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(50)
50
10
来个让你大吃一惊的 :chestnut: 在函数的内部直接修改全局变量b,我们看下会出现什么问题
>>> b = 10
>>> def f1(a):
... print('f1 a:', a)
... print('f1 b:', b)
... b = 3
...
>>> f1(3)
f1 a: 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
UnboundLocalError: local variable 'b' referenced before assignment
注意,首先输出了 3,这表明 print(a) 语句执行了。但是第二个语句print(b) 执行不了。一开始我很吃惊,我觉得会打印 10,因为有个全局变量 b,而且是在 print(b) 之后为局部变量 b 赋值的。
可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 f1(3) 时, f1 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。
如何解决上面的问题呢, 如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明 :
>>> b = 10
>>> def f1(a):
... global b
... print('f1 a:', a)
... print('f1 b:', b)
... b = 20
... print('f1 更改b值以后:', b)
...
>>> print('全局的b值', b)
全局的b值 10
>>> f1(3)
f1 a: 3
f1 b: 10
f1 更改b值以后: 20
>>> print(b)
20