修饰器 (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。当我们说调用的时候,实际上是运行了()
,如果一个对象有一个 __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) 是有趣的东西,如果对象内,有 __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')