python descriptor 详解
descriptor简介
这三个特殊的函数签名是这样的:
object.
__get__
(self, instance, owner):return value
object.
__set__
(self, instance, value):return None
object.
__delete__
(self, instance): return None
1 # -*- coding: utf-8 -*- 2 class Des(object): 3 def __init__(self, init_value): 4 self.value = init_value 5 6 def __get__(self, instance, typ): 7 print('call __get__', instance, typ) 8 return self.value 9 10 def __set__(self, instance, value): 11 print ('call __set__', instance, value) 12 self.value = value 13 14 def __delete__(self, instance): 15 print ('call __delete__', instance) 16 17 class Widget(object): 18 t = Des(1) 19 20 def main(): 21 w = Widget() 22 print type(w.t) 23 w.t = 1 24 print w.t, Widget.t 25 del w.t 26 27 if __name__=='__main__': 28 main()
运行结果如下:
('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
<type 'int'>('call __set__', <__main__.Widget object at 0x02868570>, 1)
('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
1 ('call __get__', None, <class '__main__.Widget'>)1
('call __delete__', <__main__.Widget object at 0x02868570>)
从输出结果可以看到,对于这个三个特殊函数,形参instance是descriptor实例所在的类的实例(w), 而形参owner就是这个类(widget)
descriptor注意事项
需要注意的是, descriptor的实例一定是类的属性,因此使用的时候需要自行区分实例。比如下面这个例子,我们需要保证以下属性不超过一定的阈值。
1 class MaxValDes(object): 2 def __init__(self, inti_val, max_val): 3 self.value = inti_val 4 self.max_val = max_val 5 6 def __get__(self, instance, typ): 7 return self.value 8 9 def __set__(self, instance, value): 10 self.value= min(self.max_val, value) 11 12 class Widget(object): 13 a = MaxValDes(0, 10) 14 15 if __name__ == '__main__': 16 w0 = Widget() 17 print 'inited w0', w0.a 18 w0.a = 123 19 print 'after set w0',w0.a 20 w1 = Widget() 21 print 'inited w1', w1.a
代码很简单,我们通过MaxValDes这个descriptor来保证属性的值不超过一定的范围。运行结果如下:
inited w0 0
after set w0 10
inited w1 10
可以看到,对w0.a的赋值符合预期,但是w1.a的值却不是0,而是同w0.a一样。这就是因为,a是类Widget的类属性, Widget的实例并没有'a'这个属性,可以通过__dict__查看。
那么要怎么修改才符合预期呢,看下面的代码:
1 class MaxValDes(object): 2 def __init__(self, attr, max_val): 3 self.attr = attr 4 self.max_val = max_val 5 6 def __get__(self, instance, typ): 7 return instance.__dict__[self.attr] 8 9 def __set__(self, instance, value): 10 instance.__dict__[self.attr] = min(self.max_val, value) 11 12 class Widget(object): 13 a = MaxValDes('a', 10) 14 b = MaxValDes('b', 12) 15 def __init__(self): 16 self.a = 0 17 self.b = 1 18 19 if __name__ == '__main__': 20 w0 = Widget() 21 print 'inited w0', w0.a, w0.b 22 w0.a = 123 23 w0.b = 123 24 print 'after set w0',w0.a, w0.b 25 26 w1 = Widget() 27 print 'inited w1', w1.a, w1.b
运行结果如下:
inited w0 0 1
after set w0 10 12
inited w0 0 1
可以看到,运行结果比较符合预期,w0、w1两个实例互不干扰。上面的代码中有两点需要注意:
第一:第7、10行都是通过instance.__dict__来取值、赋值,而不是调用getattr、setattr,否则会递归调用,死循环。
第二:现在类和类的实例都拥有‘a’属性,不过w0.a调用的是类属性‘a',具体原因参见下一篇文章
descriptor应用场景
They are the mechanism behind properties, methods, static methods, class methods, and super()
. They are used throughout Python itself to implement the new style classes introduced in version 2.2.
1 class TestProperty(object): 2 def __init__(self): 3 self.__a = 1 4 5 @property 6 def a(self): 7 return self.__a 8 9 @a.setter 10 def a(self, v): 11 print('output call stack here') 12 self.__a = v 13 14 if __name__=='__main__': 15 t = TestProperty() 16 print t.a 17 t.a = 2 18 print t.a
如果需要禁止对属性赋值,或者对新的值做检查,也很容易修改上面的代码实现
既然有了property,那什么时候还需要descriptor呢?property最大的问题在于不能重复使用,即对每个属性都需要property装饰,代码重复冗余。而使用descriptor,把相同的逻辑封装到一个单独的类,使用起来方便多了。详细的示例可以参见这篇文章。
1 import functools, time 2 class cached_property(object): 3 """ A property that is only computed once per instance and then replaces 4 itself with an ordinary attribute. Deleting the attribute resets the 5 property. """ 6 7 def __init__(self, func): 8 functools.update_wrapper(self, func) 9 self.func = func 10 11 def __get__(self, obj, cls): 12 if obj is None: return self 13 value = obj.__dict__[self.func.__name__] = self.func(obj) 14 return value 15 16 class TestClz(object): 17 @cached_property 18 def complex_calc(self): 19 print 'very complex_calc' 20 return sum(range(100)) 21 22 if __name__=='__main__': 23 t = TestClz() 24 print '>>> first call' 25 print t.complex_calc 26 print '>>> second call' 27 print t.complex_calc
>>> first callvery complex_calc4950>>> second call4950
第一,在访问complex_calc的时候并没有使用函数调用(没有括号);
references