修饰器

什么是修饰器 (Decorator)

修饰器 (Decorator) 特指 Python 的一种 单行以@开始 的语法,@ 后面跟的是一个函数,比如之前一些代码示例中出现的 @property 的用法,也是修饰器的实际应用。

@decorator_func
def my_func(*args, **kwargs):
    pass

当调用 my_func(*args, **kwargs) 时,实际完整的逻辑是 decorator_func(my_func)(*args, **kwargs),出现了两个连续的 (),感觉有些费解?
其实,把它分开来看就好了,decorator_func(my_func) 会先返回一个新函数,然后再传入参数 *args, **kwargs 给这个新函数调用。

在使用修饰器时,我们还会看到类似下面这种,传入参数到修饰器中的:

@decorator_func(*d_args, **d_kwargs)
def my_func(*args, **kwargs):
    pass

那么按照上面,把调用 my_func 的完整逻辑补全,是 decorator_func(*d_args, **d_kwargs)(my_func)(*args,**kwargs)
啥?这是啥?这是啥? 想必脑海中现在的感觉就是这样的,怎么有三次连续的调用呀……
我们也进行分解,decorator_func(*d_args, **d_kwargs) 会产生一个函数,随便取个名字叫 tmp_func_1,然后 tmp_func_1(my_func) 再得到一个函数,再随便取个名字叫 tmp_func_2,最后 tmp_func_2(*args, **kwargs)得到最终结果。

修饰器函数

上段内容中,如果不另外增加参数,就是单纯 @decorator_func 的用法,对应的修饰器函数的定义,大概是如下的样子:

def decorator_func(func):
    def _func(*args, **kwargs):
        return func(*args, **kwargs)
    return _func

func 就是原始传入给修饰器的源函数,而 _func 则是最终修饰器返回的新函数,里面的 *args, **kwargs 也都会传入给新函数,在新函数中,又会调用源函数进行原有的逻辑计算。

Class 与 修饰器

@ 后面跟的不全然只有函数,也可以是一个 Class。当我们说调用的时候,实际上是运行了(),如果一个对象有一个 __call__ 这个 Magic Method,其实就接管了 调用 的逻辑了。
比如 my_func(*args, **kwargs) 实际上等同于 my_func.__call__(*args, **kwargs),有时我们判断一个对象是否可调用,就会去判断 hasattr(my_func, '__call__')
如果一个 修饰器 比较复杂,或者本身也允许再接受参数的,一般我们会用 Class,而不是一个 Function,只要在 Class 里最后再定义 __call__ 就可以了。

但不是所有的 () 都是 __call__,比如一个 Class,其第一个 () 一般是实例化调用了 __init__

>>> class ClassForCall(object):
...     def __init__(self):
...         print('init now')
...     def __call__(self, *args, **kwargs):
...         print('__call__, %s, %s' % (args, kwargs))
...
>>> obj = ClassForCall()
init now
>>> obj()
__call__, (), {}

如上示例中,如果要调用 __call__,且在一行代码写完,如下:

# 第一个括号是实例化后,调用 __init__
# 第二个括号才调用了 __call__
ClassForCall()()

注: 接受参数的修饰器,不一定要写成 class,但写成 function 又会出现 子fuction 以及 子子fuction,3 层堆叠结构,个人并不推荐。

修饰器有什么用

修饰器 很好用,相对普通函数,它所处的层级更高,一个修饰器本质上是 改写函数,而且是批量式的。

它一旦用得好,就能节省大量的时间。
举一个简单的例子,假设我们正在写一个 Web 端网站,某些 URL (页面) 需要登录后才能查看,那在对应页面的函数中,增加 @need_login,如果只有特定用户才能访问的,那么增加 @need_login('admin') 此类的。need_login 这个修饰器函数需要自己另外写,但它的好处太明显了,把验证登录的逻辑统一放在一起,再用 修饰器 的方式按需调用。

一个高阶修饰器

我们要完成一个比较高级的修饰器,这个如果理解了,那么修饰器基本上也就明白了。先看代码:

class cached(object):
    def __init__(self, first_arg=None, *d_args, **d_kwargs):
        if hasattr(first_arg, '__call__'):
            self.direct_func = first_arg
        else:
            self.direct_func = None
        # d_args & d_kwargs 配合 @cached(*args, **kwargs)
        # 如有必要,进行存储,后面 def _func 的时候有用
    def __call__(self, *args, **kwargs):
        if self.direct_func:
            # @cached 用法,直接返回结果
            return self.direct_func(*args, **kwargs)
        else:
            # @cached(*args, **kwargs),返回函数
            # 这个时候,kwargs 必然是空字典,args 必然只有一个元素
            original_func = args[0]
            def _func(*func_args, **func_kwargs):
                return original_func(*func_args, **func_kwargs)
            return _func

我们希望这个 修饰器 可以同时支持两种用法,一种是 @cached,另一种是 @cached(*args, **kwargs),但之前我们介绍 修饰器 时,为了方便理解,把这两种方式压平了来看: 前者会产生 2 次 调用,而后者会产生 3 次 调用。
两种使用方式,完全是不同的性质。一般不跟参数的修饰器,常用 fucntion 来对应;而带参数的修饰器 会用 class 对应,看起来会有条理一些。
如上代码,实现了两种方式的混用,为了避免费解,实际的 缓存 逻辑并没有写进去。

另外,我们会发现这个虽然是 Class,但命名没有写成 Cached 而是 cached,是否这种风格不好?不尽然,这个风格看起来更像是 修饰器,如果是 Cached,那么用起来是 @Cached 反倒有些奇怪。
上面的代码不多,或许理解起来还是费劲,倒也没有关系,并不要求完全掌握,知道它是什么,知道它大概的模样,也许未来真要用到了,也就会了。
可能理解最费劲的是 *args 以及 **kwargs 吧,在 __init____call__ 以及 _func 中出现的,含义各不相同。我们或许换个角度去理解会容易一些,就是完全分成两种情况,一个是 @cached 这种直接的用法时,这个 cached 内的逻辑是怎么走;另一个加参数@cached(*args, **kwargs) 的时候,内部的逻辑又是怎么走的。

最后也许还会感觉有些绕口、烧脑,那也没有关系,反正已经知道它是怎么回事了,在合适的场景中,用起来,复杂点的就多花点时间,反正,磨刀不误砍柴工,修饰器值得为此磨刀。

描述符 (Descriptor)

一般不大用到,了解即可。

描述符 (Descriptor) 是有趣的东西,如果对象内,有 __get____set____delete__ 任一的 Magic Methods,那么它就是 描述符 了。主要作用,相当于同一个东西,很多个面孔,它被不同性质的对象引用作为其子属性的时候,可以呈现出不同的值;就好像你跟点头之交打招呼,以及自己心爱之人打招呼,状态是不一样的,但都是同一个你,只是在他们眼中,已经是不同性质的你了。
当然,描述符 还有一个重要的作用,就是表示你被引用了

我们前面看到 @property 这个 修饰器 的用法,可以把一个函数直接转为普通的属性值,其实就用了描述符,当这个属性被引用的时候,发现有 __get__ 属性,那就先调用 __get__,在 __get__ 函数中把当前对象的值替换了
@property 是 Python 内置的一个方法,但是每次使用它的时候,就意味着需要运行一次 源函数,为了避免重复、浪费的计算,有些时候,我们需要在一个 Class 内使用 @cached_property,也就是运行一次后自动缓存的 property。

@cached_property 的实现代码,参考如下:

class cached_property(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, parent_instance, owner_class):
        # 初次调用,会运行 __get__ 函数,后续调用则走到了 __dict__ 的缓存
        # 非常巧妙的思路
        key = self.func.__name__
        result = self.func(parent_instance)
        parent_instance.__dict__[key] = result
        return result

    def __call__(self, *args, **kwargs):
        # 实际不会运行到的函数
        print('__call__ for cached_property')