美文网首页
Python面向对象 - 描述器

Python面向对象 - 描述器

作者: 大聖Jonathan | 来源:发表于2018-03-19 06:41 被阅读0次

    描述器

    什么是描述器?描述器是干什么用的?

    现在有一个Person类来表示人。其中有两个属性,体重weight和身高height。我们都知道,人体的身高和体重的数据是变化的,但不管怎么变,都不可能是小于0。因此对于Person的使用者,我们希望在设置Person类的weight和hight的时候,可以避免将它们设置为负数。试想一下,如果这两个属性,就是普通的属性,那在类的外部就没有任何限制,也就是完全可以给它们设置负值,这是我们不希望看到的。如果是两个方法,我们就完全可以在方法内对负值进行限制。所以为了实现上述的功能,基本思路有以下两种:

    1. 把对属性的访问,变成对方法的调用。例如,将weight和height定义成私有(__weight,__height),而后通过对应的get和set方法进行读取和设置。
    2. 原理同1。只不过对于使用者而言,和使用属性没有差异,解释器会帮忙转化为对方法的调用。

    综上,描述器首先是一个对象,而后通过这个对象来描述别的对象的属性。把对对象属性的增、删、改、查操作转化为描述器上定义的方法的调用。(看着是不是有点拗口?没关系,后面通过实例就能很好的帮助理解。)

    描述器实现方式一

    我们可以通过装饰器property,把属性的调用转化成方法的调用。下面以weight为例:

    class Person(object):
        def __init__(self):
            self.__weight = 0
    
        @property
        def weight(self):
            return self.__weight
    
        @weight.setter
        def weight(self, value):
            if value < 0:
                raise ValueError("Value can't be less than zero.")
    
            self.__weight = value
    
        @weight.deleter
        def weight(self):
            del self.__weight
    
    
    if __name__ == '__main__':
        p = Person()
        p.weight = 10 # ok
        print(p.weight)
    
        p.weight = -10 # ValueError: Value can't be less than zero.
    

    描述器实现方式二

    上述的方法,可以完美的解决设置负值的问题。现在我们需要为height实现同样的逻辑。按照上述的方法,我们只需要添加另一个私有属性__height,并且添加它的get和set方法即可。然而,问题是加上__weight的set和get方法,Person类中有两套get和set方法,并且这两套的逻辑是一样的,这就造成了代码的重复。接下来,介绍的这种实现方式可以更优雅的解决代码重用的问题。

    只需要实现类中定义的get,__set__,delete方法,那么这个类就是一个描述器,就可用来描述别的类的属性。顾名思义,我们把对属性的查询,添加和修改以及删除的逻辑封装在上述三个方法中。 所以,描述器的类结构如下:

    class GreaterThanZero(object):
        def __get__(self, instance, owner):
            pass
    
        def __set__(self, instance, value):
            pass
    
        def __delete__(self, instance):
            pass
    
    
    class Person(object):
        weight = GreaterThanZero()
        height = GreaterThanZero()
    

    通过上面的示例代码,清晰可见:描述器GreaterThanZero中包含get, __set__, delete的实现,Person类中定义两个类属性weight和height,它们的类型是GreaterThanZero。这就是一个构造器的实现和使用方式。那么,这里可能会有下面几个问题:

    1. 这里weight和height为什么是类属性?可不可以是实例属性?
    2. 上述方法中,self/instance/owner分别是指什么?
    3. 对应的Person类中,weight和height的值应该保存在哪里?

    下面就一一解决这个几个问题:

    1. 这里weight和height为什么是类属性?可不可以是实例属性?

    这里必须是类属性。上面有提到,描述器作用是把对属性的调用转换成对方法的调用。如果是定义成实例属性,那么解释器并不会做这种转化,也就没有描述器的意义。

    下面来验证一下:

    height和weight设置为实例属性
    class GreaterThanZero(object):
        def __get__(self, instance, owner):
            print(self, instance, owner)
            pass
    
        def __set__(self, instance, value):
            pass
    
        def __delete__(self, instance):
            pass
    
    
    class Person(object):
        def __init__(self):
            self.height = GreaterThanZero()
    
    if __name__ == '__main__':
        p = Person()
        print(p.height)
    

    上面的代码,没有打印self,instance和owner,可见get方法并没有执行,这样就验证了上文提到的,必须讲属性定义成类属性。下面来看一下正确的使用方式,并且也回答一下问题2:

    height和weight设置为实例属性
    class GreaterThanZero(object):
        def __get__(self, instance, owner):
            print(self, instance, owner)
            pass
    
        def __set__(self, instance, value):
            pass
    
        def __delete__(self, instance):
            pass
    
    
    class Person(object):
        height = GreaterThanZero()
    
    
    if __name__ == '__main__':
        p = Person()
        print(p.height)
    

    上面的的例子中,get被调用,并且我们可以知道self, instance和owner分别代表:

    • self: <main.GreaterThanZero object at 0x000002687B5CA160> - GreaterThanZero实例属性
    • instance:<main.Person object at 0x000002687B5CA208> - Person实例属性
    • owner:<class 'main.Person'> - Person类

    3. 对应的Person类中,weight和height的值应该保存在哪里?

    既然已经了解了self, instance和owner分别代表什么,那么对于数据的保存就有两个选择:1) 存放在self中,也就是GreaterThanZero实例中 2) 存放在Person实例中 。

    实际上存在哪个实例中,可能得看具体的使用场景,下面分别通过两个例子来说明。

    数据存放在instance中
    class GreaterThanZero(object):
        def __get__(self, instance, owner):
            return instance.data
    
        def __set__(self, instance, value):
            if value < 0:
                raise ValueError("Value can't be less than zero.")
            instance.data = value
    
        def __delete__(self, instance):
            del instance.data
    
    class Person(object):
        height = GreaterThanZero()
    
    
    if __name__ == '__main__':
        p = Person()
        p.height = 180
        print(p.height)
    
    

    上述代码,将会输入height值为180。如果设置了负值,则会引发异常。上面只是添加了height属性,那么weight属性要怎么办?如果和height一样,直接创建一个类属性,那么weight和height属性之间的值就会相互覆盖,原因是上述的保存方式很简单粗暴,就是直接存放在instance.data中,而不同的属性对应的instance又是同一个,因此会相互覆盖。下面,我们把数据存放在self中,并通过instnace来索引,即可解决这个问题(参考下面的代码,思考一下是否也可以用同样的方式,把数据存放在instance中?)。

    数据存放在self中
    class GreaterThanZero(object):
    
        def __init__(self):
            self.data = {}
    
        def __get__(self, instance, owner):
            key = id(instance)
            if key in self.data:
                return self.data[key]
            raise KeyError('No such attribute.')
    
        def __set__(self, instance, value):
            if value < 0:
                raise ValueError("Value can't be less than zero.")
            key = id(instance)
            self.data[key] = value
    
        def __delete__(self, instance):
            key = id(instance)
            if key in self.data:
                del self.data[key]
            raise KeyError('No such attribute.')
    
    class Person(object):
        height = GreaterThanZero()
        weight = GreaterThanZero()
    
    if __name__ == '__main__':
        p = Person()
        p.height = 170
        p.weight = 70
        print(p.height, p.weight) # 170 70
    
        p.height = 180
        p.weight = 80
        print(p.height, p.weight) # 180 80
    
        p1 = Person()
        p1.height = 120
        p1.weight = 50
        print(p1.height, p1.weight) # 120 50
    
        print(p.height, p.weight) # 180 80
    

    可上面的结果可知,height和weight的判断逻辑是共享的,并且相互之间的值不会互相影响。一图胜千言,下面这个图是上面代码的一个引用示例:

    imageimage

    从图中可知:

    • height是Person的类属性,因此Person的所有实例来访问height,都是访问到同一个GreaterThanZero实例
    • data是GreaterThanZero实例中的一个字典,以Person实例的地址作为key,以具体的height的值作为value
    • 换句话说,data存放的是Person实例的一个特定的属性的值,而且是所有Person实例的同名属性的值
    • weight与height类似,遵循相同的思路

    相关文章

      网友评论

          本文标题:Python面向对象 - 描述器

          本文链接:https://www.haomeiwen.com/subject/pqfqqftx.html