摘自《流畅的python》
7.1 装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
假如有个名为decorate 的装饰器:
@decorate
def target():
print('running target()')
上述代码的效果与下述写法一样:
def target():
print('running target()')
target = decorate(target)
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
7.2 Python何时执行装饰器
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python 加载模块时)。
函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了Python 程序员所说的导入时和运行时之间的区别。
装饰器可以原封不动地返回被装饰的函数:很多Python Web 框架使用这样的装饰器把函数添加到某种中央注册处,例如把URL 模式映射到生成HTTP 响应的函数上的注册处。这种注册装饰器可能会也可能不会修改被装饰的函数。
不过,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。
7.4 变量作用域规则
>>> 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
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)
3
6
>>> b
9
7.5 闭包
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
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)。这是一个技术术语,指未在本地作用域中绑定的变量。
series 绑定在返回的avg 函数的__closure__ 属性中。
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
7.6 nonlocal声明
前面实现make_averager 函数的方法效率不高。
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):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
问题是,当count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count =count + 1 一样。因此,我们在averager 的定义体中为count 赋值了,这会把count 变成局部变量。total 变量也受这个问题影响。
第一个实例没遇到这个问题,因为我们没有给series 赋值,我们只是调用series.append,并把它传给sum 和len。也就是说,我们利用了列表是可变的对象这一事实。
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1,其实会隐式创建局部变量count。这样,count 就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3 引入了nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
7.7 实现一个简单的装饰器
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args) # 这行代码可用,是因为clocked 的闭包中包含自由变量func。
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 装饰器
to be continued...
7.8 标准库中的装饰器
to be continued...
7.9 叠放装饰器
把@d1 和@d2 两个装饰器按顺序应用到f 函数上,作用相当于f = d1(d2(f)):
@d1
@d2
def f():
print('f')
等同于:
def f():
print('f')
f = d1(d2(f))
7.10 参数化装饰器
解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
7.10.1 一个参数化的注册装饰器
为了便于启用或禁用register 执行的函数注册功能,我们为它提供一个可选的active 参数,设为False 时,不注册被装饰的函数。从概念上看,这个新的register 函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。
registry = set()
def register(active=True):
def decorate(func): #decorate 这个内部函数是真正的装饰器;注意,它的参数是一个函数。
print('running register(active=%s)->decorate(%s)'% (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func #decorate 是装饰器,必须返回一个函数。
return decorate #register 是装饰器工厂函数,因此返回decorate。
@register(active=False) #@register 工厂函数必须作为函数调用,并且传入所需的参数。
def f1():
print('running f1()')
@register() #即使不传入参数,register 也必须作为函数调用(@register())
def f2():
print('running f2()')
如果不使用@ 句法,那就要像常规函数那样使用register;若想把f 添加到registry中,则装饰f 函数的句法是register()(f);不想添加(或把它删除)的话,句法是register(active=False)(f)。
7.10.2 参数化clock装饰器
参数化装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套。接下来会探讨这种函数金字塔。
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): #clock 是参数化装饰器工厂函数。
def decorate(func): #decorate 是真正的装饰器。
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