Python 的 @property 裝飾器提供簡潔的屬性管理方式,但面對複雜需求,例如多個屬性需要相同驗證邏輯時,容易造成程式碼重複。描述器則能有效解決這個問題,它允許自訂屬性存取行為,讓程式碼更簡潔、易維護。想像一下,在一個線上教育平台中,需要驗證學生的各科成績,如果使用 @property,每個科目的成績都需要各自定義 getter 和 setter 方法,並重複撰寫驗證邏輯。但如果使用描述器,只需定義一個描述器類別,就能夠重複使用驗證邏輯,大幅減少程式碼量。描述器本質上是一個具有 __get__、__set__ 和 __set_name__ 方法的類別。當在其他類別中使用描述器例項作為屬性時,Python 會自動呼叫這些方法,進而控制屬性存取。當我們嘗試取得或設定屬性值時,Python 會先檢查該屬性是否為描述器例項。如果是,則會分別呼叫描述器的 __get__ 和 __set__ 方法。__set_name__ 方法則在描述器被指定給類別屬性時自動呼叫,用於記錄屬性名稱。
為何我放棄傳統 @property:描述器(Descriptors)的妙用
在軟體開發的道路上,我們經常會遇到需要對屬性進行額外控制的情況。Python 的 @property 裝飾器,提供了一種優雅的方式來定義 getter 和 setter 方法,從而允許我們在屬性存取時執行額外的邏輯。然而,當需要在多個屬性或類別中重複使用相同的邏輯時,@property 就顯得有些力不從心。這時候,描述器(Descriptors)就派上用場了。
@property 的侷限性:重複與維護的噩夢
讓我們先看看 @property 在處理驗證邏輯時的侷限性。假設我們需要建立一個 Homework 類別,其中 grade 屬性必須在 0 到 100 之間:
class Homework:
    def __init__(self):
        self._grade = 0
    @property
    def grade(self):
        return self._grade
    @grade.setter
    def grade(self, value):
        if not (0 <= value <= 100):
            raise ValueError("成績必須介於 0 到 100 之間")
        self._grade = value
這段程式碼簡潔易懂,但如果我們需要為 Exam 類別中的多個科目(例如 writing_grade、math_grade)重複相同的驗證邏輯,就會發現程式碼變得冗長與難以維護。
class Exam:
    def __init__(self):
        self._writing_grade = 0
        self._math_grade = 0
    @staticmethod
    def _check_grade(value):
        if not (0 <= value <= 100):
            raise ValueError("成績必須介於 0 到 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
身為玄貓,我深知程式碼重複是萬惡之源。當我們需要修改驗證邏輯時,必須修改多個地方,這不僅容易出錯,也違反了 DRY(Don’t Repeat Yourself)原則。
描述器(Descriptors):屬性存取的幕後推手
描述器是一種更強大的工具,它可以讓我們自訂屬性存取的行為。簡單來說,描述器是一個具有 __get__、__set__ 或 __delete__ 方法的類別。當我們在另一個類別中使用描述器時,Python 會自動呼叫這些方法來處理屬性存取。
以下是一個 Grade 描述器的範例:
class Grade:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("成績必須介於 0 到 100 之間")
        instance.__dict__[self.name] = value
    def __set_name__(self, owner, name):
        self.name = name
在這個範例中,Grade 類別定義了 __get__、__set__ 和 __set_name__ 方法。__get__ 方法用於取得屬性值,__set__ 方法用於設定屬性值,而 __set_name__ 方法則用於在類別建立時自動設定屬性的名稱。
現在,我們可以在 Exam 類別中使用 Grade 描述器來驗證成績:
class Exam:
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()
這樣,我們就避免了程式碼重複,並將驗證邏輯集中在 Grade 描述器中。
描述器如何運作:__getattribute__ 的魔法
要理解描述器的工作原理,我們需要了解 Python 的 __getattribute__ 方法。當我們存取一個物件的屬性時,Python 會先呼叫 __getattribute__ 方法。如果該屬性是一個描述器,Python 就會呼叫描述器的 __get__、__set__ 或 __delete__ 方法。
這種機制賦予了描述器強大的能力,讓我們可以自訂屬性存取的行為,實作各種有趣的功能。
描述器的陷阱:分享狀態的風險
雖然描述器很強大,但也存在一些陷阱。例如,如果我們在描述器中直接使用案例項屬性來儲存值,可能會導致多個例項分享相同的狀態。
以下是一個錯誤的範例:
class Grade:
    def __init__(self):
        self._value = 0  # 錯誤:分享狀態
    def __get__(self, instance, owner):
        return self._value
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("成績必須介於 0 到 100 之間")
        self._value = value
在這個範例中,Grade 描述器的 _value 屬性是所有例項分享的。這意味著當我們修改一個例項的 grade 屬性時,所有其他例項的 grade 屬性也會被修改。
為了避免這種情況,我們應該使用案例項的 __dict__ 屬性來儲存值,如上面的正確範例所示。
玄貓:擺脫 Property 的束縛,用 Descriptor 釋放 Python 屬性管理的無限可能
在 Python 的世界裡,屬性管理是物件導向設計中不可或缺的一環。你可能已經習慣使用 @property 裝飾器來控制屬性的讀取、設定和刪除行為。但當面對更複雜的需求時,@property 可能會顯得力不從心。這時候,Descriptor (描述器) 就如同救星般降臨,為我們提供了更強大、更靈活的屬性管理工具。
為什麼我放棄傳統 ORM:Descriptor 的優雅與力量
在過去的專案中,我曾深度使用過 SQLAlchemy 這類別的 ORM (物件關聯對映) 框架。它們確實簡化了資料函式庫操作,但當資料表結構變得複雜,或是需要更細緻的屬性控制時,ORM 往往會成為效能瓶頸。
例如,在一個金融科技專案中,我們需要處理大量的交易資料。每個交易物件都有數十個屬性,其中一些屬性需要經過複雜的驗證和轉換才能儲存到資料函式庫。如果使用 @property,我們需要在每個屬性的 getter 和 setter 中重複編寫驗證邏輯,這不僅冗長乏味,而與難以維護。
這時候,我就開始思考是否有更優雅的解決方案。Descriptor 的出現,讓我眼睛一亮。
Descriptor 是什麼?一窺 Python 屬性管理的幕後英雄
簡單來說,Descriptor 是一個實作了 __get__、__set__ 或 __delete__ 方法的類別。當我們將 Descriptor 例項指定給一個類別屬性時,Python 就會自動接管該屬性的存取行為,並將其導向 Descriptor 類別中定義的方法。
這種機制賦予了我們極大的彈性,可以精準控制屬性的讀取、設定和刪除過程,並在其中加入自定義的邏輯。
Grade 類別:Descriptor 的實戰演練
讓我們來看一個簡單的例子。假設我們需要設計一個 Grade 類別,用於表示學生的成績。成績必須介於 0 到 100 之間,並且我們希望能夠追蹤每個學生的成績。
class Grade(object):
    def __init__(self):
        self._values = {}
    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
內容解密:
- __init__: 初始化方法,建立一個字典- _values來儲存每個- Exam例項的成績。
- __get__: 當我們嘗試讀取- Exam例項的- writing_grade屬性時,這個方法會被呼叫。它會從- _values字典中取出對應的成績,如果沒有找到,則傳回預設值 0。
- __set__: 當我們嘗試設定- Exam例項的- writing_grade屬性時,這個方法會被呼叫。它會先驗證成績是否介於 0 到 100 之間,然後將其儲存到- _values字典中。
在這個例子中,__get__ 方法負責讀取成績,__set__ 方法負責設定成績。透過這兩個方法,我們就能夠精準控制成績的存取行為,並在其中加入驗證邏輯。
記憶體洩漏的陷阱:WeakKeyDictionary 的妙用
上面的實作看似完美,但其實隱藏著一個潛在的風險:記憶體洩漏。由於 _values 字典會持有所有 Exam 例項的參照,導致這些例項無法被垃圾回收機制清理,最終造成記憶體洩漏。
為瞭解決這個問題,我們可以使用 Python 內建的 weakref 模組。這個模組提供了一個特殊的類別 WeakKeyDictionary,它可以自動移除不再使用的鍵 (在這個例子中,就是 Exam 例項)。
import weakref
class Grade(object):
    def __init__(self):
        self._values = weakref.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
內容解密:
- import weakref: 匯入- weakref模組。
- self._values = weakref.WeakKeyDictionary(): 使用- WeakKeyDictionary來儲存成績,取代原本的字典。
透過使用 WeakKeyDictionary,我們就能夠確保 Descriptor 類別不會造成記憶體洩漏,讓我們的程式碼更加健壯。
Exam 類別:Descriptor 的終極應用
有了 Grade 類別,我們就可以將其應用到 Exam 類別中,實作更精確的屬性管理。
class Exam(object):
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print('First ', first_exam.writing_grade, 'is right')
print('Second', second_exam.writing_grade, 'is right')
內容解密:
- math_grade = Grade(): 將- Grade類別的例項指定給- math_grade屬性。
- writing_grade = Grade(): 將- Grade類別的例項指定給- writing_grade屬性。
- science_grade = Grade(): 將- Grade類別的例項指定給- science_grade屬性。
現在,當我們嘗試讀取或設定 Exam 例項的 math_grade、writing_grade 或 science_grade 屬性時,Python 就會自動呼叫 Grade 類別中定義的方法,實作我們想要的屬性管理行為。
玄貓的經驗分享:Descriptor 的最佳實踐
在實際專案中,玄貓發現 Descriptor 在以下場景特別有用:
- 資料驗證: 使用 Descriptor 來驗證屬性的值,確保資料的正確性。
- 延遲載入: 使用 Descriptor 來延遲載入屬性的值,提高程式的效能。
- 屬性轉換: 使用 Descriptor 來轉換屬性的值,使其符合特定的格式。
- 唯讀屬性: 使用 Descriptor 來建立唯讀屬性,防止屬性被意外修改。
Python 屬性管理:動態屬性與元程式設計的力量
在 Python 中,屬性管理是物件導向設計中一個核心概念。Python 提供了多種方式來控制和自訂物件屬性的存取行為,這使得開發者能夠實作高度靈活和可擴充套件的程式碼。本文將探討 Python 中用於屬性管理的 __getattr__、__getattribute__ 和 __setattr__ 方法,以及如何使用元類別來驗證子類別的正確性。
攔截屬性存取:__getattr__ 的妙用
當我們嘗試存取一個物件不存在的屬性時,Python 會自動呼叫 __getattr__ 方法。這個方法允許我們動態地產生屬性,或者執行其他自訂邏輯。
class LoggingLazyDB(object):
    def __init__(self):
        self.exists = 5
    def __getattr__(self, name):
        print('玄貓呼叫了 __getattr__(%s)' % name)
        value = '屬性 %s 的值' % name
        setattr(self, name, value)
        return value
data = LoggingLazyDB()
print('初始狀態:', data.__dict__)
print('foo 屬性:', data.foo)
print('修改後狀態:', data.__dict__)
內容解密:
- LoggingLazyDB類別: 定義了一個具有- exists屬性的類別。
- __getattr__(self, name)方法: 當存取不存在的屬性時,這個方法會被呼叫。它會列印一條訊息,然後動態地建立一個新的屬性,並將其設定為一個預設值。
- setattr(self, name, value)函式: 用於設定物件的屬性值。
- 執行結果: 當我們第一次存取 data.foo時,__getattr__會被呼叫,foo屬性會被建立,並賦予一個預設值。後續存取data.foo時,由於foo已經存在,__getattr__不會再被呼叫。
全域性屬性攔截:__getattribute__ 的威力
與 __getattr__ 不同,__getattribute__ 方法會在每次屬性存取時都被呼叫,無論屬性是否存在。這使得我們能夠在屬性存取前後執行一些通用的邏輯,例如驗證或日誌記錄。
class ValidatingDB(object):
    def __init__(self):
        self.exists = 5
    def __getattribute__(self, name):
        print('玄貓呼叫了 __getattribute__(%s)' % name)
        try:
            return super().__getattribute__(name)
        except AttributeError:
            value = '屬性 %s 的值' % name
            setattr(self, name, value)
            return value
data = ValidatingDB()
print('exists:', data.exists)
print('foo: ', data.foo)
print('foo: ', data.foo)
內容解密:
- ValidatingDB類別: 定義了一個具有- exists屬性的類別。
- __getattribute__(self, name)方法: 每次存取屬性時,這個方法都會被呼叫。它會先嘗試使用- super().__getattribute__(name)來取得屬性值。如果屬性不存在,則會捕捉- AttributeError異常,並動態地建立一個新的屬性。
- super().__getattribute__(name)方法: 用於呼叫父類別的- __getattribute__方法,以避免無限遞迴。
- 執行結果: 每次存取屬性時,__getattribute__都會被呼叫。如果屬性不存在,則會動態地建立一個新的屬性。
屬性指定攔截:__setattr__ 的應用
__setattr__ 方法允許我們攔截屬性指定的過程。每次我們嘗試設定一個物件的屬性時,__setattr__ 方法都會被呼叫。這使得我們能夠在屬性指定前後執行一些自訂邏輯,例如驗證或日誌記錄。
class SavingDB(object):
    def __setattr__(self, name, value):
        # Save some data to the DB log
        # ...
        super().__setattr__(name, value)
class LoggingSavingDB(SavingDB):
    def __setattr__(self, name, value):
        print('玄貓呼叫了 __setattr__(%s, %r)' % (name, value))
        super().__setattr__(name, value)
data = LoggingSavingDB()
print('之前:', data.__dict__)
data.foo = 5
print('之後:', data.__dict__)
data.foo = 7
print('最後:', data.__dict__)
內容解密:
- SavingDB類別: 定義了一個具有- __setattr__方法的類別。
- __setattr__(self, name, value)方法: 每次設定屬性時,這個方法都會被呼叫。它可以用於執行一些自訂邏輯,例如將資料儲存到資料函式庫日誌中。
- LoggingSavingDB類別: 繼承自- SavingDB,並覆寫了- __setattr__方法,以增加日誌記錄功能。
- 執行結果: 每次設定屬性時,__setattr__都會被呼叫,並列印一條訊息。
避免無限遞迴:super() 的重要性
在使用 __getattribute__ 和 __setattr__ 時,需要特別注意避免無限遞迴。如果我們在這些方法中直接存取或修改物件的屬性,可能會導致這些方法被重複呼叫,最終導致堆積疊溢位。
為了避免這種情況,我們應該使用 super() 來呼叫父類別的 __getattribute__ 和 __setattr__ 方法。這樣可以確保我們存取的是物件的底層屬性,而不是觸發遞迴呼叫。
class DictionaryDB(object):
    def __init__(self, data):
        self._data = data
    def __getattribute__(self, name):
        data_dict = super().__getattribute__('_data')
        return data_dict[name]
內容解密:
- DictionaryDB類別: 定義了一個使用字典來儲存資料的類別。
- __getattribute__(self, name)方法: 每次存取屬性時,這個方法都會被呼叫。它使用- super().__getattribute__('_data')來取得底層的- _data字典,然後從字典中取得屬性值。
- super().__getattribute__('_data')方法: 用於呼叫父類別的- __getattribute__方法,以避免無限遞迴。
使用元類別驗證子類別
元類別是 Python 中一個強大的特性,它允許我們在類別建立時自訂類別的行為。其中一個常見的應用是使用元類別來驗證子類別的正確性。
class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print((meta, name, bases, class_dict))
        return type.__new__(meta, name, bases, class_dict)
class MyClass(object, metaclass=Meta):
    stuff = 123
    def foo(self):
        pass
內容解密:
- Meta類別: 定義了一個元類別,它繼承自- type。
- __new__(meta, name, bases, class_dict)方法: 這個方法在類別建立時被呼叫。它接收類別的元資料,包括類別名稱、父類別和屬性字典。我們可以在這個方法中修改類別的元資料,或者執行一些驗證邏輯。
- MyClass類別: 使用- Meta作為元類別。當- MyClass被建立時,- Meta.__new__方法會被呼叫。
 
            