python编程之数据描述器

  第一次实际接触描述符是在我们项目中我们需要实现自己的Manger管理器从而在查询时加上特定条件,所以去看了Django ORM的源码就打开了对数据描述符的大门。

  要入门描述符,我们首先要知道几个前提的知识:

  1. 什么是描述器?
  2. 什么是数据描述器?
  3. 什么是非数据描述器?
  4. 属性被调用时,属性访问的顺序?

 

01、什么是描述器

  python官方的解释是:一般地,一个描述器是一个包含 “绑定行为” 的对象,对其属性的访问被描述器协议中定义的方法覆盖。这些方法有:__get__()__set__() 和 __delete__()。如果某个对象中定义了这些方法中的任意一个,那么这个对象就可以被称为一个描述器。

  通俗的讲,描述器其实就是一个类, 这个类至少要实现__get__()__set__() 和 __delete__()中的一个。

class Descriptor:
    def __init__(self, default):
        self.value = default
        self.attr_dict = dict()

    def __get__(self, instance, owner):
        # 在这里instance等于Digit的实例子, owner等于Digit
        return self.attr_dict[instance]

    def __set__(self, instance, value):
        self.attr_dict[instance] = value


class Digit:
    num = Descriptor(0)

    def __init__(self, num):
        self.num = num

  在上图代码中 Descriptor 类实现了__get__()__set__() 和 __delete__()中的一个,所以是一个描述器。

 

02、什么是数据描述器

  数据描述器:只要实现了__get__()__set__() 和 __delete__()中的任意两个,就称为数据描述器。

 

03、什么是非数据描述器

  非数据描述器: 只是实现了__get__()__set__() 和 __delete__()中的一个,就称为非数据描述器。

 

04、属性被调用时,属性访问的顺序

  ① __getattribute__(), 无条件调用
  ② 数据描述符:由 ① 触发调用 (若人为的重载了该 __getattribute__() 方法,可能会调职无法调用描述符)
  ③ 实例对象的字典(若与描述符对象同名,会被覆盖哦)
  ④ 类的字典
  ⑤ 非数据描述符
  ⑥ 父类的字典
  ⑦ __getattr__() 方法
 
  根据这个顺序我们知道,如果描述器是一个数据描述器时,当描述器的属性名和实例的属性重名时,如上代码中Digit类属性num和实例属性num重名了, 这个时候如果Digit类的实例访问num属性,其实是访问数据描述器的__get__放法的。自然如果Descriptor描述器如果只实现了__get__方法即为非数据描述器,这是Digit类的实例访问num属性,访问的即是实例自己的num属性。并且根据这个属性访问顺序我们可以知道,如果重写__getattribute__()是可以让数据描述失效的,这个大家可以测试一下。
 
 
  当我们了解了上述知识后,我们就想知道这个描述器有怎样的应用场景呢?
python官方的应用场景有:函数、属性、静态方法和类方法,也就是python的这些底层实现是用到了描述器的,后续我们将自己来实现python自带的property描述器。
我们平时代码中的应用场景有:数据校验,还有ORM Manager的只让类调用,不能是实例调用。
 

05、ORM中ManagerDescriper的实现

  我们在使用Django ORM语句对数据库进行操作时,都是用类.objects.get()为什么不能是instance.objects.get()呢?这是因为ManagerDescriper描述器中对调用者做了限制,看代码。
class ManagerDescriptor:

    def __init__(self, manager):
        self.manager = manager

    def __get__(self, instance, cls=None):
        if instance is not None:           # 这里表示不允许用实例去调用onjects只能用类调用
            raise AttributeError("Manager isn't accessible via %s instances" % cls.__name__)

        if cls._meta.abstract:
            raise AttributeError("Manager isn't available; %s is abstract" % (
                cls._meta.object_name,
            ))

        if cls._meta.swapped:
            raise AttributeError(
                "Manager isn't available; '%s.%s' has been swapped for '%s'" % (
                    cls._meta.app_label,
                    cls._meta.object_name,
                    cls._meta.swapped,
                )
            )

        return cls._meta.managers_map[self.manager.name]

  这里真的设计的真的很精妙,可能很多朋友看到这有点懵逼objects是什么?ManagerDescriptor是怎样被调用的,因为本节主讲描述器相关的知识,所以这些可去查询ORM相关的源码。

 

06、数据校验

  当我做教学管理系统时,我们有一个学生类用于记录每个学生每科的成绩,我们要求记录记录的成绩不能是负数,这时我们学生类的代码可能如下:

class Student:

    def __init__(self, chinese, math):
        self.chinese = chinese if chinese >= 0 else 0
        self.math = math if chinese >= 0 else 0

  试想如果是这种写法的话,如果我们对于分数多几个校验条件,或者多几个科目,这个代码整体将会非常的臃肿和难看。

 

  这时我们的脑海可能立马闪现出用python自带的property来装饰科目属性,就可以省去大量的重复代码了,这样就有我们的2.0代码

class Student:

    def __init__(self, chinese, math):
        self._chinese = None
        self._math = None
        self.chinese = chinese
        self.math = math

    @property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if value >= 0:
            self._math = value
        else:
            self._math = 0

    @property
    def chinese(self):
        return self._chinese

    @chinese.setter
    def chinese(self, value):
        if value >= 0:
            self._chinese = value
        else:
            self._chinese = 0


c = Student(1, -1)

 

  这时我们如果有多种校验时能让代码简洁很多,可是还是没能解决多科目的情况下重复代码的问题,并且带来一个额外的开销是要创建每个科目的额外变量来存储分数。

  这时就是我们的描述器上场了。

class Score:

    def __init__(self):
        self.score = dict()

    def __get__(self, instance, owner):
        if not self.score[instance]:
            return 0
        return self.score[instance]

    def __set__(self, instance, value):
        if value >= 0:
            self.score[instance] = value
        else:
            self.score[instance] = 0


class Student:
    chinese = Score()
    math = Score()

    def __init__(self, chinese, math):
        self.chinese = chinese
        self.math = math


c = Student(1, -1)
print(c.math)

  这里我们用Score描述器去存储各科目的分数,完美的解决了多科目时的数据校验问题, 不过它也有局限性,这里能这样用的前提是, 每个科目的检验方式是相同的,不然就得新建一个描述器去储存分数值,在描述器中我们用instance作为字典的键,这是有一定局限性的,如果instance的类是一个list类型等可变类型就不能把instance作为键。

  看到这里有没有发现,有点像ORM的Model类了,哈哈,自己去看看源码。

 

07、用描述器实现property

class Property:
    def __init__(self, fget=None, fset=None, fdelete=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdelete

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(instance)

    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(instance, value)

    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(instance)

    def getter(self, func):
        return type(self)(func, self.fset, self.fdel)

    def setter(self, func):
        return type(self)(self.fget, func, self.fdel)

    def deleter(self, func):
        return type(self)(self.fget, self.fset, func)


class Student:

    def __init__(self, chinese, math):
        self._chinese = None
        self._math = None
        self.chinese = chinese
        self.math = math

    @Property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if value >= 0:
            self._math = value
        else:
            self._math = 0

    @Property
    def chinese(self):
        return self._chinese

    @chinese.setter
    def chinese(self, value):
        if value >= 0:
            self._chinese = value
        else:
            self._chinese = 0


c = Student(1, -1)
print(c.math)
c.math = 1

上面代码中的Property是python代码的实现,发现它本质上用的就是描述器。

其他python底层方法实现可以参考官网https://docs.python.org/zh-cn/3/howto/descriptor.html

posted @ 2020-06-28 18:26  种树飞  阅读(335)  评论(0编辑  收藏  举报