装饰器是在函数调用上的修饰。这些修饰仅是当声明一个函数或者方法的时候才会应用的额外调用。
装饰器的语法看起来像是这个样子
def decorator(func2bedecorated):#定义装饰器
return wrappedFunc
decorator([dec_opt_args])#使用装饰器
def func2bedecorated([func_opt_args]):#定义被装饰的函数
首先需要定义装饰器或者你也可以用内建的装饰器其实装饰器就是一个函数它接受其他函数为参数并返回一个装饰过的函数或其他对象。
是的装饰器在使用的时候后面没有冒号因此下面的语句也就不需要缩进。
从定义装饰器的语句可以看到它是有返回值的。他把自定义的wrappedFunc 返回给了 func2bedecorated也就是被装饰的函数自身即原函数被覆盖了。对应到数学概念上使用装饰器来定义函数就像是写一个复合函数就像 g(f(x)) 这样g(x)是装饰器
因此看起来似乎正确的装饰器函数调用方法应该是这样
func2bedecorated decorator(func2bedecorated)
或者至少也应该是这样
decorator(func2bedecorated)
实际上面两种方法都可行。从某种意义上说装饰器的语法就是一个语法糖。还记得前面介绍过的如何定义类的静态方法么
class C:
def __init__(self):
pass
def foo():
print("calling static method foo().")
foo staticmethod(foo)
这里的staticmethod()其实就是一个内建的装饰器函数。因此上面的示例代码也可以使用装饰器的标准语法写成
class C:
def __init__(self):
pass
staticmethod
def foo():
print("calling static method foo().")
这种语法看起来要漂亮得多。
好了那么装饰器一般都用在什么地方呢除了上面提到的静态方法、或者静态属性
于是最后给出一个时间戳的例子因为偶尔会有给函数做性能测试的需求如果给每个函数都手动添加 time.ctime() 的话就太不科学了。这时候使用一个时间戳装饰器是个不错的做法
from time import ctime,sleep
def tsfunc(func):
def wrappedFunc():
print([%s] %s() called%(ctime(),func.__name__))
func()
print([%s] %s() ended%(ctime(),func.__name__))
return wrappedFunc
tsfunc
def foo():
sleep(1)
foo()
这里我们首先定义了一个名为 tsfunc 的时间戳装饰器。可以看到它对传入的 func 都做了什么
在装饰器内定义出新的函数名为 wrappedFunc() 好待会把它返回出去
wrappedFunc 的实际内容仅为先打印出一行程序启动的时间戳随后调用 func() , 最后再打印出一行程序结束的时间戳
要做测试的演示函数 foo() 的内容仅是 sleep 一秒钟可以看到下面的运行结果正如期望
[Sat Nov 23 20:33:34 2013] foo() called
[Sat Nov 23 20:33:35 2013] foo() ended
>>>
在Python3 中类也有装饰器了用法与函数相同即可以给装饰器函数传入一个class作为参数。另外地上面提到过一句装饰器返回的可以不是一个函数。这一点会在之后的“描述符”descriptor中讲到。
然后很多地方会提到装饰器分为有参数和无参数两种。这种说法容易造成混淆让人误以为他们之间只是带不带参数的区别。而实际上有无参数的装饰器在结构和功能上都是不同的。或者直白些讲带参数的装饰器其实是一个返回另一个装饰器的 deco_maker不带参数的装饰器才是真正的 real_deco。用伪代码可以表示为
def real_deco(func_2b_decorated):
def wrapped(func_2b_decorated):
...
...
return wrapped
def deco_maker(deco_args):
def real_deco(func_2b_decorated):
...
...
return real_deco
即带参数的装饰器返回的其实是一个真正(不带参数)的装饰器。前面说过装饰器就是一个函数一个接受函数做参数并返回一个新函数的函数所以装饰器也可以用来返回另一个装饰器就不难理解了。deco 的语法仅是一种语法糖它和社交网站的 somebody 没有啥区别表示通知 deco 来干活了。所以你看到 deco 这语法里仅是 了一下函数名但带参数的情况就不同了带参数的装饰器用起来是这样子的
deco_maker(deco_args)
def func_2b_decorated():
...
...
这里 的对象明显已经不是一个函数名了而是一个函数调用。所以这里真正被叫来干活装饰 func_2b_decorated的家伙其实是将要被 deco_maker 返回的那个 real_deco。
这就是带参数的装饰器的作用他可以通过参数的设定来返回不同的装饰器这比直接调用不同的装饰器看起来优雅一些。下面贴出一个带参数装饰器的示例并引出闭包的话题
from time import time
def logged(when):
def log(f,*args,**kargs):
print(Called:
function:%s
args:%r
kargs:%r%(f,args,kargs))
def pre_logged(f):
def wrapper(*args,**kargs):
log(f,*args,**kargs)
return f(*args,**kargs)
return wrapper
def post_logged(f):
def wrapper(*args,**kargs):
now time()
try:
return f(*args,**kargs)
finally:
log(f,*args,**kargs)
print(time delta:%s%(time()-now))
return wrapper
try:
return {pre:pre_logged,post:post_logged}[when]
except KeyError as e:
raise ValueError(e,must be "pre" or "post")
下面是运行示例
>>> logged(post)
def hello(name):
print("Hello,",name)
>>> hello(Worlds!)
Hello, Worlds!
Called:
function:
args:(Worlds!,)
kargs:{}
time delta:0.034002065658569336
本装饰器提供的功能为给函数加 log而且分为“先输出 log后调用函数”和“先调用函数后输出 log”两种方式。log 的内容包含函数名、非关键字参数、关键字参数的信息。并且如果是 post_log 的方式还可以打印出函数执行花费的时间。和上面的时间戳功能类似。
这里在使用装饰器的时候就给“logged(post)”传了一个“post”参数用来选定要返回的 real_deco。同时返回的也是一个闭包。因为 Python 支持静态嵌套域所以可以从函数内部访问外部作用域的变量。假如在一个外部函数中嵌套了一个内部函数并且这个内部函数又引用了外部函数作用域里的变量那么这个内部函数连带外部作用域一起就叫做闭包。所以说到底闭包仍然是一个函数但他比一般函数高级之处就是带了多余的自由变量被引用的外部作用域非全局的变量就叫做自由变量。这使得这个函数在调用的时候能记忆状态了因为自由变量没有位于内部函数里所以即使内部函数调用完毕这个变量也不会被释放。这看起来就像类的属性一样实际用起来也像类的属性一样。因为 Python 并非专业的函数式编程语言所以除非有额外要求必须返回一个带自己作用域的回调函数一般还是建议使用类实现。上面的装饰器就是一个有额外要求的例子。因为我们是要包装函数的所以也要返回一个函数。返回的装饰器携带了 log() 函数所以这个装饰器也就是一个闭包。
如果上面这个例子不明显的话一般用来做闭包举例的是下面这样的计数程序
def counter(start_at0):
count [start_at]
def incr():
count[0] 1
return count[0]
return incr
运行结果如下
>>> a counter()
>>> b counter(100)
>>> a()
1
>>> a()
2
>>> b()
101
>>> b()
102
>>> a()
3
a 和 b 两个函数对象分别携带了自己的自由变量 count并可以通过这个变量来计数这里用函数实现了平时要用一个类来实现的功能并且对象还是可调用的。不过这里需要注意的是如你所见在 counter() 函数里使用的是一个列表既然只使用一个值count[0],干吗不用整数呢这是因为如果把 count[0] 换成整数 count 的话会报下面的错
UnboundLocalError: local variable count referenced before assignment
因为内部作用域会覆盖外部作用域。所以当你在内部作用域运行 count 1这条语句的时候解释器会直接认定你在声明本地变量 count 并试图给它赋值。实际上解释器运行 incr() 这个函数时做的第一件事就是把 count 标记为本地变量因为他看到了这个名字后面有一个赋值符号。而接下来又因为 count 还没有赋值就被等号后面引用了这才引发了那个异常。也就是说解释器根本没想着去外部作用域寻找 count 变量。但 count[0] 就不同了这时的 count 是一个可变类型解释器会试图寻找 count 并赋值而不是直接声明本地变量。也许这种状况会让人产生“为什么 Python 不把变量声明和赋值分开呢那样就不会出错了”的抱怨但实际上这种动态设定带来的好处远比麻烦多而且也不是没有方法来解决上面这个问题。虽然这个方法是从 Python3 开始添加的
这个解决方法就是 nonlocal 关键字它的作用与 global 极其相似区别仅在于作用域不同。Python2.1 以前函数只能访问自己的作用域或者全局作用域那样有 global 就够用了现在因为有了闭包就需要一种声明变量既不是本地又不是全局的方法这就是 nonlocal。所以配合 nonlocal就可以使用整数来实现上面的计数器了
def counter(start_at0):
count start_at
def incr():
nonlocal count
count 1
return count
return incr
运行
>>> a counter()
>>> a()
1
>>> a()
2
最后要说的是闭包的功能是可以被类取代的。只要定义了 __call__(self) 方法这个类的实例就是可调用的再带上实例本身的属性就可以当闭包用。