装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数),装饰器可能会处理被装饰的函数,然后把他返回,或者将其替换成另一个函数或可调用对象。函数额装饰器用在源码中‘标记’函数,想要完全掌握装饰器,必须要先理解闭包。闭包是指在函数内部在定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包,简单的说,就是如果在一个内部函数里,对在外部作用域(但不是在在全局作用域)的变量进行引用,那么内部函数被称之为闭包。说回装饰器,下面看一个使用装饰器的例子:
def deco(func):
def inner():
print('running inner()')
return inner
@deco
def target():
print('running target()')
"""
上面代码相当于:
def target():
print('running target()')
target = deco(target)
"""
>>>target()
running inner
>>>target
<function __main__.deco.<locals>.inner()>
由上面的代码可知,调用被装饰的target其实是会运行inner而且target现在是inner的引用。严格来说,装饰器只是语法糖,其一大特性就是能把装饰的函数替换成其他函数,第二大特性是装饰器在加载模块时立即执行。
Python何时执行装饰器
装饰器的关键特性就是在被装饰的函数定义之后立即执行。通常是在导入时也就是python加载模块时。但是被装饰的函数只在明确调用时运行。这也就是导入和运行之间的区别。
装饰器在真实代码中的常用方式有两个:
- 装饰器函数与被装饰的函数在同一个模块中定义,实际上,装饰器通常在一个模块中定义,然后应用到其他模块上。
- 装饰器给返回的函数与通过参数传入的相同,实际上,大部分装饰器会在内部定义一个函数,然后将其返回。
下面给出一个使用装饰器改进‘策略’模型的代码
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order:
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.___total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total - discount
def __repr__(self):
fmt = '<Order total:{:.2f} due:{:.2f}>'
return fmt.format(self.total, self.due())
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
return order.total * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(oder):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
@promotion
def large_order(order):
distinct_item = {item.product for item in order.cart}
if len(distinct_item) >= 10:
return order.total * .07
return 0
def best_promo(order):
return max(promo(order) for promo in promos)
if __name__ == '__main__':
Joe = Customer('John Joe', 0)
ann = Customer('Ann Smith', 1100)
cart = [
LineItem('banana', 50, 2.5),
LineItem('apple', 10, 1.5),
LineItem('watermelons', 5, 5.0)
]
order = Order(Joe, cart)
print(bulk_item(order))
print(promos)
print(best_promo(order))
promos列表中的值使用promotion装饰器填充,这一代码中国promotion将promo_func添加到promos列表中,然后原封不动的将其返回,被promotion装饰的函数都会添加到promos列表中,best_promps无需修改。使用装饰器的优点在于:
- @promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略:只需要把装饰器注释掉
- 促销折扣策略可以在其他模块中定义,在系统中的任何地方都可以,只要使用@promotion装饰即可。
注意:多数装饰器会修改被装饰的函数,通常会定义一个内部函数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。因此下面讲一讲闭包
闭包
闭包是指在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。简单的说,如果在一个内部函数里,对再外部作用域但又不是全局作用域的变量进行引用,那么内部函数就被认为是闭包。
定义一个计算移动平均值的高阶函数
方法1:使用类实现
class Average(object):
def __init__(self):
self.series = []
# 类中定义__call__函数,那么他的实例可以作为函数调用
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
>>>avg = Average()
>>>avg(12)
12
>>>avg(13)
12.5
>>>avg.series
[12,13]
方法2:使用函数式实现
def make_average():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
>>>avg = make_average()
>>>avg(10)
10
>>>avg(11)
10.5
>>>avg.__code__.co_varnames
('new_value', 'total')
>>>avg.__code__.co_freevars
('series')
>>>avg.__closure__[0].cell_contents
[10,11]
上面代码再调用Average()和make_average()时候都得到一个可调用的avg,他会更新历史值,然后计算当前均值,在第一段代码中,avg是Average的实例,第二段代码中是内部函数average,只需要调用avg(n),把n放在系列值中,然后重新计算均值。第一段代码中avg存储的历史值在self.series中。但是第二个呢?第二段代码中series是make_averager函数的局部变量,因为那个函数的定义体中初始化series:series =[],在average函数中,series是自由变量,average整个代码段是一个闭包。闭包是一种函数,他会保留定义函数存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然使用那些绑定。
python中的闭包有以下的特点:
- 绑定了外部作用域的变量的函数闭包
- .引用了外部自由变量的函数
- .即使程序每次离开外部作用域,如果闭包仍然可见,绑定变量不会销毁
- .每次运行外部函数都会重新创建闭包
- .自由变量:不在当前函数定义的变量
- 特性:自由变量和闭包同时存在
- 闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量之间互不干扰
- 一个闭包实例对其自由变量的修改会传递到下一次该闭包实例的调用
python3.2之后的版本引入了一个关键词nonlocal。作用是把变量标记为自由变量,即使在函数中为变量赋予新值,也会变成自由变量。如果nonlocal声明的变量赋予新值,闭包中保存的绑定就会更新。官网上nonlocal的用法:
- 非局部声明变量指代的已有标识符是最近外面函数的已声明变量,但是不包括全局变量。这个是很重要的,因为绑定的默认行为是首先搜索本地命名空间。nonlocal声明的变量只对局部起作用,离开封装函数,那么该变量就无效。
- 非局部声明不像全局声明,我们必须在封装函数前面事先声明该变量
- 非局部声明不能与局部范围的声明冲突
下面用nonlocal改写上面的make_average代码:
def make_average2():
count = 0
total = 0
def averager(new_value):
nonlocal count,total
count = count +1
total += new_value
return total/count
return averager
>>>avg = make_average2()
>>>avg(10)
10.0
>>>avg(11)
10.5
>>>avg.__code__.co_varnames
>>>avg.__code__.co_freevars
('new_value',)
('count','total')
实现简单装饰器
实现一个简单的装饰器,输出函数的运行时间
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
# 闭包的自由变量是func
result = func(*args)
elapsed = time.perf_counter()- t0
name = func.__name__
arg_str = ','.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r'%(elapsed,name,arg_str,result))
return result
return clocked
@clock
def snooze(seconds):
time.sleep(seconds)
在上面的例子中,snooze会作为func参数传递给clock,然后clock函数会返回clocked函数,python解释器会把clocked赋值给snooze,用Python Tutor 可视化工具可以得到:
clocked大致做了下面几件事情:
- 记录初始时间t0
- 调用原来的snooze函数,保存结果
- 计算经过的时间
- 格式化收集的数据,然后打印
- 返回保存的结果
标准库中的装饰器
Python中内置了三个用于装饰方法的函数:property、classmethod、staticmethod
后面补充
叠放装饰器
@d1
@d2
def f():
print('f')
#等同于
def f():
print('f')
f = d1(d2(f))