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的引用
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上,装饰器的一大特色是,能把被装饰的函数替换成其他函数,第二个特性是,装饰器在加载模块时立即执行。
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当做脚本运行时 得到如下结果:
注意,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)
出现错误并不奇怪,在以上实例中,如果先给全局变量b赋值,然后再调用f,那就不会出错
b = 6
f1(3)
看一下以下示例的f2函数,前两行代码和f1一样,然后为b赋值,再打印它的值,可是,在赋值之前,第二个print失败了
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
注意,首先输出了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)
b
f3(3)
b = 30
b
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))
# 计算移动平均的高阶函数
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)。这是一个技术术语,指未在本地 作用域中绑定的变量