我眼中的 decorator
decorator 是什么呢?正如其名字一样,是“装饰”,就比如你买个礼物,为了好看,你会在外用彩纸包装一下,
外观确实看起来不一样了。那么在 python 中呢,在我的理解中,它可以使你在不对原函数修改(这里的修改指的是代码本身修改,
是指增加减少几行代码这种修改形式)的情况下做一些自定义行为,最简单的比如说在函数调用前和调用后添加你想要做的行为。
注意这个和 GoF 中的装饰器模式不一样,装饰器模式是在运行时叠加组合一些功能,但在 python 中,定义时就已经完成了对函数的包装。
看起来有点抽象,先来个具体的例子吧:
def take_off_pants(function): def wrapper(): print('I take off my pants.') function() return wrapper def put_on_pants(function): def wrapper(): function() print("cause I've put on my pants.") return wrapper @take_off_pants def naked(): print("I'm naked now!") @put_on_pants def not_naked(): print("I'm not naked now!") naked() print('='*10) not_naked() # output: I take off my pants. I'm naked now! ========== I'm not naked now! cause I've put on my pants.
可以看到,虽然仍旧调用的是 naked 和 not_naked 但是分别在函数前面和后面执行的我定义的动作。
还是有点疑惑?对于 @ 用法表示不解?好吧,我们接下来不使用 @ 语法,而直接手工来做一个装饰器。还是以之前的例子来说。
def naked(): print("I'm naked now!") def not_naked(): print("I'm not naked now!") naked = take_off_pants(naked) not_naked = put_on_pants(not_naked) naked() print('='*10) not_naked() # output: I take off my pants. I'm naked now! ========== I'm not naked now! cause I've put on my pants.
仔细观察就会发现,使用 @ 语法的版本和手工制作版本的区别在于:
@take_off_pants ==> naked = take_off_pants(naked) @put_on_pants ==> not_naked = put_on_pants(not_naked)
而实际上 @ 的作用也是这样,只是更加简单方便了。
现在我们来看看装饰器到底做了什么。
def take_off_pants(function): def wrapper(): print('I take off my pants.') function() return wrapper naked = take_off_pants(naked)
应该是比较容易理解的,take_off_pants 返回的是 wrapper, 这时我们的 naked 已经是 wrapper 了,
虽然原函数没有修改,但当你调用 naked 的时候,其实就等于在调用 wrapper 了,这样,只要我们在 wrapper
中继续调用原来的函数,并且做一些我们自己的修改,看起来就像在没有修改原函数的情况下,还增加了自定义的行为。
既然我们实际上操作的是 wrapper, 我们就应该明白,正如我们只能看到被包装好了的礼物的外表,我们是看不到里面的礼物具体是什么
(我们可以通过 wraps 知道里面礼物的信息,但是这相当于在外面贴了小标签来告知你,我们是无法直接获知里面的礼物信息的),
我们也是只能看到 wrapper,这样想想,我们是可以完全替换掉原来的函数,或者在之前之后增加行为,或者对原函数本身加上点修改等等,
我们可以在 wrapper 里面定义我们自己想要的行为,我们看到的是外表,谁知道你对里面的礼物做了什么呢。
现实中的 decorator
- benchmark
- cache
- lazy_evaluation:
- memoization
最近挖了一个新坑,收集装饰器的一些资源和实际开发中的用法,github 地址: awesome-python-decorator
其他
关于 functools.wraps
在我们使用一般的装饰器语法时,对于第一个例子:
>>> print naked.__name__ wrapper
看看第二个手工的方法就应该能明白,的确是这样,就像我们对礼物包装了以后只能看到礼物的外表(wrapper),
我们是不知道里面是什么东西的,但是如果我们想要知道原来被装饰的函数,我们可以使用 functools.wraps,
可以使我们得到被装饰的函数信息,这就像在礼物的包装纸上贴个小标签。
- functools.wraps(wrapped[, assigned][, updated])
在使用装饰器的时候,其实我们是新定义了一个函数(wrapper),在这个新定义的函数里面,我们再对原来的函数自定义行为,
比如可以在原函数之前或者之后做一些额外的事情。所以其实是返回了 wrapper,并给原来的函数。但是也带来了一个新的问题,
就是原来函数的一些信息(__name__, __doc__)也被抹掉了,这在我们 debug 的时候是不利的。
而只要你在新定义的函数前使用 wraps 这个装饰器,原函数的信息就可以正确得到啦。
- functools.update_wrapper(wrapper, wrapped[, assigned][, updated])
那么为什么用 wraps 就可以保留原来函数的信息呢。就是用的这个啦。这个的作用呢,就是
Update a wrapper function to look like the wrapped function.
wrapped 的有些属性信息会直接赋值传递给 wrapper, 有些会把 wrapper 中的同名属性更新, 看看该函数的用法就应该明白,
assigned updated 默认值是:
module level constants WRAPPER_ASSIGNMENTS (which assigns to the wrapper
function’s __name__, __module__ and __doc__, the documentation string) and
WRAPPER_UPDATES (which updates the wrapper function’s __dict__, i.e. the
instance dictionary).
- functools.partial(func[,*args][, **keywords])
The partial() is used for partial function application which “freezes” some
portion of a function’s arguments and/or keywords resulting in a new object
with a simplified signature.
对某个函数,你可以自定义 freeze 一些 args 或 keyword,就像他们是默认的一样,返回一个 partial
对象(当被调用的时候表现的就像函数)。结果就是,你拥有了一个新的'函数',他和原函数基本差不多,
只是多了一些你自定义过的固定参数。举个例子:
>>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo('10010') 18
本来你是要一遍又一遍的用 int('xxxx', 2) 来达到效果,现在你使用 basetwo('xxxx') 就可以了,
因为 base=2 是每次都要用到的嘛,那么干脆就把这个参数 freeze, 生成了一个新的更为简单的可以用的'函数'。
前面提到的 wraps 源码其实就是
def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
下面分别以三种方式实现 functools.wraps, 可以看看具体的应用方法是怎么样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #!/usr/bin/env python # -*- coding: utf-8 -*- from functools import wraps, update_wrapper, partial def orininal_wraps(wrapped): """The original functools.wraps implementation.""" return partial(update_wrapper, wrapped=wrapped) def my_wraps(wrapped): """Implement wraps with functools.update_wrapper.""" def my_decorator(wrapper): return update_wrapper(wrapper, wrapped) return my_decorator def my_decorator(func): """Try the 3 different wraps, they're equal to each other.""" # @wraps(func) # @my_wraps(func) @orininal_wraps(func) def wrapper(): print("call from decorator") func() return wrapper @my_decorator def example(): print("call from example") if __name__ == '__main__': example() print(example.__name__) |