聊聊Python中的描述符


描述符是實現描述符協議方法的Python對象,當將其作為其他對象的屬性進行訪問時,該描述符使您能夠創建具有特殊行為的對象。

通常,描述符是具有“綁定行為”的對象屬性,其屬性訪問已被描述符協議中的方法所覆蓋。這些方法是__get __(),__set __()和__delete __()。如果為對象定義了這些方法中的任何一種,則稱其為描述符。屬性訪問的默認行為是從對象的字典中獲取,設置或刪除屬性。例如,a.x具有一個查找鏈,查找鏈從a .__ dict __ ['x']開始,然后鍵入(a).__ dict __ ['x'],並繼續遍歷類型(a)的基類(不包括元類)。如果查找到的值是定義描述符方法之一的對象,則Python可能會覆蓋默認行為並改為調用描述符方法。優先鏈在何處發生取決於定義了哪些描述符方法。描述符是功能強大的通用協議。它們是屬性,方法,靜態方法,類方法和super()背后的機制。在Python本身中使用它們來實現2.2版中引入的新樣式類。

descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None

定義這些方法中的任何一個,對象被視為描述符,並且在被視為屬性時可以覆蓋默認行為。

如果對象定義了__set __()或__delete __(),則將其視為數據描述符。僅定義__get __()的描述符稱為非數據描述符(它們通常用於方法,但也可以用於其他用途)。數據和非數據描述符在實例字典中替代計算方式方面有所不同。如果實例的字典中的屬性名稱與數據描述符的名稱相同,則以數據描述符為准。如果實例的字典中具有與非數據描述符同名的屬性,則該字典屬性優先。我們來看一下例子:

class lazy(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val

class Circle(object):
    def __init__(self, radius):
        self.radius = radius
        
    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2

    def __getattr__(self, item):
        return 1


c = Circle(4)
print(c.area)
print(c.area)

輸出結果是

evalute
50.24
50.24

我們定義了一個描述符的類 lazy,它只實現了__get__方法,是一個非數據的描述符,我們用它定義了類Circle中的area方法,所以area方法成為了一個描述符的對象,可以看到,在第一次調用c.area的時候,執行了area的方法,打印了"evalute",在第二次的時間就直接輸出了結果,沒有指向area的方法,這是為什么呢?

那么重點來了,可以看到在lazy定義的__get__方法中,執行了被描述對象的方法,也就是這里的area函數,獲取到結果之后,給當前的instance設置了一個同名的屬性,並且設值為結果,這樣下次在調用的時間,因為這是一個非數據的描述符,看上面的黑體字,實例的字典中的屬性名稱與數據描述符的名稱相同,則以數據描述符為准。所以會取你剛剛設置的屬性的值,不會再去取描述符的值。我們再來看看數據描述符的一個例子:

class lazy(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val

    def __set__(self, instance, value):
        pass


class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2

    def __getattr__(self, item):
        return 1


c = Circle(4)
print(c.area)
print(c.area)
 

我們看一下輸出的結果:

evalute
50.24
evalute
50.24

  

 

同樣的定義,只是在描述符中添加了__set__方法,就會執行調用描述符定義的屬性,和非描述符的調用方式天壤之別。這就是這個高級特性的特別之處。我們可以使用非數據描述符做惰性加載,只計算一次,下次直接取值,我在工作中也是這樣干的。

 

知其然,知其所以然,我們來看一下是為什么:

根據官方的解釋,描述符可以通過其方法名稱直接調用。例如,d .__ get __(obj)。另外,更常見的是在屬性訪問時自動調用描述符。例如,obj.d在obj的字典中查找d。如果d定義了方法__get __(),則根據下面列出的優先級規則調用d .__ get __(obj)。調用的細節取決於obj是對象還是類。

    對於對象,機制位於object .__ getattribute __()中,它將b.x轉換為type(b).__ dict __ ['x'] .__ get __(b,type(b))。該實現通過優先級鏈進行工作,該優先級鏈賦予數據描述符優先於實例變量的優先級,實例變量優先於非數據描述符的優先級,並為__getattr __()分配最低優先級。完整的C實現可在Objects / object.c中的PyObject_GenericGetAttr()中找到。 

對於類,機制的類型為.__ getattribute __(),它將B.x轉換為B .__ dict __ ['x'] .__ get __(無,B)。在純Python中,它看起來像:

def __getattribute__(self, key): "Emulate type_getattro() in Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v

要記住的重要點是:

  • 描述符由__getattribute __()方法調用

  • 重寫__getattribute __()防止自動描述符調用

  • object .__ getattribute __()和type .__ getattribute __()對__get __()進行不同的調用。

  • 數據描述符始終會覆蓋實例字典。非數據描述符可以被實例字典覆蓋。

 

具體的可以查看Python的c源碼。

以上就是今天要和大家一起學習的內容。

代碼地址

https://github.com/oldman1991/testdemo/blob/master/0028_python_descriptor.py

 

更多問題歡迎關注微信公眾號

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM