Python面向对象 - 描述器

描述器

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

现在有一个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: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的判断逻辑是共享的,并且相互之间的值不会互相影响。一图胜千言,下面这个图是上面代码的一个引用示例:

Python面向对象 - 描述器_第1张图片
image

从图中可知:

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

你可能感兴趣的:(Python面向对象 - 描述器)