概览
本章主要介绍装饰器和闭包(closure)。函数装饰器允许我们在源码上对函数进行标记,以增强函数的功能。要想能充分掌握装饰器,必须先理解闭包。
Decorators 101
装饰器是一种把另外一个函数作为参数的可调用对象。装饰器可能在被装饰的函数上做一些处理,然后返回这个函数或者其他的函数或可调用对象。
装饰器的效果可以用下面这个例子来展示:
@decorate
def target():
print('running target()')
等价于:
def target():
print('running target()')
target = decorate(target)
严格来讲装饰器只是一种语法糖,你可以像常规的调用一个可调用对象一样去使用装饰器,把另外一个函数作为参数。
总结一下装饰器有两个很重要的特性:
- 能够把被装饰的函数替换为另一个
- 装饰器是在模块加载的时候就立即执行的
When Python Executes Decorators
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
main()
running register()
running register()
running main()
registry -> [, ]
running f1()
running f2()
running f3()
从上面的例子可以看到装饰器是在模块加载的时候就运行。
Variable Scope Rules
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "", line 1, in
File "", line 3, in f1
NameError: name 'b' is not defined
>>> b = 6
>>> f1(3)
3
6
>>> b=6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "", line 1, in
File "", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
这是python的一个设计抉择:python不需要声明变量,但是python认为在一个函数里赋值的变量是一个局部变量。
Closures
闭包是一个包含扩展了可见范围的非全局变量的函数,这些变量在函数中引用,但是不在函数中定义。闭包的关键在于是否能访问定义在函数体外的非全局变量。
>>> def make_averager():
... series = []
...
... def averager(new_value):
... series.append(new_value)
... total = sum(series)
... return total/len(series)
...
... return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
可以看到series是make_averager中的局部变量。当调用avg(10)的时候,make_averager已经返回,并且他的局部可见范围已经消失。对于averager,series是一个自由变量(free variable,没有绑定到局部可见范围的变量)。
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
|
前面的series是一个可变对象,如果是一个不可变对象,进行赋值操作会使得变量变成一个局部变量,就会出错。python3提供了一个nonlocal
关键字,可以把一个变量变为一个自由变量。
>>> def make_averager():
... count = 0
... total = 0
...
... def averager(new_value):
... count += 1
... total += new_value
... return total / count
...
... return averager
...
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
File "", line 1, in
File "", line 5, in averager
UnboundLocalError: local variable 'count' referenced before assignment
>>> def make_averager():
... count = 0
... total = 0
...
... def averager(new_value):
... nonlocal count, total
... count += 1
... total += new_value
... return total / count
...
... return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
简单装饰器
import time
def clock(func):
def clocked(*args): #
t0 = time.perf_counter()
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)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
print(factorial.__name__)
**************************************** Calling snooze(.123)
[0.12481301s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000064s] factorial(1) -> 1
[0.00001838s] factorial(2) -> 2
[0.00003116s] factorial(3) -> 6
[0.00004362s] factorial(4) -> 24
[0.00005653s] factorial(5) -> 120
[0.00007111s] factorial(6) -> 720
6! = 720
clocked
这个是装饰器的一个典型的模式,用一个新的函数替换被装饰的函数,这个函数和被装饰的函数的参数相同,并且返回的内容也相同。从最后的打印可以看到factorial实际上持有的是到clocked函数的引用。
标准库里的装饰器
- functools.wraps:把被装饰函数的一些属性拷贝到装饰器函数,如name
- functools.lru_cache:可以对函数调用的结果进行缓存
- functools.singledispatch:将一个函数变为泛型函数,可以根据第一个参数的不同类型去做不同的操作
带参数的装饰器
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
print(clock)
print(clock())
print(clock()(snooze))
[0.12632895s] snooze(0.123) -> None
[0.12463617s] snooze(0.123) -> None
[0.12320995s] snooze(0.123) -> None
.decorate at 0x1075b6c80>
.decorate..clocked at 0x1075f5268>