decorator

我眼中的 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.

可以看到,虽然仍旧调用的是 nakednot_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

最近挖了一个新坑,收集装饰器的一些资源和实际开发中的用法,github 地址: awesome-python-decorator

其他

关于 functools.wraps

在我们使用一般的装饰器语法时,对于第一个例子:

>>> print naked.__name__
wrapper

看看第二个手工的方法就应该能明白,的确是这样,就像我们对礼物包装了以后只能看到礼物的外表(wrapper), 我们是不知道里面是什么东西的,但是如果我们想要知道原来被装饰的函数,我们可以使用 functools.wraps, 可以使我们得到被装饰的函数信息,这就像在礼物的包装纸上贴个小标签。

在使用装饰器的时候,其实我们是新定义了一个函数(wrapper),在这个新定义的函数里面,我们再对原来的函数自定义行为, 比如可以在原函数之前或者之后做一些额外的事情。所以其实是返回了 wrapper,并给原来的函数。但是也带来了一个新的问题, 就是原来函数的一些信息(__name__, __doc__)也被抹掉了,这在我们 debug 的时候是不利的。

而只要你在新定义的函数前使用 wraps 这个装饰器,原函数的信息就可以正确得到啦。

那么为什么用 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).

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__)