第七章:函数装饰器和闭包

7.1 装饰器基本知识

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

假如有个名为decorate的装饰器:

@decorate
def target():
  print('running target()')

上述代码的效果与下述写法一样:

def target():
    print('running target()')

target = decorate(target)

两种写法的最终结果一样:上述两短代码片段执行完毕后得到的target不一定是原来那个target函数,而是decorate(target)返回的函数。

为了确认被装饰的函数会被替换,请看以下实例

def deco(func):
    def inner():
        print('running inner()')
    return inner  #deco 返回inner

@deco
def target():   #使用deco装饰target
    print('running target()')

target()  #调用被装饰的target其实会运行inner
target    #审查对象,发现target现在是inner的引用
1

严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。

综上,装饰器的一大特色是,能把被装饰的函数替换成其他函数,第二个特性是,装饰器在加载模块时立即执行。

7.2 Python何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python加载模块时),如下示例中的registration.py模块所示。

registry = [] #registry 保存被@register 装饰的函数引用

def register(func):    register 的参数是一个函数
    print('running register(%s)' %func) #为了演示,显示被装饰的函数
    registry.append(func) #把func存入registry
    return func    #返回func:必须返回函数;这里返回的函数与通过参数传入一样

@register     
#f1和f2被@register 装饰。
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3(): #f3没有装饰
    print('running f3()')

def main(): #main显示registry ,然后代用f1() f2() 和f3()
    print('running main')
    print('registry ->' , registry)
    f1()
    f2()
    f3()

if __name__ = '__main__':
    main() #只有把registration.py当做脚本运行时才调用main

当把registration.py当做脚本运行时 得到如下结果:


第七章:函数装饰器和闭包_第1张图片
2

注意,register 在模块中其他函数之前运行(两次)。调用register时,传给它的参数是被装饰的函数,例如

加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及f3,只在main明确调用它们时才执行。

7.3 使用装饰器改进“策略”模式

使用注册装饰器可以改进第六章的电商促销折扣示例

proms = []

def promotion(promo_func):
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%的折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0 

@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount

@promotion
def large_order(order):
    """订单中的不同商品达到10个或者以上时提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0 

def best_promo(order):
    """选择最佳折扣"""
    return max(pormo(order) for promo in promos)

7.4 变量作用域规则

#一个函数,读取一个局部变量和一个全局变量
def f1(a):
    print(a)
    print(b)
f1(3)
第七章:函数装饰器和闭包_第2张图片
3

出现错误并不奇怪,在以上实例中,如果先给全局变量b赋值,然后再调用f,那就不会出错

b  = 6
f1(3)
4

看一下以下示例的f2函数,前两行代码和f1一样,然后为b赋值,再打印它的值,可是,在赋值之前,第二个print失败了

b = 6
def f2(a):
    print(a)
    print(b)
    b = 9
f2(3)
第七章:函数装饰器和闭包_第3张图片
5

注意,首先输出了3 ,这表明print(a)语句执行了,但是第二个语句print(b)执行不了。原先以为会打印6 因为有个全局变量b,而且print(b)之后为局部变量b赋值的。

可事实上,python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python会尝试从本地环境获取b。后面调用f2(3)时,f2的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值

这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变 量是局部变量。这比 JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘 记把变量声明为局部变量(使用 var),可能会在不知情的情况下获取全局变量。
如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

b = 6
def f3(a):
    global b
    print(a)
    print(b)
    b = 9  

f3(3)
6
b
7
f3(3)
8
b = 30
b
9

7.5 闭包

在博客圈,人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数 不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。 因此,很多人是同时知道这两个概念的。
其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的 非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
这个概念难以掌握,最好通过示例理解。
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中 某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。

#计算移动平均的类
class Averager():

    def __init__(self):
        self.series = []

    def __call__(self , new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
avg = Averager()
print(avg(10))
print(avg(11))
print(avg(12))
10
# 计算移动平均的高阶函数
def make_averager():
    series = []

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

    return averager

注意,这两个示例有共通之处:调用 Averager() 或 make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在示例 7-8 中,avg 是 Averager 的实例;在示 例 7-9 中是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n 放入系列值中, 然后重新计算均值。

Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示 例中的 avg 函数在哪里寻找 series 呢?

例中的 avg 函数在哪里寻找 series 呢?
注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10) 时,make_averager 函数已经返回了,而它的本 地作用域也一去不复返了。

在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地 作用域中绑定的变量

第七章:函数装饰器和闭包_第4张图片
11

你可能感兴趣的:(第七章:函数装饰器和闭包)