1. 程式人生 > 實用技巧 >描述符詳解

描述符詳解

一,什麼是描述符?

  • 官方定義:python描述符是一個“繫結行為”的物件屬性,在描述符協議中,它可以通過方法重寫屬性的訪問。這些方法由__get__()__set__()__delete()。如果這些方法中的任何一個被定義在一個物件中,這個物件就是一個描述符。

二,__dict__屬性

  • 作用:字典型別,存放本物件的屬性,key(鍵)即為屬性名,value(值)即為屬性的值,形式為{attr_key : attr_value}

  • 物件屬性的訪問順序:

    • 例項屬性
    • 類屬性
    • 父類屬性
    • __getattr__()方法
class Test:
    cls_val = 1

    def __init__(self):
        self.ins_val = 10

t = Test()
print(Test.__dict__)
"""
{'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000001FA72913E18>,
 '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, 
 '__doc__': None}
"""
print(t.__dict__)
"""
{'ins_val': 10}
"""

# 更改例項t的屬性cls_val,只是新增了該屬性,並不影響類Test的屬性cls_val
t.cls_val = 20
print(t.__dict__)
"""
{'ins_val': 10, 'cls_val': 20}
"""
print(Test.__dict__)
"""
{'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000001FA72913E18>,
 '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, 
 '__doc__': None}
"""

# 更改了類Test的屬性cls_val的值,由於事先增加了例項t的cls_val屬性,因此不會改變例項的cls_val值(井水不犯河水)
Test.cls_val = 30
print(t.__dict__)
"""
{'ins_val': 10, 'cls_val': 20}
"""
print(Test.__dict__)
"""
{'__module__': '__main__', 'cls_val': 30, '__init__': <function Test.__init__ at 0x000001FA72913E18>,
 '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, 
 '__doc__': None}
"""

從以上程式碼可以看出,例項t的屬性並不包含cls_valcls_val是屬於類Test的。

三,魔法方法:__get__(), __set__(), __delete__()

  • 方法的原型為:
    • __get__(self, instance, owner)
    • __set__(self, instance, value)
    • __del__(self, instance)
class Desc:

    def __get__(self, instance, owner):
        print("__get__...")
        print("self : \t\t", self)
        print("instance : \t", instance)
        print("owner : \t", owner)
        print('=' * 40, "\n")

    def __set__(self, instance, value):
        print('__set__...')
        print("self : \t\t", self)
        print("instance : \t", instance)
        print("value : \t", value)
        print('=' * 40, "\n")


class TestDesc:
    x = Desc()


t = TestDesc()
print(t.x)
"""
__get__...
self : 		 <__main__.Desc object at 0x000001F28742C8D0>
instance : 	 <__main__.TestDesc object at 0x000001F287458E10>
owner : 	 <class '__main__.TestDesc'>
======================================== 
"""

可以看到,例項化類TestDesc後,呼叫物件t訪問其屬性x,會自動呼叫類Desc __get__方法,由輸出資訊可以看出:

  • self: Desc的例項物件,其實就是TestDesc的屬性x
  • instance: TestDesc的例項物件,其實就是t
  • owner: 即誰擁有這些東西,當然是TestDesc這個類,它是最高統治者,其他的一些都是包含在它的內部或者由它生出來的,是instance所屬的類

到此,我可以揭開小小的謎底了,其實,Desc類就是是一個描述符(描述符是一個類哦),為啥呢?因為類Desc定義了方法 __get__, __set__.

所以,某個類,只要是內部定義了方法 __get__

, __set__, __delete__ 中的一個或多個,就可以稱為描述符

問題1. 為什麼訪問 t.x的時候,會直接去呼叫描述符的 __get__() 方法呢?

  • 訪問Owner__getattribute__()方法(其實就是 TestDesc.__getattribute__()),首先訪問例項屬性,發現沒有,然後去訪問類TestDesc的屬性,找到了!

  • 判斷屬性 x 為一個描述符時,它就會做一些變動了,將 TestDesc.x 轉化為 TestDesc.__dict__['x'].__get__(t, TestDesc) 來訪問

  • 進入類Desc__get__()方法,進行相應的操作

問題2. 描述符的物件 x 其實是類 TestDesc 的類屬性,那麼可不可以把它變成例項屬性呢?

class Desc(object):
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        print("__get__...")
        print('name = ', self.name)
        print('=' * 40, "\n")

    def __set__(self, instance, value):
        print("__set__...")


class TestDesc(object):
    x = Desc('x')

    def __init__(self):
        self.x = Desc('x')
        self.y = Desc('y')

    def __getattribute__(self, item):
        print(item,'1111')
        return super(TestDesc, self).__getattribute__(item)

t = TestDesc()
print(t.x)
print(t.y)
print(t.__dict__)
"""
__set__...
__get__...
name =  x
======================================== 

<__main__.Desc object at 0x000001A44CA58E10>
{'y': <__main__.Desc object at 0x000001A44CA58E10>}
"""
"""
為什麼沒有列印t.y的資訊呢?
因為沒有訪問__get__() 方法啊。
因為例項化t的時候,並沒有y屬性。而是在__init__方法中建立的例項屬性。
所以在第一步查詢例項屬性的時候就找到了y屬性。

而x屬性是TestDesc的類屬性,在t例項化之前就儲存在TestDesc的類屬性中。
在__init__方法中給例項x屬性賦值時,發現TestDesc的類屬性中有x,而且x是一個描述符。
那麼會觸發x的__set__方法。而不會建立例項屬性。(如果沒有__set__方法,則會成功建立例項屬性。)
"""

問題3. 如果 類屬性的描述符物件 和 例項屬性描述符的物件 同名時,咋整?

class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ", self.name)

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

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


class TestDesc(object):
    _x = Desc('x')

    def __init__(self, x):
        self._x = x


# 以下為測試程式碼
t = TestDesc(10)
print(t._x)
"""
__init__(): name =  x
__set__() ...
__get__() ...
x
"""


print(t.__dict__) # {}
print(TestDesc.__dict__)
"""
{'__module__': '__main__', 
'_x': <__main__.Desc object at 0x000001D3BD1DC898>,
 '__init__': <function TestDesc.__init__ at 0x000001D3BD26A598>, 
 '__dict__': <attribute '__dict__' of 'TestDesc' objects>,
  '__weakref__': <attribute '__weakref__' of 'TestDesc' objects>, 
  '__doc__': None}
"""

"""
不對啊,按照慣例,t._x 會去呼叫 __getattribute__() 方法,然後找到了 例項t 的 _x 屬性就結束了,
為啥還去呼叫了描述符的 __get__() 方法呢?

這是因為,_x屬性是TestDesc的類屬性,在t例項化之前就儲存在TestDesc的類屬性中。
在__init__方法中給例項_x屬性賦值時,發現TestDesc的類屬性中有_x,而且_x是一個描述符。
那麼會觸發x的__set__方法。而不會建立例項屬性。(如果沒有__set__方法,則會成功建立例項屬性。)
"""

如果刪除__set__()方法會發生什麼?

class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ", self.name)

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


class TestDesc(object):
    _x = Desc('x')

    def __init__(self, x):
        self._x = x


t = TestDesc(10)
print(t._x)
"""
__init__(): name =  x
10
"""


print(t.__dict__) # {'_x': 10}
print(TestDesc.__dict__)
"""
{'__module__': '__main__', '_x': <__main__.Desc object at 0x000001E4EA63C8D0>,
 '__init__': <function TestDesc.__init__ at 0x000001E4EA6396A8>,
  '__dict__': <attribute '__dict__' of 'TestDesc' objects>,
 '__weakref__': <attribute '__weakref__' of 'TestDesc' objects>,
  '__doc__': None}
"""

"""
這次沒有呼叫__get__()方法。這是因為在TestDesc的__init__中建立例項屬性時,發現_x是描述符。
但是_x是非資料描述符(只有__get__),依然建立了例項屬性。
"""

問題4. 什麼是資料描述符,什麼是非資料描述符?

  • 一個類,如果只定義了 __get__() 方法,而沒有定義 __set__(), __delete__() 方法,則認為是非資料描述符; 反之,則成為資料描述符

四,總結

  • 例項化t時,如果在__init__中嘗試建立和類屬性中的描述符同名的屬性;如果是資料描述符,則觸發描述符的__set__方法,不建立例項屬性;如果是非資料描述符,則正常建立例項屬性。
  • 查詢屬性的順序:
    • __getattribute__(), 無條件呼叫
    • 例項屬性(例項的__dict__
    • 類屬性(類的__dict__
    • 如果在類屬性中發現要查詢的屬性,嘗試呼叫這個屬性的__get__方法。沒有就返回這個屬性。
    • 父類的屬性
    • __getattr__() 方法