1. 程式人生 > >Python學習:描述符

Python學習:描述符

一、描述符是什麼

  描述符:是一個類,只要內部定義了方法__get__, __set__, __delete__中的一個或者多個。描述符,屬性,方法繫結等內部機制都是描述符在起作用。描述符以單個屬性出現,並針對該屬性的不同訪問行為做出響應。最重要的是,描述符能“感知”通過什麼引用該屬性,從而和目標建立繫結關聯。

二、描述符的實現

  

class Descriptor:
    """
    描述符
    """

    def __set_name__(self, owner, name):
        """
        描述符屬性必須定義為型別成員,所以其自身不適合儲存例項相關的狀態,在建立屬性時,__set_name__方法被呼叫,並可以通過引數獲知目標型別(owner),以及屬性名稱
        :param owner:
        :param name:
        :return:
        """
        print(self, owner, name)
        self.name = f"__{name}__"

    def __get__(self, instance, owner):
        """
        以型別或例項訪問描述符屬性時,__get__被自動呼叫,且會接收到型別和例項引用
        :param instance:
        :param owner:
        :return:
        """
        print(self,instance,owner)
        return getattr(instance,self.name, None)

    def __set__(self, instance, value):
        """
        僅在例項引用時被呼叫。以型別引用進行賦值,會導致描述符屬性被替換
        :param instance:
        :param value:
        :return:
        """
        print(self, instance, value)
        setattr(instance, self.name, value)

    def __delete__(self, instance):
        """
        僅在例項被引用時呼叫。以型別引用進行刪除操作,會導致描述符屬性被刪除
        :param instance:
        :return:
        """
        print(self, instance)
        raise AttributeError("delete is disabled")


class X:
    data = Descriptor()


x = X()    # 執行__set_name__  <__main__.Descriptor object at 0x0000026DEB4E8438> <class '__main__.X'> data
x.data = 100    # 執行__set__  <__main__.Descriptor object at 0x0000018A54408470> <__main__.X object at 0x0000018A54408908> 100
print(x.data)    # 執行__get__ <__main__.Descriptor object at 0x0000020685688438> <__main__.X object at 0x00000206856888D0> <class '__main__.X'>
print(x.__dict__)    # {'__data__': 100}
print(X.__dict__)    #  {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x000001E841598438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}
X.data = 2    # 以型別引用進行賦值,會導致描述符屬性被替換
print(x.data)    # 2
print(X.data)    # 2
print(x.__dict__)    # {'__data__': 100}
print(X.__dict__)    # {'__module__': '__main__', 'data': 2, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

三、資料描述符

  如果定義了__set__或__delete__方法,那麼我們便稱其為資料描述符(data descriptor),而僅有__get__的則是非資料描述符(non-data descriptor)。這兩者的區別在於,資料描述符屬性的優先順序高於例項名字空間中的同名成員。

class Descriptors:
    """
    資料描述符
    """
    def __set_name__(self, owner, name):
        self.name = name     # 獲取Descriptors 例項物件名字

    def __get__(self, instance, owner):
        print("執行Descriptors的get")
        return self.name

    def __set__(self, instance, value):

        self.name = value
        print("執行Descriptors的set")

    def __delete__(self, instance):
        print("執行Descriptors的delete")


class Light:
    # 使用描述符
    name = Descriptors()

    def __init__(self, name, price):
        self.name = name
        self.price = price


# 使用類的例項物件來測試
light = Light("電燈泡", 60)  # 執行描述符的set內建屬性
light.name  # 執行描述符的get內建屬性
print(light.__dict__)  # 檢視例項的字典,不存在name  {'price': 60}
print(Light.__dict__)  # 檢視類的字典,存在name(為描述符的物件)
# {'__module__': '__main__', 'name': <__main__.Descriptors object at 0x000002261E7D8438>,
# '__init__': <function Light.__init__ at 0x00000226257FED90>, '__dict__': <attribute '__dict__' of 'Light' objects>,
# '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None}
del light.name  # 執行描述符的delete內建屬性
del Light.name    # 以型別引用進行刪除操作,會導致描述符屬性被刪除
print(Light.__dict__)    # {'__module__': '__main__', '__init__': <function Light.__init__ at 0x000001CBC197EEA0>, '__dict__': <attribute '__dict__' of 'Light' objects>, '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None}
print(light.name)    # 報錯,描述符屬性被刪除

  如果註釋掉__set__,就成為了非資料描述符。

  描述符的優先順序問題:類屬性>資料描述符>例項屬性>非資料描述符>找不到屬性觸發__getattr__()

    說明問題一:類屬性>資料描述符

class  Descriptor:

    def __get__(self, instance, owner):
        print("__get__")
        return self.name

    def __set__(self, instance, value):
        print("開始賦值:", value)
        self.name = value
        print("__set__")


class X:
    data = Descriptor()


x = X()
x.data = 100     # 呼叫__set__賦值
print(x.__dict__)    # {}
x.data = 3
print(x.data)    # 3
print(x.__dict__)    # {}
print(X.data)    # 呼叫__get__
print(X.__dict__)    #  {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x0000015EE4A38438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

X.data = 44444    # 語句沒有觸發set的執行,說明類屬性的優先順序大於資料描述符的優先,此時相當於類屬性覆蓋了資料描述符,從而說明對類屬性的一切操作都與描述符無關
print(x.data)    # 4
print(x.__dict__)    # {}
print(X.__dict__)    # {'__module__': '__main__', 'data': 44444, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

  說明問題二:資料描述符>例項屬性    參考“描述符程式碼” ,資料描述符的優先順序大於例項屬性的優先順序,此時例項屬性name被資料描述符所覆蓋,而price沒有描述符代理,所以它任然是例項屬性。

  說明問題三:例項屬性>非資料描述符

class Descriptors:
    """
    非資料描述符
    """

    def __get__(self, instance, owner):
        print("執行Descriptors的get")

    def __delete__(self, instance):
        print("執行Descriptors的delete")


class X:
    data = Descriptors()


x = X()
x.data = 3    # 報錯,AttributeError: __set__

四、方法繫結

  因為函式預設實現了描述符協議,所以當以例項或型別訪問方法時,__get__首先被呼叫。型別和例項作為引數被傳入__get__,從而截獲繫結目標(self),如此就將函式包裝稱繫結方法物件返回。實際被執行的,就是這個會隱式傳入第一個引數的包裝品。

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_info(self):
        print("my name is %s ,my age is %s" % (self.name, self.age))

p = Person("ways", 13)
print(p.print_info)    # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>>
print(p.print_info.__get__(p,Person))    # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>>
m = p.print_info.__get__(p,Person)
Person.print_info(m.__self__,)    # my name is ways ,my age is 13
print(m.__self__, m.__func__)    # <__main__.Person object at 0x000002092DBE8438> <function Person.print_info at 0x0000020934BEED08>
"""
方法執行分成了兩個步驟:
    p.print_info():
        #1. m = p.print_info.__get__(p,Person)      將函式包裝成繫結方法
        #2. m()等價Person.print_info(m.__self__,)   執行時,隱式將self/cls引數傳遞給目標函式
"""

五、描述符的使用例子

  1、模擬property

class My_Property:
    """
    使用描述符模擬property
    """

    def __init__(self, func):
        self.func = func
        print(self.func)    # <function Test.my_area at 0x000001671607ED90>

    def __get__(self, instance, owner):
        res = self.func(instance)    # 回撥傳入的函式,將執行結果儲存在res中
        setattr(instance,self.func.__name__,res)    # 為函式名func.__name__ 設定值為res,存入物件的字典
        return res


class Test:
    def __init__(self,weight,height):
        self.weight = weight
        self.height = height
    @My_Property
    def my_area(self):
        return self.weight*self.height


test = Test(3, 4)
print(test.my_area)    # 12
print(test.__dict__)    # {'weight': 3, 'height': 4, 'my_area': 12}

六、描述符的使用總結

  1、描述符是可以實現大部分python類特性中的底層魔法,包括@classmethod,@staticmethd,@property甚至是__slots__屬性

  2、描述父是很多高階庫和框架的重要工具之一,描述符通常是使用到裝飾器或者元類的大型框架中的一個元件.

 

 

參考:https://www.cnblogs.com/Lynnblog/p/9033455.html 和《python3學習筆記上》