Python 装饰器
本文内容参考 fluent python
装饰器基础知识
装饰器一般将一个函数替换为另一个函数,举例来说:
@deco
def foo():
print("running foo")
def foo():
print("running foo")
foo = deco(foo)
这两种写法的作用是一致的。所以目前可以直观的理解为,装饰器就是在某个函数的外面又包了一层。
什么时间执行装饰器
Python装饰器的一个关键特性是:它们在被装饰的函数定义后立即执行。
也就是说函数装饰器在导入模块时立刻运行,而被装饰的函数只在明确调用时运行。
tmp = []
def reg(func):
print("aaaa")
tmp.append(func)
return func
@reg
def f1():
print("running f1")
当你在命令行中输入以上代码时,会得到aaaa的输出,即使你还没有明确的调用f1,你会发现其实reg已经执行了,此时查看tmp也会看到f1的信息。而你明确调用f1时,即输入f1()
,会得到running f1的输出。
当然上面只是一个说明装饰器何时执行的demo,在实际使用中,装饰器和被装饰函数往往不在同一个模块中,而且装饰器返回的函数往往是内部定义的,并将其返回。
实际使用
这里书中举了一个前文的例子,个人觉得并不需要了解,只需要知道,装饰器可以在这个函数被调用前做一些其他的事情就OK了。
更常见的使用方式是,装饰器内部定义一个新的内部函数,并将其返回,替代被装饰的函数。说到这里,就不得不先提一下什么是闭包。
闭包
先来看一段代码:
b = 6
def foo(a=3):
print(a)
print(b)
b = 9
foo()
如果按照C或者C++中的经验,我们会认为输出是3和6,但实际上,是下面的结果:
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in foo
UnboundLocalError: local variable 'b' referenced before assignment
这是Python的一个设计选择(这不是bug,这是语言特性):Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。
也就是说,如果我们把b=9这行去掉,那么还是会输出3和6。那么可以通过global关键字让解释器把b当做全局变量。了解完Python的作用域,接下来就开始说明闭包。
首先说明。闭包与匿名函数不是一个概念,闭包指延伸了作用域的函数,包含在函数定义体中引用、但是不在定义体中定义的非全局变量,关键在于能否访问定义体之外定义的非全局变量。
举例来说,我们想计算一个不断增长的序列的均值:
class Averager:
def __init__(self) -> None:
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
这个实现并不能算错,而是不够pythonic,下面是函数式实现:
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = mkae_averager()
我们在调用avg时,因为make_averager已经返回,所以它的本地作用域被回收,但是实际上series是一个自由变量,并未在本地作用域中绑定。那么averager的闭包延伸到了他自己的作用域之外。
即,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用,但是仍可以使用这些自由变量的绑定。
python3提供了nonlocal
关键字,作用是把变量标记为自由变量。具体用处如下:
def make_averager():
total = 0
count = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
上面是make_averager的另一种实现,存在问题,这里因为averager内部的count进行了赋值操作,所以解释器默认为局部变量,因此闭包中并未绑定count为自由变量,total同理,此时就可以用nonlocal:
def make_averager():
total = 0
count = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
一个简单的装饰器
一个输出函数运行时间的装饰器:
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kw):
t0 = time.perf_counter()
res = func(*args, **kw)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kw:
pairs = ['%s=%r'%((k, w) for k, w in kw.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, res))
return res
return clocked
标准库中的装饰器
lru_cache
实现了备忘功能,有效减少递归深度。
singledispatch
根据第一个参数的类型,以不同的方式执行相同的操作的一组函数。
参数化装饰器
因为装饰器本质上是函数,因此,也可以定义一些参数。但这个时候往往需要在装饰器里再多一层嵌套。
def reg(active=True):
def deco(func):
if active:
func("deco")
else:
func("undeco")
return func
return deco
deco就是多的那一层嵌套。