描述符詳解

初學py的時候你們都說描述符是高級內容難度較大,仔細擼過文檔以後感受還好,不過用起來確實不那麼直觀。html

按照慣例,先來看一下官文API文檔:python

In general, a descriptor is an object attribute with 「binding behavior」, one whose attribute access has been overridden by methods in the descriptor protocol: __get__(), __set__(), and __delete__().
If any of those methods are defined for an object, it is said to be a descriptor.

總的來講,描述符是一個帶有綁定行爲的對象屬性,訪問這個對象屬性的時候會被描述符協議中的方法覆蓋,能夠理解爲一種hook機制。app

The following methods only apply when an instance of the class containing the method (a so-called descriptor class) appears in an owner class.
the descriptor must be in either the owner’s class dictionary or in the class dictionary for one of its parents.

描述符協議包括三個魔術方法:less

  • object.__get__(self, instance, owner):經過owner class或其實例訪問描述符實例時被調用。
  • object.__set__(self, instance, value):給owner class的中的描述符實例賦值的時候被調用。
  • object.__delete__(self, instance):刪除owner class中的描述符實例的時候被調用,這個方法不多用。

一個對象只要實現其中之一就是一個描述符。
描述符必須在owner class或者其父類的屬性字典中,也就是owner的類屬性,定義成實例屬性無效。ide

For instance bindings, the precedence of descriptor invocation depends on the which descriptor methods are defined.
A descriptor can define any combination of __get__(), __set__() and __delete__().
If it does not define __get__(), then accessing the attribute will return the descriptor object itself unless there is a value in the object’s instance dictionary.
If the descriptor defines __set__() and/or __delete__(), it is a data descriptor. 
if it defines neither, it is a non-data descriptor. 

Normally, data descriptors define both __get__() and __set__(), while non-data descriptors have just the __get__() method.
Data descriptors with __set__() and __get__() defined always override a redefinition in an instance dictionary.
In contrast, non-data descriptors can be overridden by instances.

若是描述符class中沒有定義__get__()方法,那麼訪問描述符實例僅僅會返回一個描述符class的實例,而不會觸發調用。
若是定義了__set__()和__delete__ ()其中之一就是一個數據描述符
若是都沒有定義,即只定義__get__,那麼就是一個非數據描述符code

一般,數據描述符會同時定義__get__()和__set__(),非數據描述符僅定義__get__()。orm

其實,基礎概念看到這裏就能夠了,弄清何時觸發__get__()、何時觸發__set__()便可。
至於描述符實例啥時候會被覆蓋這一點常常會把萌新弄暈,並且這一點屬於面向對象基礎知識,跟描述符自己無關。htm


下面舉兩個例子簡單看一下非數據描述符和數據描述符。對象

非數據描述符:ip

class A:
    def __init__(self):
        self.a = 'a'
        print('A init')

    def __get__(self, instance, owner):
        print('A.__get__ {} {} {}'.format(self, instance, owner))
        return self

class B:
    x = A()
    def __init__(self):
        self.b = 'b'  # 這個僅僅是一個實例屬性,與描述符無關
        # self.x = 'x'   # 因爲描述符沒有定義__set__方法,這裏不能這樣定義,實例化的時候實例屬性會覆蓋類屬性,描述符將被覆蓋
        print('B init')

print(B.x)  # 這裏會調用A的__get__方法,返回的是__get__方法的返回值
# A init
# A.__get__ <__main__.A object at 0x10302d5c0> None <class '__main__.B'>
# <__main__.A object at 0x10302d5c0>

b = B()
print(b.x)  # 這裏也會調用A的__get__方法,不過這裏會把b實例傳遞進去
# B init
# A.__get__ <__main__.A object at 0x10302d5c0> <__main__.B object at 0x10302d748> <class '__main__.B'>
# <__main__.A object at 0x10302d5c0>

# 爲了可以獲取描述符實例的屬性,可讓__get__方法返回self
print(b.x.a)
# A.__get__ <__main__.A object at 0x10302d5c0> <__main__.B object at 0x10302d748> <class '__main__.B'>
# a

print(b.b)
# b

數據描述符:

class A:
    def __init__(self):
        self.a = 'a'
        print('A init')

    def __get__(self, instance, owner):
        print('A.__get__ {} {} {}'.format(self, instance, owner))
        return self

    def __set__(self, instance, value):
        print('A.__set__ {} {} {}'.format(self, instance, value))
        instance.c = value

class B:
    x = A()
    def __init__(self):
        self.b = 'b'
        self.x = 'c'    # 因爲描述符定義了__set__方法,這裏對描述符實例的賦值操做會調用描述符的__set__方法,並無覆蓋描述符
        print('B init')

b = B()
print(b.__dict__)
# {'b': 'b', 'c': 'c'}

b.x = 100
print(b.__dict__)
# {'b': 'b', 'c': 100}

因爲涉及兩個類和其實例的交互,闡述起來也不是很容易呢。不過仔細捋一下,其實並非很複雜,有沒有?


最後,須要提醒一下,staticmethod、classmethod、property三個內置裝飾器都是經過描述符實現的。
其中,staticmethod和classmethod是經過非數據描述符實現的,property是經過數據描述符實現的。

Python methods (including staticmethod() and classmethod()) are implemented as non-data descriptors.
The property() function is implemented as a data descriptor.

一般你們知道怎麼用這幾個裝飾器,但每每可能不會細究是如何實現的,下篇繼續來講這幾個裝飾器是如何實現的。

參考:
https://docs.python.org/3/reference/datamodel.html#implementing-descriptors
https://docs.python.org/3/reference/datamodel.html#invoking-descriptors

相關文章
相關標籤/搜索