Python-描述符使用示例

发布于 2020-05-24  668 次阅读


为什么要使用描述符?

假设你正在写一个成绩管理系统,并没有太多编程经验的你可能这么写

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    def __str__(self):
        return 'name:{} math:{} chinese:{} english:{}'.format(self.name, self.math, self.chinese, self.english)

s = Student('zhangsan', 70, 30, 100)
print(s)

###############################################################
name:zhangsan math:70 chinese:30 english:100

看起来一切完美,没有什么问题,但是程序不会人那样智能,会根据场景判断输入数据的合法性。如果输入人员一不小心手一抖把年龄、身高、体重输入成负数或者巨大无比的数程序会无法感知的。

聪明的你已经想到在程序中加入逻辑判断了

class Student:
     def __init__(self, name, math, chinese, english):
        self.name = name

        if 0 <= math <= 100:
            self.math = math
        else:
            raise ValueError('valid value must be in [0,100]')

        if 0 <= chinese <= 100:
            self.chinese = chinese
        else:
            raise ValueError('valid value must be in [0,100]')

        if 0 <= english <=100:
            self.english = english
        else:
            raise ValueError('valid value must be in [0,100]')

     def __str__(self):
        return 'name:{} math:{} chinese:{} english:{}'.format(self.name, self.math, self.chinese, self.english)


s = Student2('zhangsan', 70, 30, 100)
print(s)
s.math = 170 # 此时无法执行进行条件判断
print(s.math)

s = Student('zhangsan', 170, 30, 100) # 此时会在init里判断
print(s)

########################################
name:zhangsan math:70 chinese:30 english:100
170

Traceback (most recent call last):
  File "G:/Git/python_study/魔术方法/test.py", line 41, in <module>
    s = Student2('zhangsan', 170, 30, 100)
  File "G:/Git/python_study/魔术方法/test.py", line 26, in __init__
    raise ValueError('valid value must be in [0,100]')
ValueError: valid value must be in [0,100]

这下程序有点智能,能够明辨是非了,但是在类实例化对象后如果有人试着运行s.math=170那么谁也无法阻止,条件限制就失去了作用,而且__init__里有太多逻辑判断影响代码可读性

幸运的是,Python的property解决了这个问题

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

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

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError('valid value must be in [0,100]')

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

    @chinese.setter
    def chinese(self, value):
        if 0 <= value <= 100:
            self._chinese = value
        else:
            raise ValueError('valid value must be in [0,100]')

    @property
    def english(self):
        return self._english

    @english.setter
    def english(self, value):
        if 0 <= value <= 100:
            self._english = value
        else:
            raise ValueError('valid value must be in [0,100]')


    def __str__(self):
        return 'student:{} ,math:{}, chinese:{}, english:{}'.format(self.name, self.math, self.chinese, self.english)

s = Student('zhangsan', 170, 30, 100)
print(s)

################################################################
Traceback (most recent call last):
  File "G:/Git/python_study/魔术方法/test.py", line 65, in <module>
    s = Student1('zhangsan', 170, 30, 100)
  File "G:/Git/python_study/魔术方法/test.py", line 24, in __init__
    self.math = math
  File "G:/Git/python_study/魔术方法/test.py", line 37, in math
    raise ValueError('valid value must be in [0,100]')
ValueError: valid value must be in [0,100]

###############################################################
s = Student1('zhangsan', 70, 30, 100)
print(s)
s.math = 170  # 通过类实例赋值
print(s.math)

Traceback (most recent call last):
  File "G:/Git/python_study/魔术方法/test.py", line 123, in <module>
    s.math = 170
  File "G:/Git/python_study/魔术方法/test.py", line 93, in math
    raise ValueError('valid value must be in [0,100]')
ValueError: valid value must be in [0,100]

我们用@property装饰器指定了一个getter方法,用@math.setter(其他两个参数类似)装饰器指定了一个setter方法。当我们这么做时,每当有人试着访问math属性,Python就会自动调用相应的getter/setter方法。比方说,当遇到s.math= value这样的代码时就会自动调用math.setter

如果没有property,我们不得不把所有的实例属性都隐藏起来,提供大量显式的类似get_math、set_math方法,代码将会java一样臃肿。

类里的三个属性,math、chinese、english,都使用了 Property 对属性的合法性进行了有效控制。功能上,没有问题,但就是太啰嗦了(property最大的缺点不能重复使用),三个变量的合法性逻辑都是一样的,只要大于0,小于100 就可以,代码重复率太高了,这里三个成绩还好,但假设还有地理、生物、历史、化学等十几门的成绩呢,使用描述符可以简化代码。

class Score:
    def __init__(self, default=0):
        self.__score = default

    def __get__(self, instance, owner):
        return self.__score

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('value must be integer')

        if value <= 0 or value >= 100:
            raise ValueError('value must be in [0,100]')

        self.__score = value

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

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

    def __str__(self):
        return 'name:{} math:{} chinese:{} english:{}'.format(self.name, self.math, self.chinese, self.english)

s = Student('zhangsan', 170, 30, 100)
print(s)

##################################################
Traceback (most recent call last):
  File "G:/Git/python_study/魔术方法/test.py", line 73, in <module>
    s = Student3('zhangsan', 170, 30, 100)
  File "G:/Git/python_study/魔术方法/test.py", line 66, in __init__
    self.math = math
  File "G:/Git/python_study/魔术方法/test.py", line 55, in __set__
    raise ValueError('value must be in [0,100]')
ValueError: value must be in [0,100]

描述符给我们带来的编码上的便利,它在实现 保护属性不受修改、属性类型检查 的基本功能,同时有大大提高代码的复用率。


一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。