python教程:屬性查詢順序,資料描述符

嗨学编程發表於2024-05-21

資料描述符,屬性查詢優先順序

如果在一個類中定義了 __get__() , __set__(), __delete__() 這三種方法之一,那麼這個類是一個描述符。

描述符分成兩種:

  • 如果這種類只定義了 __get__ 方法,那麼就是一個非資料描述符,
  • 定義了__get__()__set__()的資料描述符。

描述符的用處就是,當一個物件的某個屬性是一個描述符時,你訪問這個描述符型別的屬性,就會呼叫這個描述符的方法。譬如你獲取描述符的值時,會呼叫它的__get__().

我們先看一下這三個方法的docstring:

def __delete__(self, *args, **kwargs): # real signature unknown
    """ Delete an attribute of instance. """
    # 刪除一個例項的屬性

def __set__(self, *args, **kwargs): # real signature unknown
    """ Set an attribute of instance to value. """
    # 給例項的屬性設定一個值

def __get__(self, *args, **kwargs): # real signature unknown
    """ Return an attribute of instance, which is of type owner. """
    # 返回例項的屬性,該例項是 `owner` 型別的

例項:

class A(object):
    def __init__(self):
        self.value = None

    def __set__(self, instance, value): # self:類A的例項,也是類B的屬性a;instance:類 B 的例項 b;value:透過b.a的賦值
        print('set: self,instance,value',self,instance,value)
        self.value = value
        return self.value

    def __get__(self, instance, owner):# instance:類B的例項b;owner:類B
        print('get: self,instance,owner',self,instance,owner)
        return self.value

class B(object):
    a = A()

    def __init__(self):
        self.val = 20

1.上述程式碼中,有兩個類,A,B。先看類 B,有一個類屬性 a , 且 a 是類 A 的例項,我們先來例項化一下類 B ,看一下 類 B 和例項 b 的屬性:

b = B()
print(b.__dict__)
print(B.__dict__)

"""
{'val': 20}

{'__module__': '__main__', 'a': <__main__.A object at 0x0163FD70>, '__init__': <function B.__init__ at 0x07845078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""

可以看出,例項 b 的屬性中,只有一個 val 屬性 ;類 B 的屬性中,則有一個 a ,且 a 是類 A 的一個物件。

2.接下來,我們呼叫一下例項 a:

b = B()
b.a
B.a

"""
get: self,instance,owner <__main__.A object at 0x03458E68> <__main__.B object at 0x03458F28> <class '__main__.B'>

 get: self,instance,owner <__main__.A object at 0x03458E68> None <class '__main__.B'>
"""

我們看一下什麼意思:

  • 當呼叫 b.a 時,程式會自動去呼叫b.__getattribute__('a'), 也就是 b.__dict__['a'], 即透過物件 b 的字典去查詢屬性,但是在第一步我們已經知道, 物件 b 只有一個屬性 {'val': 20} ,既然在例項 b 中找不到 a。 所以會去父類中找,呼叫:type(b).__dict__['a'].__get__(b,type(b)), 也就是: B.__dict__['a'].__get__(b,B),列印了第一行的資訊,並返回了None

  • 當呼叫 B.a 時,會直接呼叫 B.__dict__['a'].__get__(None,B),所以第二處列印的資訊中間有個 None

3.現在,我們嘗試給 b.a 賦值

b = B()
b.a = 11
print(b.__dict__)
print(B.__dict__)
B.a = 12
print(b.__dict__)
print(B.__dict__)

"""
set: self,instance,value <__main__.A object at 0x037CFD70> <__main__.B object at 0x037CFDD0> 11
{'val': 20}

{'__module__': '__main__', 'a': <__main__.A object at 0x037CFD70>, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}

{'val': 20}

{'__module__': '__main__', 'a': 12, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""

可以看出,當呼叫了 b.a=11 時,呼叫了描述符的 __set__() , 但是物件 b 的例項屬性並沒有改變,依然只有 val=20, 同時類B的類屬性也沒有改變。 但是當呼叫 B.a = 12 時,類屬性 a 變成了12,並沒有呼叫描述符的__set__()方法。

所以,結合上面的 docstring,我們可以看出,資料描述符應該是給例項使用的,類使用它用處不大,至少沒法呼叫它的 __set__()

  • 如果類屬性的描述符物件和例項物件的屬性同名,如果查詢?

  • 也就是說,如果把類B改成:

class B(object):
	a = A()

	def __init__(self):
		self.val = 20
		self.a = 11  # 這裡同名的屬性

此時呼叫 b.a ,會如何?

當類A是一個資料描述符,也就是說類A包含 __set__ 方法時,此時資料描述符優先順序高,所以例項屬性 self.a 其實就是對類屬性 a 的賦值,會呼叫資料描述符的 __set__ 方法:

set: self,instance,value <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> 11
get: self,instance,owner <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> <class '__main__.B'>
11

當類A是一個非資料描述符,那麼例項的字典優先順序高,所以會使用例項字典中的資料,即結果:

11

屬性查詢優先順序:

  1. obj.__getattribute__()

  2. 資料描述符

  3. 例項的字典

  4. 類的字典

  5. 非資料描述符

  6. 父類的字典

  7. __getattr__

class Quantity1(object):
    def __get__(self, instance, owner):
        return 2

    def __set__(self, instance, val):
        pass


class Quantity2(object):
    def __get__(self, instance, owner):
        return 5


class A(object):
    val = 6  # 6 父類屬性
    x = None


class B(A):
    val = Quantity2()  # 5 非覆蓋型描述符
    val = 4  # 4 類屬性
    val = Quantity1()  # 2 覆蓋型描述符

    def __init__(self):
        super(B, self).__init__()
        self.val = 3

    def __getattr__(self, name):  # 7 __getattr__
        return 7

    def __getattribute__(self, name):  # 1 __getattribute__
        return 1

b = B()
print(b.val)

說了一堆有的沒的,其實描述符就是一個特殊的實現,當你的一個物件的屬性是描述符時,設定/賦值/讀取 這個屬性,都會觸發這個描述符內部相應實現的方法。從而可以實現一些定製化的內容。

相關文章