Manjusaka

Manjusaka

Python 描述符入門指北

很久都沒寫 Flask 代碼相關了,想想也真是惭愧,然並卵,這次還是不寫 Flask 相關,不服你來打我啊(就這麼賤,有本事咬我啊
這次我來寫一下 Python 一個很重要的東西,即 Descriptor (描述符)

初識描述符#

老規矩,Talk is cheap,Show me the code. 我們先來看看一段代碼

class Person(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, first_name, last_name):
        """Constructor"""
        self.first_name = first_name
        self.last_name = last_name

    #----------------------------------------------------------------------
    @property
    def full_name(self):
        """
        Return the full name
        """
        return "%s %s" % (self.first_name, self.last_name)

if __name__=="__main__":
    person = Person("Mike", "Driscoll")
    print(person.full_name)
    # 'Mike Driscoll'
    print(person.first_name)
    # 'Mike'

這段代大家肯定很熟悉,恩,property 嘛,誰不知道呢,但是 property 的實現機制大家清楚麼?什麼不清楚?那還學個毛的 Python 啊。。。開個玩笑,我們看下面一段代碼

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

看起來是不是很複雜,沒事,我們來一步步的看。不過這裡我們首先給出一個結論:Descriptors 是一種特殊 的對象,這種對象實現了 __get____set____delete__ 這三個特殊方法。

详解描述符#

说说 Property#

在上文,我們給出了 Propery 實現代碼,現在讓我們來詳細說說這個

class Person(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, first_name, last_name):
        """Constructor"""
        self.first_name = first_name
        self.last_name = last_name

    #----------------------------------------------------------------------
    @Property
    def full_name(self):
        """
        Return the full name
        """
        return "%s %s" % (self.first_name, self.last_name)

if __name__=="__main__":
    person = Person("Mike", "Driscoll")
    print(person.full_name)
    # 'Mike Driscoll'
    print(person.first_name)
    # 'Mike'

首先,如果你對裝飾器不了解的話,你可能要去看看這篇文章,簡而言之,在我們正式運行代碼之前,我們的解釋器就會對我們的代碼進行一次掃描,對涉及裝飾器的部分進行替換。類裝飾器同理。在上文中,這段代碼

    @Property
    def full_name(self):
        """
        Return the full name
        """
        return "%s %s" % (self.first_name, self.last_name)

會觸發這樣一個過程,即 full_name=Property(full_name) 。然後在我們後面所實例化對象之後我們調用 person.full_name 這樣一個過程其實等價於 person.full_name.__get__(person) 然後進而觸發__get__() 方法裡所寫的 return self.fget(obj) 即原本上我們所編寫的 def full_name 內的執行代碼。

這個時候,同志們可以去思考下 getter() ,setter() , 以及 deleter() 的具體運行機制了 =。= 如果還是有問題,歡迎在評論裡進行討論。

關於描述符#

還記得之前我們所提到的一個定義麼:Descriptors 是一種特殊的對象,這種對象實現了 __get____set____delete__ 這三個特殊方法。然後在 Python 官方文檔的說明中,為了體現描述符的重要性,有這樣一段話:“They are the mechanism behind properties, methods, static methods, class methods, and super (). They are used throughout Python itself to implement the new style classes introduced in version 2.2. ” 簡而言之就是 先有描述符後有天,秒天秒地秒空氣。恩,在新式類中,屬性,方法調用,靜態方法,類方法等都是基於描述符的特定使用。

OK,你可能想問,為什麼描述符是這麼重要呢?別急,我們接著看

使用描述符#

首先請看下一段代碼

class A(object): #注:在 Python 3.x 版本中,對於 new class 的使用不需要顯式的指定從 object 類進行繼承,如果在 Python 2.X(x>2)的版本中則需要
    def a(self):
        pass
if __name__=="__main__":
    a=A()
    a.a()

大家都注意到了我們存在著這樣一個語句 a.a() ,好的,現在請大家思考下,我們在調用這個方法的時候發生了什麼?
OK?想出來了麼?沒有?好的我們繼續
首先我們調用一個屬性的時候,不管是成員還是方法,我們都會觸發這樣一個方法用於調用屬性 __getattribute__() , 在我們的 __getattribute__() 方法中,如果我們嘗試調用的屬性實現了我們的描述符協議,那麼會產生這樣一個調用過程 type(a).__dict__['a'].__get__(b,type(b))。好的這裡我們又要給出一個結論了:“在這樣一個調用過程中,有這樣一個優先級順序,如果我們所嘗試調用屬性是一個 data descriptors ,那麼不管這個屬性是否存在我們的實例的 __dict__ 字典中,優先調用我們描述符裡的 __get__ 方法,如果我們所嘗試調用屬性是一個 non data descriptors,那麼我們優先調用我們實例裡的 __dict__ 裡的存在的屬性,如果不存在,則依照相應原則往上查找我們類,父類中的 __dict__ 中所包含的屬性,一旦屬性存在,則調用 __get__ 方法,如果不存在則調用 __getattr__() 方法”。理解起來有點抽象?沒事,我們馬上會講,不過在這裡,我們先要解釋下 data descriptorsnon data descriptors,再來看一個例子。什麼是 data descriptorsnon data descriptors 呢?其實很簡單,在描述符中同時實現了 __get____set__ 協議的描述符是 data descriptors ,如果只實現了 __get__ 協議的則是 non data descriptors 。好了我們現在來看個例子:

import math
class lazyproperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value
class Circle:
    def __init__(self, radius):
        self.radius = radius
        pass

    @lazyproperty
    def area(self):
        print("Com")
        return math.pi * self.radius * 2

    def test(self):
        pass
if __name__=='__main__':
    c=Circle(4)
    print(c.area)

好的,讓我們仔細來看看這段代碼,首先類描述符 @lazyproperty 的替換過程,前面已經說了,我們不在重複。接著,在我們第一次調用 c.area 的時候,我們首先查詢實例 c__dict__ 中是否存在著 area 描述符,然後發現在 c 中既不存在描述符,也不存在這樣一個屬性,接著我們向上查詢 Circle 中的 __dict__ ,然後查找到名為 area 的屬性,同時這是一個 non data descriptors ,由於我們的實例字典內並不存在 area 屬性,那麼我們便調用類字典中的 area__get__ 方法,並在 __get__ 方法中通過調用 setattr 方法為實例字典註冊屬性 area 。緊接著,我們在後續調用 c.area 的時候,我們能在實例字典中找到 area 屬性的存在,且類字典中的 area 是一個 non data descriptors,於是我們不會觸發代碼裡所實現的 __get__ 方法,而是直接從實例的字典中直接獲取屬性值。

描述符的使用#

描述符的使用面很廣,不過其主要的目的在於讓我們的調用過程變得可控。因此我們在一些需要對我們調用過程實行精細控制的時候,使用描述符,比如我們之前提到的這個例子

class lazyproperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value

    def __set__(self, instance, value=0):
        pass


import math


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

    @lazyproperty
    def area(self, value=0):
        print("Com")
        if value == 0 and self.radius == 0:
            raise TypeError("Something went wring")

        return math.pi * value * 2 if value != 0 else math.pi * self.radius * 2

    def test(self):
        pass

利用描述符的特性實現懶加載,再比如,我們可以控制屬性賦值的值

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value=None):
        if value is None:
            raise TypeError("You can`t to set value as None")
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

class test():
    def __init__(self, value):
        self.value = value

    @Property
    def Value(self):
        return self.value

    @Value.setter
        def test(self, x):
            self.value = x

如上面的例子所描述的一樣,我們可以判斷所傳入的值是否有效等等。

總結#

Python 中的描述符可以說是新式類調用鏈中的根基,所有的方法,成員,變量調用時都將會有描述符的介入。同時我們可以利用描述符的特性來將我們的調用過程變得更為可控。這一點,我們可以在很多著名框架中找到這樣的例子。

參考#

1.《Python Cookbook》 8.10 章 P271
2.《Descriptor HowTo Guid》
3.《Python 黑魔法》

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。