跳至內容

Python/描述符

維基教科書,自由的教學讀本

描述符(descriptor)是指實現了以下描述符協議中至少一個方法的類:

  • __get__(self, instance, owner):訪問屬性時觸發
  • __set__(self, instance, value):設置屬性時觸發
  • __delete__(self, instance):刪除屬性時觸發

在 Python 中,描述符(Descriptor) 是一種底層機制,用於控制屬性的訪問方式。它是通過實現特定的魔法方法(__get__、__set__ 和 __delete__)來實現的。例如:

class MyDescriptor:
    def __get__(self, instance, owner):
        print("调用 __get__")
        return instance._value

    def __set__(self, instance, value):
        print("调用 __set__")
        instance._value = value

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
obj.attr = 42      # 调用 __set__
print(obj.attr)    # 调用 __get__输出 42

在通過類對象或者實例對象讀取屬性attr時,python調用了這個MyDescriptor()實例的__get__方法,給它3個實參值,其中第一個實參值是描述符對象本身,第二個實參值是MyClass實例對象(可以為None),第三個實參值是MyClass類對象。

描述符的應用場景:

  • 實現@staticmethod、@classmethod、@property等內置裝飾器語法糖。甚至是__slots__等的實現。
  • 屬性驗證(如類型檢查、範圍限制)
  • 惰性加載屬性
  • 緩存機制
  • ORM 框架中的字段定義(如 Django 的 models.Field)

使用描述符,可以讓程序員在引用一個對象屬性時自定義要完成的工作。本質上看,描述符就是一個類,只不過它定義了另一個類中屬性的訪問方式。換句話說,一個類可以將屬性管理全權委託給描述符類。

如下示例一個描述符及引用描述符類的代碼。Descriptors類就是一個描述符,Person是使用描述符的類:

class Descriptors:
 
    def __init__(self, key, value_type):
        self.key = key
        self.value_type = value_type
 
    def __get__(self, instance, owner):
        print("执行Descriptors的get")
        return instance.__dict__[self.key]
 
    def __set__(self, instance, value):
        print("执行Descriptors的set")
        if not isinstance(value, self.value_type):
            raise TypeError("参数%s必须为%s"%(self.key, self.value_type))
        instance.__dict__[self.key] = value 
 
    def __delete__(self, instance):
        print("执行Descriptors的delete")
        instance.__dict__.pop(self.key) 
 
class Person:
 
    name = Descriptors("name", str)
    age = Descriptors("age", int)
 
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
 
person = Person("xiaoming", 15)
print(person.__dict__)
person.name
person.name = "jone"
print(person.__dict__)
#输出:
#执行Descriptors的set
#执行Descriptors的set
#{'name': 'xiaoming', 'age': 15}
#执行Descriptors的get
#执行Descriptors的set
#{'name': 'jone', 'age': 15}
  • 至少實現了內置__set__()和__get__()方法的描述符稱為數據描述符
  • 實現了除__set__()以外的方法的描述符稱為非數據描述符

@property 修飾的屬性、數據描述符或非數據描述符定義的屬性,都是在類體的頂層定義,而不是在 __init__ 等實例方法中賦值,所以它們都被保存在類對象的 __dict__ 中。

通過類對象或者屬性對象使用一個屬性時,查找這個屬性的操作優先級從高到低順序:

  • 數據描述符
  • 實例屬性:顯然在實例對象的__dict__中
  • 非數據描述符
  • 類屬性
  • 找不到的屬性觸發__getattr__()

所以,用className.VarName=value,則為類屬性;用instanceName.VarName=value,則優先是數據描述符。

在每次查找obj.attr 屬性時,都用類對象的特殊方法 __getattribute__(),調用obj.__getattribute__('attr'):

  1. 先在 obj 的類及其父類的 __dict__ 中查找名為 attr 的屬性。如果找到,並且它是一個數據描述符(即定義了 __get__ 和 __set__),則調用該描述符的 __get__ 方法,直接返回。
  2. 如果沒有數據描述符,則查找實例對象自身的 __dict__(即 obj.__dict__)。如果找到了,就直接返回實例屬性的值。
  3. 如果實例屬性沒有找到,再查找類及其父類的 __dict__。如果找到,並且它是一個非數據描述符(只定義了 __get__),則調用其 __get__ 方法,返回其值。如果只是普通的類屬性(不實現描述符協議),直接返回屬性值。
  4. 如果以上都沒有找到,則查找類是否定義了 __getattr__ 方法。如果有,則調用 __getattr__(self, attr)。如果沒有定義 __getattr__,則拋出 AttributeError。

當執行 obj.attr = value,Python 實際調用 obj.__setattr__('attr', value)。在 object的__setattr__ 的默認實現:

  1. 先檢查 obj 的類(及其父類)中有沒有名為 attr 的描述符。如果有,並且它實現了 __set__(即是數據描述符),就調用 該描述符.__set__(obj, value)。
  2. 如果沒有數據描述符,就把值放進 obj.__dict__。

類對象賦值的默認行為就是直接把屬性寫入類對象自身的 __dict__中,不會觸發數據描述符的 __set__ 方法。描述符的 __set__ 只會在實例賦值(instance.attr = value)時被觸發。