over 3 years ago

Use Descriptors for Reusable @property Methods

property 最大的問題是可能造成 duplicated code 這種 code smell。

下面的程式 math_grade 以及 math_grade 就有這樣的問題。

class Exam(object):
    def __init__(self):
        self._writing_grade = 0
        self._math_grade = 0

    @staticmethod
    def _check_grade(value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')

    @property
    def writing_grade(self):
        return self._writing_grade

    @writing_grade.setter
    def writing_grade(self, value):
        self._check_grade(value)
        self._writing_grade = value

    @property
    def math_grade(self):
        return self._math_grade

    @math_grade.setter
    def math_grade(self, value):
        self._check_grade(value)
        self._math_grade = value

可以使用 descriptor 解決,下面的程式將重複的邏輯封裝在 Grade 裡頭。但是這個程式根本不能用,因為存取到的是 class attributes,例如 exam.writing_grade = 40 其實是 Exam.__dict__['writing_grade'].__set__(exam, 40),這樣所有 Exam 的 instances 都是存取到一樣的東西 (Grade())。

下面的程式中, second_exam 覆寫了 exam.writing_grade 。

class Grade(object):
    def __init__(self):
        self._value = 0

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

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

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

exam = Exam()
second_exam = Exam()
exam.writing_grade = 40
second_exam.writing_grade = 52

解決方式是用個 dictionary 存起來,這裡使用 WeakKeyDictionary 避免 memory leak。

from weakref import WeakKeyDictionary

class Grade(object):
    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
← Effective Python 心得筆記: Item 29 and Item 30 Effective Python 心得筆記: Item 32 →
 
comments powered by Disqus