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_grademath_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

內容解密:

  1. __init__: 初始化方法,建立一個字典 _values 來儲存每個 Exam 例項的成績。
  2. __get__: 當我們嘗試讀取 Exam 例項的 writing_grade 屬性時,這個方法會被呼叫。它會從 _values 字典中取出對應的成績,如果沒有找到,則傳回預設值 0。
  3. __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

內容解密:

  1. import weakref: 匯入 weakref 模組。
  2. 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')

內容解密:

  1. math_grade = Grade(): 將 Grade 類別的例項指定給 math_grade 屬性。
  2. writing_grade = Grade(): 將 Grade 類別的例項指定給 writing_grade 屬性。
  3. science_grade = Grade(): 將 Grade 類別的例項指定給 science_grade 屬性。

現在,當我們嘗試讀取或設定 Exam 例項的 math_gradewriting_gradescience_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__)

內容解密:

  1. LoggingLazyDB 類別: 定義了一個具有 exists 屬性的類別。
  2. __getattr__(self, name) 方法: 當存取不存在的屬性時,這個方法會被呼叫。它會列印一條訊息,然後動態地建立一個新的屬性,並將其設定為一個預設值。
  3. setattr(self, name, value) 函式: 用於設定物件的屬性值。
  4. 執行結果: 當我們第一次存取 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)

內容解密:

  1. ValidatingDB 類別: 定義了一個具有 exists 屬性的類別。
  2. __getattribute__(self, name) 方法: 每次存取屬性時,這個方法都會被呼叫。它會先嘗試使用 super().__getattribute__(name) 來取得屬性值。如果屬性不存在,則會捕捉 AttributeError 異常,並動態地建立一個新的屬性。
  3. super().__getattribute__(name) 方法: 用於呼叫父類別的 __getattribute__ 方法,以避免無限遞迴。
  4. 執行結果: 每次存取屬性時,__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__)

內容解密:

  1. SavingDB 類別: 定義了一個具有 __setattr__ 方法的類別。
  2. __setattr__(self, name, value) 方法: 每次設定屬性時,這個方法都會被呼叫。它可以用於執行一些自訂邏輯,例如將資料儲存到資料函式庫日誌中。
  3. LoggingSavingDB 類別: 繼承自 SavingDB,並覆寫了 __setattr__ 方法,以增加日誌記錄功能。
  4. 執行結果: 每次設定屬性時,__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]

內容解密:

  1. DictionaryDB 類別: 定義了一個使用字典來儲存資料的類別。
  2. __getattribute__(self, name) 方法: 每次存取屬性時,這個方法都會被呼叫。它使用 super().__getattribute__('_data') 來取得底層的 _data 字典,然後從字典中取得屬性值。
  3. 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

內容解密:

  1. Meta 類別: 定義了一個元類別,它繼承自 type
  2. __new__(meta, name, bases, class_dict) 方法: 這個方法在類別建立時被呼叫。它接收類別的元資料,包括類別名稱、父類別和屬性字典。我們可以在這個方法中修改類別的元資料,或者執行一些驗證邏輯。
  3. MyClass 類別: 使用 Meta 作為元類別。當 MyClass 被建立時,Meta.__new__ 方法會被呼叫。