• 浅谈对属性描述符__get__、__set__、__delete__的理解


    1、属性描述符的基础介绍

    1.1 何为属性描述符?

    属性描述符是一种Python语言中的特殊对象,用于定义和控制类属性的行为。属性描述符可以通过定义__get__、__set__、__delete__方法来控制属性的读取、赋值和删除操作。

    通过使用属性描述符,可以实现对属性的访问控制、类型检查、计算属性等高级功能。

    如果一个对象定义了这些方法中的任何一个,它就是一个描述符。

    看完上面的文字描述,是不是感觉一头雾水,没关系,接下来通过一个简单的案例来讲解属性描述符的作用。

    1.2 为什么需要属性描述符?

    假设我们现在要做一个成绩管理系统,在定义学生类时,我们可能这样写:

    class Student(object):
    
        def __init__(self, name, age, cn_score, en_score):
            self.name = name
            self.age = age
            self.cn_score = cn_score
            self.en_score = en_score
    
        def __str__(self):
            return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
    
    
    xiaoming = Student("xiaoming", 18, 70, 55)
    print(xiaoming)
    
    1.2.1 init函数中做参数校验

    因为python是动态语言类型,不像静态语言那样,可以给参数指定类型,所以在传参时,无法得知参数是否正确。比如,当cn_score传入的值为字符串时,程序并不会报错。这个时候,一般就会想到对传入的参数做校验,当传入的参数不符合要求时,抛错。

    class Student(object):
    
        def __init__(self, name, age, cn_score, en_score):
            self.name = name
            if not isinstance(age, int):
                raise TypeError("age must be int")
            if age <= 0:
                raise ValueError("age must be greater than 0")
            self.age = age
    
            if not isinstance(cn_score, int):
                raise TypeError("cn_score must be int")
            if 0 <= cn_score <= 100:
                raise ValueError("cn_score must be between 0 and 100")
            self.cn_score = cn_score
    
            if not isinstance(en_score, int):
                raise TypeError("en_score must be int")
            if 0 <= en_score <= 100:
                raise ValueError("en_score must be between 0 and 100")
            self.en_score = en_score
    
        def __str__(self):
            return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
    
    
    xiaoming = Student("xiaoming", -1, 70, 55)
    print(xiaoming)
    

    虽然上面的代码可以实现参数校验,但是过多的逻辑判断在初始化函数里面,会导致函数特别臃肿,当增加新的参数时,需要增加逻辑判断,一方面重复代码增加,另外也不符合开闭原则

    1.2.2 使用property做参数校验

    这个时候该怎么处理呢,我们知道python的内置函数 property可用于装饰方法,使方法之看起来像属性一样。我们可以借助此函数来优化代码,优化后如下:

    class Student(object):
    
        def __init__(self, name, age, cn_score, en_score):
            self.name = name
            self.age = age
            self.cn_score = cn_score
            self.en_score = en_score
    
        @property
        def age(self):
            return self.age
    
        @age.setter
        def age(self, value):
            if not isinstance(value, int):
                raise TypeError("age must be int")
            if value <= 0:
                raise ValueError("age must be greater than 0")
            self.age = value
    
        @property
        def cn_score(self):
            return self.cn_score
    
        @cn_score.setter
        def cn_score(self, value):
            if not isinstance(value, int):
                raise TypeError("cn_score must be int")
            if 0 <= value <= 100:
                raise ValueError("cn_score must be between 0 and 100")
            self.cn_score = value
    
        @property
        def en_score(self):
            return self.en_score
    
        @en_score.setter
        def en_score(self, value):
            if not isinstance(value, int):
                raise TypeError("en_score must be int")
            if 0 <= value <= 100:
                raise ValueError("en_score must be between 0 and 100")
            self.en_score = value
    
        def __str__(self):
            return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
    
    
    xiaoming = Student("xiaoming", -1, 70, 55)
    print(xiaoming)
    

    现在代码看起来已经挺不错的了,确实。但是想想平常开发中,我们使用Diango 的 ORM 时,定义model时,只需要定义 modle 的属性,就可以使其完成参数的校验,比如ip = models.CharField(max_length=20, db_index=True, verbose_name='IP')。这是怎么做到的呢?

    1.2.3 使用属性描述符做参数校验

    其实,Django 是使用到了Python的属性描述符 __get__、__set__。接下来,我们使用上面的两个方法,来进行改造。代码如下:

    class Score:
        def __init__(self, score):
            self.score = score
    
        def __get__(self, instance, owner):
            return self.score
    
        def __set__(self, instance, value):
            if not isinstance(value, int):
                raise TypeError("value must be int")
            if 0 <= value <= 100:
                self.score = value
            else:
                raise ValueError("value must be between 0 and 100")
    
    
    class Age:
    
        def __init__(self, age):
            self.age = age
    
        def __get__(self, instance, owner):
            return self.age
    
        def __set__(self, instance, value):
            if not isinstance(value, int):
                raise TypeError("age must be int")
            if value <= 0:
                raise ValueError("age must be greater than 0")
            self.age = value
    
    
    class Student(object):
    
        age = Age(0)
        cn_score = Score(0)
        en_score = Score(0)
    
        def __init__(self, name, _age, _cn_score, _en_score):
            self.name = name
            # 通过这里参数名称的区别,我们可以更加明确的知道,是调用
            self.age = _age
            self.cn_score = _cn_score
            self.en_score = _en_score
    
        def __str__(self):
            return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
    
    
    xiaoming = Student("xiaoming", -1, 70, 55)
    print(xiaoming)
    

    通过上面的定义,也能够实现之前的功能,而且代码重用度更高,看起来也更加简洁。

    1.3 属性描述符分类

    常见的属性描述符包括数据描述符和非数据描述符。

    • 数据描述符

    是指同时定义了__get__、__set__方法的属性描述符,它可以完全控制属性的读写操作。

    • 非数据描述符

    是指只定义了__get__方法的属性描述符,它只能控制属性的读取操作,而不能控制属性的赋值和删除操作。

    2、属性描述符的详细介绍

    2.1 属性描述符的调用时机

    描述符本质就是一个新式类,在这个新式类中,至少实现了__get__、__set__、__delete__中的一个,这也被称为描述符协议。

    • __get__():调用一个属性时,触发

    • __set__():为一个属性赋值时,触发

    • __delete__():采用del删除属性时,触发

    通过下面的例子将更加清晰的知道 属性描述符的调用时机。

    class Age:
    
        def __init__(self, age):
            self.age = age
    
        def __get__(self, instance, owner):
            print("coming __get__")
            return self.age
    
        def __set__(self, instance, value):
            print("coming __set__")
            if not isinstance(value, int):
                raise TypeError("age must be int")
            if value <= 0:
                raise ValueError("age must be greater than 0")
            self.age = value
    
        def __delete__(self, instance):
            print("coming __del__")
            del self.age
    
    
    class Student(object):
    
        age = Age(0)
    
        def __init__(self, name):
            self.name = name
    
    
    xiaoming = Student("xiaoming")
    xiaoming.age = 9
    print(xiaoming.age)
    del xiaoming.age
    
    
    #################
    结果:
    coming __set__
    coming __get__
    coming __del__
    
    

    2.2 属性的搜索顺序

    这里跟属性描述符关系不是特别大,主要是看看属性的搜索顺序。

    默认的属性访问是从对象的字典中 get, set, 或者 delete 属性。例如a.x的查找顺序是:

    a.__getattribute__() -> a.__dict__['age'] -> type(a).__dict__['age'] -> type(a)的基类(不包括元类)-> a.__getattr__ -> 抛错

    如果查找的值是对象定义的描述方法之一,python可能会调用描述符方法来重载默认行为,发生在这个查找环节的哪里取决于定义了哪些描述符方法。

    1、非数据描述器,实例的属性搜索顺序如下:

    a.__getattribute__() -> a.__dict__['age'] -> a.__get__() -> type(a).__dict__['age'] -> type(a)的基类(不包括元类)-> a.__getattr__ -> 抛错

    class Age(object):
        def __get__(self, instance, owner):
            print("coming __get__")
            return "__get__"
    
        # def __set__(self, instance, value):
        #     print("coming __set__")
        #     self.age = value
    
    
    class A2(object):
    
        age = 10
        def __init__(self):
    
            self.age = 1000
    
    
    class A(object):
    
        age = Age()
    
        def __init__(self):
            super().__init__()
    
        # def __getattribute__(self, item):
        #     print("coming __getattribute__")
        #     return "xxx"
        #
        def __getattr__(self, item):
            print("coming __getattr__")
            return "__getattr__"
    
    
    a = A()
    print(a.age)
    

    2、数据描述器,实例的属性搜索顺序如下:

    a.__getattribute__() -> a.__get__() -> a.__dict__['age'] -> type(a).__dict__['age'] -> type(a)的基类(不包括元类)-> a.__getattr__ -> 抛错

    class Age(object):
        def __get__(self, instance, owner):
            print("coming __get__")
            return "__get__"
    
        def __set__(self, instance, value):
            print("coming __set__")
            self.age = value
    
    
    class A2(object):
    
    
        def __init__(self):
    
            self.age = 1000
    
    
    class A(object):
    
        age = Age()
    
        def __init__(self):
            self.age = 100
            super().__init__()
    
        # def __getattribute__(self, item):
        #     print("coming __getattribute__")
        #     return "xxx"
        #
        def __getattr__(self, item):
            print("coming __getattr__")
            return "__getattr__"
    
    
    a = A()
    print(a.age)
    

    参考链接:

    【案例讲解】Python为什么要使用描述符?

    属性描述符:__get__函数、__set__函数和__delete_函数


    __EOF__

  • 本文作者: 画个一样的我
  • 本文链接: https://www.cnblogs.com/huageyiyangdewo/p/17311733.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【新书推荐】AI时代,当程序员遇到ChatGPT,开发效率飞起来!
    【论文阅读】AID(ICCV‘23)
    Cocos Creator 3.6 新特性详解 3/3:性能篇
    数字孪生赋能智能矿山信息化、可视化化、智能化解决方案
    雅思口语同替高分表达
    在unity中利用公开变量引用物体和组件(有手就会)
    Java中如何将String类型的2023年09月21日这个值变成DATE相关的类型
    Tornado 异步性能分析
    111111111
    CTFshow wbe41 教你写脚本
  • 原文地址:https://www.cnblogs.com/huageyiyangdewo/p/17311733.html