Loading

Effective Python Ver2.0_StudyNotes_用描述符来改写需要复用的@property方法

用描述符来改写需要复用的@property方法

@property机制的最大缺点是不方便复用,其不能把他修饰的方法所使用的逻辑,套用在同一个类的其他属性上面,也不能在无关的类里面复用。
要实现复用,可以通过描述符实现:
描述符协议规定了程序应该如何处理属性访问操作,充当描述符的那个类能够实现__get__与__set__方法,这样其他类就可以共用这个描述符而实现已有逻辑的复用

先简单说说描述符的内部运行机制:

示例代码:

class Grade:
    def __init__(self):
        self._values = 0

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

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("Value must between 0 and 100")
        self._values = value

class Exam:
    math_grade = Grade()
    math_science = Grade()

if __name__ == '__main__':
    exam = Exam()
    exam.math_grade = 40
    exam.math_grade

Exam实例进行赋值操作时候,Python会把这次赋值操作转译成:Exam.__dict__['math_grade'].__set__(exam, 40)
同样的,进行获取属性操作,Python会将此次获取操作转译成:Exam.__dict__['math_grade'].__get__(exam)
其实,这样的转译效果是由object的__getattribute__方法促成的,通俗的说就是当Exam实例里面没有math_grade的属性时候,Python会转而在类的层面查找,查询Exam类里面有没有这样一个属性,如果有,而且还是实现了__get__与__set__方法的对象,那么系统就认定你通过描述符协议定义了这个属性的访问行为。

进一步应用

以上简单的示例还不完善,稍加完善的代码可以解决一些常见的问题,比如:Exam每次实例化的对象访问的math_grade其实是同一个Grade实例,这里需要做区分操作,最直接的方法时self._values={},使用不同的Exam实例去获取对应的values值,当然随之出现的另一个问题就是--内存泄漏。

大致说下内存泄漏的原因:在程序运行时,传给__set__方法的那些Exam实例全部会被Grade之中的_values字典所引用(因为_values字典里面存储的就是以实例为key的键值对),于是,指向那些实例的引用数量永远都不会降为0,这就导致垃圾回收机制没办法将Exam的实例清除掉。
解决这个问题的一个方式是:可以使用Python内置weakref模块,该模块中有一种特殊的字典,名为WeakKeyDictionary,他可以取代_values的普通字典。这个字典的特殊之处在于:如果运行时系统发现,指向Exam实例的引用只剩下一个(也就是只有Grade里面的_values的字典在使用其作为key的引用,而其他地方并没有再使用这个实例了),同时这个引用又是由WeakKeyDictionary的key所发起的,那么系统会将该引用从这个特殊字典里面删除,于是指向那个Exam实例的引用降为0,垃圾回收机制可以被触发,以防止内存的泄漏。

最后的完善代码,以作参考:

from weakref import WeakKeyDictionary

class Grade:
    def __init__(self):
        self._values = WeakKeyDictionary()

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

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

class Exam:
    math_grade = Grade()
    science_grade = Grade()


if __name__ == '__main__':
    first_exam = Exam()
    first_exam.math_grade = 82
    first_exam.science_grade = 99
    second_exam = Exam()
    second_exam.math_grade = 75
    second_exam.science_grade = 100
    print(f"First math_grade {first_exam.math_grade} is right")
    print(f"Second math_grade {second_exam.math_grade} is right")
posted @ 2021-09-01 23:00  MrSu  阅读(39)  评论(0编辑  收藏  举报