您的位置:首页 > 编程语言 > Python开发

python descriptor 详解

2017-01-09 21:53 323 查看

descriptor简介

  在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor。descriptor有分为data descriptor与non-data descriptor, descriptor通常用来改变默认的属性访问(attribute lookup),这部分会在下一遍文章中介绍。注意 ,descriptor的实例是一定是的属性(class attribute)
  这三个特殊的函数签名是这样的:

  object.
__get__
(self, instance, owner):return value
  object.
__set__
(self, instance, value):return None
  object.
__delete__
(self, instance): return None
  

  下面的代码展示了简单的用法:

# -*- coding: utf-8 -*-
class Des(object):
def __init__(self, init_value):
self.value = init_value

def __get__(self, instance, typ):
print('call __get__', instance, typ)
return self.value

def __set__(self, instance, value):
print ('call __set__', instance, value)
self.value = value

def __delete__(self, instance):
print ('call __delete__', instance)

class Widget(object):
t = Des(1)

def main():
w = Widget()
print type(w.t)
w.t = 1
print w.t, Widget.t
del w.t

if __name__=='__main__':
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)

  w.t 等价于 Pro.__get__(t, w, Widget).而Widget.t 等价于 Pro.__get__(t, None, Widget)

descriptor注意事项

  需要注意的是, descriptor的实例一定是类的属性,因此使用的时候需要自行区分实例。比如下面这个例子,我们需要保证以下属性不超过一定的阈值。

class MaxValDes(object):
def __init__(self, inti_val, max_val):
self.value = inti_val
self.max_val = max_val

def __get__(self, instance, typ):
return self.value

def __set__(self, instance, value):
self.value= min(self.max_val, value)

class Widget(object):
a = MaxValDes(0, 10)

if __name__ == '__main__':
w0 = Widget()
print 'inited w0', w0.a
w0.a = 123
print 'after set w0',w0.a
w1 = Widget()
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__查看。

  那么要怎么修改才符合预期呢,看下面的代码:

class MaxValDes(object):
def __init__(self, attr, max_val):
self.attr = attr
self.max_val = max_val

def __get__(self, instance, typ):
return instance.__dict__[self.attr]

def __set__(self, instance, value):
instance.__dict__[self.attr] = min(self.max_val, value)

class Widget(object):
a = MaxValDes('a', 10)
b = MaxValDes('b', 12)
def __init__(self):
self.a = 0
self.b = 1

if __name__ == '__main__':
w0 = Widget()
print 'inited w0', w0.a, w0.b
w0.a = 123
w0.b = 123
print 'after set w0',w0.a, w0.b

w1 = Widget()
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应用场景

  其实从上面的例子可以看出,descriptor主要用于控制属性的访问(读、写、删除)。python doc里面有写到,property()就是一个data descriptor实现(可参见这个文档)。 python2.2中,大量新式类的实现都基于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.


  在实践中,我们有可能需要监控或者限制对属性的访问。比如,对象的一个属性被“莫名其妙”地修改了,但搜索所有文件有找不到可以的地方,那么我们可以通过__setattr__(self, k, v)方法,对于我们关心的 k 打印出调用栈。另外,也可以用property,示例代码如下:

class TestProperty(object):
def __init__(self):
self.__a = 1

@property
def a(self):
return self.__a

@a.setter
def a(self, v):
print('output call stack here')
self.__a = v

if __name__=='__main__':
t = TestProperty()
print t.a
t.a = 2
print t.a


  如果需要禁止对属性赋值,或者对新的值做检查,也很容易修改上面的代码实现

  既然有了property,那什么时候还需要descriptor呢?property最大的问题在于不能重复使用,即对每个属性都需要property装饰,代码重复冗余。而使用descriptor,把相同的逻辑封装到一个单独的类,使用起来方便多了。详细的示例可以参见这篇文章

  笔者之前看bottle.py源码的时候,看到这么一个descriptor使用,部分源代码和测试代码如下:

import functools, time
class cached_property(object):
""" A property that is only computed once per instance and then replaces
itself with an ordinary attribute. Deleting the attribute resets the
property. """

def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func

def __get__(self, obj, cls):
if obj is None: return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value

class TestClz(object):
@cached_property
def complex_calc(self):
print 'very complex_calc'
return sum(range(100))

if __name__=='__main__':
t = TestClz()
print '>>> first call'
print t.complex_calc
print '>>> second call'
print t.complex_calc


  运行结果如下:

>>> first call
very complex_calc
4950
>>> second call
4950


  注意两点:
  第一,在访问complex_calc的时候并没有使用函数调用(没有括号);

  第二,第一次调用的时候打印了“very complex_calc”,第二次没有。

  笔者也是因为这段代码开始学习descriptor,但看懂这段代码还需要了解Python的属性查找顺序,下一篇文章会对此简单介绍。

references

(0)Implementing Descriptors, python2.7 doc
(1)Descriptor HowTo Guide, https://docs.python.org/2/howto/descriptor.html#descriptor-protocol

(2)Python描述符(descriptor)解密, http://www.geekfan.net/7862/
(3)bottlepy,http://www.bottlepy.org/docs/dev/
(4)bottle.py(源码),https://raw.githubusercontent.com/bottlepy/bottle/master/bottle.py
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: