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

浙公网安备 33010602011771号