descriptor理解和例程

目录

descriptor简介

descriptor注意事项

descriptor应用场景


descriptor简介

        在python中,如果一个新式类定义了__get__() , __set__() , __delete__() 方法中的一个或者多个,那么称之为descriptor。

        python中的描述符可以用来定义触发自动执行的代码,它像是一个对象属性操作(访问、赋值、删除)的代理类一样。

        descriptor有分为data descriptor和non-data discriptor。

        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

descriptor实现的大致流程如下:

  1. 定义一个描述符类D,其内包含一个或多个__get__() , __set__() , __delete__() 方法
  2. 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr = D()
  3. 之后访问、赋值、删除attr属性,将会自动触发描述符类中的__get__() , __set__() , __delete__() 方法

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

# _*_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))
    print(w.t)
    print(Widget.t)
    del w.t
 
if __name__=='__main__':
    main()

运行结果如下:

call __get__ <__main__.Widget object at 0x000001A83812DC88> <class '__main__.Widget'>
<class 'int'>
call __get__ <__main__.Widget object at 0x000001A83812DC88> <class '__main__.Widget'>
1
call __get__ None <class '__main__.Widget'>
1
call __delete__ <__main__.Widget object at 0x000001A83812DC88>

        从输出结果可以看到,对于这个三个特殊函数,形参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)
 
    w2 = Widget()
    print('inited w2:',w2.a)
    
    w1 = Widget()
    print('inited w1:', w1.a, w1.b)
    w1.a = 5
    w1.b = 5
    print('after set w1:',w1.a, w1.b)

 运行结果如下:

inited w0: 0 1
after set w0: 10 12
inited w2: 0
inited w1: 0 1
after set w1: 5 5

        可以看到,运行结果比较符合预期,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)

运行结果如下:

1
output call stack here
2       

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

        既然有了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的属性查找顺序.

本文版权归作者xybaby(博文地址:http://www.cnblogs.com/xybaby/)所有,欢迎转载和商用,请在文章页面明显位置给出原文链接并保留此段声明,否则保留追究法律责任的权利,其他事项,可留言咨询。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值