[Python] 用描述符实现复用@property方法

1 Python内置的@property机制弊端在于不方便复用

不能把它所修饰方法中的逻辑,套用在同一个类中的其他属性上。

  • 例如,编写一个类记录分数
class Grade:
    def __init__(self):
        self._values = 0
    
    @property
    def grade(self):
        return self._values

    @grade.setter
    def grade(self, value):
        if isinstance(value, int):
            self._values = value
  • 假设,还有一个类,用于记录各个体育项目的分数
class Exam:
    def __init__(self):
        self._tennis = 0
        self._ping_pong = 0
    
    @property
    def tennis(self):
        return self._values

    @tennis.setter
    def tennis(self, value):
        if isinstance(value, int):
            self._values = value

    @property
    def ping_pong (self):
        return self._values

    @ping_pong .setter
    def ping_pong (self, value):
        if isinstance(value, int):
            self._values = value

显然,在这个类中,针对每一个体育项目,都要一套@property方法,但其实每个该方法的逻辑都是一模一样的。针对这个问题,Python提供了描述符实现。

用描述符方法实现复用

Python中,描述符协议规定了程序如何处理属性访问操作。
充当描述符的类要实现__get____set__方法,他们分别对应获取属性、赋值属性操作。

  • 以描述符的方式,重写Grade
class Grade:
    def __init__(self):
        self._values = 0

    def __get__(self, instance, instance_type):
        return self._values

    def __set__(self, instance, value):
        if isinstance(value, int):
            self._values = value
  • 重新定义Exam类,采用类级别的属性来实现分数的访问
class Exam:
    tennis= Grade()
    ping_pong  = Grade()
  • 使用这两个类完成各个项目的分数记录
exam = Exam()
exam.tennis = 99
exam.ping_pong = 100

实际上,Grade的写法不对,会造成一些奇怪的现象,如下:

first_exam = Exam()
sec_exam = Exam()
first_exam.tennis = 90
sec_exam.tennis = 100
print(first_exam.tennis)
print(sec_exam.tennis)

>>>
100
100

出现这种现象的原因在于:Grade只在定义Exam类时构造一次,而创建的Exam的每个实例中的tennis属性作为指针都指向了那个Grade。这导致改变不同Exam实例中的相同属性,最终都修改了该属性指向的Grade实例

2.1 解决方法

把每个Exam实例在这个属性上的值都记录下来,为防止内存泄漏,可使用weakref模块提供的WeakKeyDictionary来实现每个实例的状态保存。

from weakref import WeakKeyDictionary

class Grade1:
    def __init__(self):
        self._values = WeakKeyDictionary()  # 一种特殊字典

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0<=value<=100):
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value

在使用普通字典的情况下,每个Exam实例都会被Grade中的_values字典引用,导致垃圾回收器不会自动释放这些实例,在一直使用该类的情况下,导致内存泄漏,占满整个堆区。
在使用这种特殊字典的情况下,可防止内存泄漏,具体原理咱就不清楚了。

posted on 2023-01-29 00:25  _gua  阅读(54)  评论(0)    收藏  举报