Python的property可以让我们很方便地将一个object的函数当做一个属性来操作,可以赋值、读取。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Parrot: def __init__(self): self._voltage = 100000 @property def voltage(self): """Get the current voltage.""" return self._voltage p = Parrot() p.voltage # 100000 |
但是不支持class,如果想使用这个特性,就要实例化对象。我去参考了celery实现的class_property,发现有一行特别难懂,花了很长时间琢磨,终于是看懂了。
celery的代码如下:
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 |
class class_property(object): def __init__(self, getter=None, setter=None): if getter is not None and not isinstance(getter, classmethod): getter = classmethod(getter) if setter is not None and not isinstance(setter, classmethod): setter = classmethod(setter) self.__get = getter self.__set = setter info = getter.__get__(object) # just need the info attrs. self.__doc__ = info.__doc__ self.__name__ = info.__name__ self.__module__ = info.__module__ def __get__(self, obj, type=None): if obj and type is None: type = obj.__class__ return self.__get.__get__(obj, type)() def __set__(self, obj, value): if obj is None: return self return self.__set.__get__(obj)(value) def setter(self, setter): return self.__class__(self.__get, setter) |
使用方法如下:
1 2 3 4 5 6 7 |
class Logging: @class_property def already_setup(self): return True Logging.already_setup # True |
这段代码能work,而是没有任何问题。装饰在class内的一个函数上,可以将此函数的返回值作为一个class的属性,可以通过class和instance来访问,并且还支持setter。
首先,__init__()
比较好理解,传入getter和setter,将其转换为classmethod然后绑定到内部的变量上,以便后来调用。
setter()
方法也比较好理解,可以当做装饰器调用, 然后已有的self.__get
和传入的setter重新初始化一个class_property,如果已经有getter的话,这里也不会对getter造成影响。参考下面的例子:
1 2 3 |
@already_setup.setter # noqa def already_setup(self, was_setup): self._setup = was_setup |
然后看__get__
和__set__
方法,很明显,这里使用的描述器。(关于描述器,我之前有一篇博文详细讲了描述器——理解Python对象的属性和描述器。)第一个if判断是为了确保用class_property
装饰的属性通过instance也能访问到。
最后到了最让人不理解的部分了,如果要我实现的话,是直接返回getter就可以了:
return self.__get()
这样看起来也没问题:访问Logging类的时候,发现没有这个already_setup
这个属性,然后试图访问描述器,描述器__get__()
中返回self.__get()
不正好是getter会返回的结果吗?
于是我将这行return改成了上面我的意思,结果报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-20-6db509700877> in <module>() ----> 1 class_property(Logging.already_setup)() <ipython-input-14-9345a093811c> in __get__(self, obj, type) 20 print(self.__get) 21 print(self.__get.__get__(obj, type)) ---> 22 return self.__get(obj) 23 24 def __set__(self, obj, value): TypeError: 'classmethod' object is not callable |
classmethod为什么不是callable的呢?因为classmethod本来就不是callable的!在之前的文章中,我模拟了classmethod的实现:
1 2 3 4 5 6 7 8 9 10 11 12 |
class ClassMethod(object): "Emulate PyClassMethod_Type() in Objects/funcobject.c" def __init__(self, f): self.f = f def __get__(self, obj, klass=None): if klass is None: klass = type(obj) def newfunc(*args): return self.f(klass, *args) return newfunc |
它的原理是这样的:Class读一个属性的时候,如果Class.__dict__
有该属性并且属性不是一个描述器,就返回该属性;如果该属性是一个描述器,就调用它的__get__
方法。
举个例子说,下面的代码:
1 2 3 4 5 6 7 |
class Foo: @classmethod def bar(cls): return "hello" Foo.bar() # hello |
在调用的时候,先看Foo.__dict__里面的bar是一个描述器,那么调用就转换成:
Foo.__dict__['bar'].__get__(None, Foo)()
回到我们的class_property上来,拿最初的Logging举例。用了class_property装饰的already_setup
使用起来(Logging.already_setup
)就是:
Logging.__dict__['already_setup'].__get__(None, Logging)()
执行我们的class_property的__get__
之后,也就是
Logging.__dict__['already_setup'].__get()
此时,橙色的部分拿到的是一个class_property处理之后的classmethod,而上面我们可以看到,classmethod并不是callable的,而是通过描述器协议,class在调用的时候会默认调用__get__
,然后拿到一个和class绑定之后的func,这时候的func才是可以调用的。
celery里面原来写的return,调用链展开就是:
Logging.__dict__['already_setup'].__get.__get__(obj, type)()
其中橙色的部分依然是拿到了classmethod,即:
Logging.classmethod_get.__get__(obj, type)()
classmethod的__get__()
返回的是一个unbound method,即
Logging.already_setup()
这样调用就没问题了。
所以说,这里手动调用__get__(obj, type)
的原因是,描述器协议之能调用一次,如果返回的还是一个描述器,那么并不会继续调用__get__方法,需要手动调用。