阿布云

你所需要的,不仅仅是一个好用的代理。

Python 函数装饰器和闭包

阿布云 发表于

50.png

装饰器基础知识

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

假如有个名为 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何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 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)

与函数策略给出的方案相比,这个方案有几个优点:

  1. @promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略,只需要把装饰器注释掉

  2. 促销折扣策略可以在其他的模块中定义,在系统中的任何地方都行,只要使用@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