python中基於descriptor的一些概念(下)


3. Descriptor介紹

3.1 Descriptor代碼示例

class RevealAccess(object):
    """創建一個Descriptor類,用來打印出訪問它的操作信息
    "
""

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print 'Retrieving', self.name
        return self.val

    def __set__(self, obj, val):
        print 'Updating' , self.name
        self.val = val
#使用Descriptor
 class MyClass(object):
       #生成一個Descriptor實例,賦值給類MyClass的x屬性
       x = RevealAccess(10, 'var "x"')
       y = 5 #普通類屬性
運行結果:

3.2 定義

descriptor可以說是一個綁定了特定訪問方法的類屬性,這些訪問方法是重寫了descriptor
protocol中的三個方法,分別是__get__, __set__, __del__方法。如果三個中任一一個方法在
對象中定義了,就說這個對象是一個descriptor對象,可以把這個對象賦值給其它屬性。descriptor protocol
可以看成是一個有三個方法的接口。
 
通常對一個實例的屬性的訪問操作,如get, set, delete是通過實例的__dict__字典屬性進行的,
例如,對於操作a.x,會一個查找鏈從a.__dict['x'](實例的字典),再到type(a).__dict__['x'](類的
字典),再到type(a)的父類的字典等等。代碼如下:
 
可以看出類和實例的字典屬性的值的內容其它是不一樣的,因為實例中有綁定屬性的存在。type(a)
返回就是實例a的類型,類A。
 
如果這個需要被查找的屬性是一個定義了descriptor協議方法的對象,那么python就不會按照默認的
查找方式,而是調用descriptor協議中定義的方法去做處理。descriptor只對新式類和新式實例有效。

3.3 Descriptor Protocol(協議)

有下面這三個方法
get__(self, obj, type=None) --> value
set__(self, obj, value) --> None
delete__(self, obj) --> None
 
只要對象重寫任何上面的一個方法,對象就被看作是descriptor,就可以不去采用默認的查找屬性的順序。
 
 
如果一個對象同時定義了__get__,__set__方法,被看作是data descriptor;只定義了__get__,被稱
為non-data descriptor。如果實例字典中有一個key和data descriptor同名,那么查找時優先采用
data descriptor;如果實例字典中有一個key和non-data descriptor同名,那么優先采用實例字典的
方法。
 
 
創建一個只讀data descriptor,只需要在同時定義__get__,__set__方法的同時,讓__set__方法拋出異常
AttributeError。

3.4 Descriptor調用方法

可以直接使用descriptor實例進行方法調用,如d.__get__(obj),但我這樣嘗試會報錯。。。
 
一般是在屬性訪問的時候自動被調用,例如obj.d是在obj實例的字典屬性中查找d變量,如果d定義了__get__
方法和__set__方法,是一個data descriptor,則根據上面提到的優先級,會自動去調用 d.__get__(obj)。
 
對於實例來說,對於任意的屬性訪問實現的內部機制是使用object.__getatrribute__,
把b.x轉化為type(b).__dict__['x'].__get__(b, type(b)),實現的優先級是按data descriptor > instance variables
> non-data descriptor > __getattr__(如果定義了的話)。
 
對於類來說,是使用type.__getattribute__,把B.x轉化為B.__dict__['x'].__get__(None, B)。
 
用純python語言來描述的類屬性的訪問的話,大概是這個樣子:
def __getattribute__(self, key):
    "模擬type.__getattribute__()實現"
    #通過默認方式查找到目標
    v = object.__getattribute__(self, key)
    #如果目標屬性含有__get__方法,則表示它是一個descriptor
    if hasattr(v, '__get__'):
       #優先使用descriptor中定義的方法返回值
       return v.__get__(None, self)
    #如果不是descriptor,就按默認的方式返回
    return v
 
需要注意的幾點:
  • descriptor是被__getattribute__方法調用的。
  • 重寫__getattribute__方法,會阻止自動的descriptor調用,必要時需要你自己加上去。
  • __getattribute__方法只在新式類和新式實例中有用。
  • object.__getattribute__和class.__getattribute__會用不一樣的方式調用__get__
  • data descriptors總是覆蓋instance dictionary
  • non-data descriptors有可能被instance dictionary覆蓋
使用super()返回的對象也有一個__getattribute__方法來調用descriptor。對於super(B, obj).m()
是在obj.__class__.__mro__(類屬性__mro__)查找路徑中找類B的基類A,然后再調用A.__dict__['m'].__get__(obj, A)。
如果m不是descriptor,則直接返回;如果不在A的字典里,則會使用object.__getattribute__進行查找。
 
可以看到,descriptor實現的細節被定義在了object, type和super()的__getattribute__方法中。新式類從object繼承了
這一特性,或者也可以通過元類的實現去完成類似的用法,同樣的,類定義時也可以通過重寫__getattribute__方法來關閉
descirptor的調用。

4. 基於Descriptor實現的功能

descriptor協議是簡單而又強大的,新式類中的一些新特性就是利用descriptor功能封裝成一個獨立的函數調用,如:
  • Property
  • 綁定和非綁定方法
  • 靜態方法
  • 類方法
  • super

4.1 property

調用proprety()是一種創建data descriptor的一種簡潔的方式,函數結構如下:
property(fget=None, fset=None, fdel=None, doc=None) #返回的是property對象,可以賦值給某屬性,
propety方法有四個參數,只要對沒有進行賦值的參數進行訪問就會報錯。
 
x 是 C 的一個實例, attrib是C中定義的一個property屬性:
當你引用 x.attrib 時, python調用 fget 方法取值給你.
當你為x.attrib賦值: x.attrib=value 時, python調用 fset方法, 並且value值做為fset方法的參數,
當你執行del x.attrib 時, python調用fdel方法,
當你傳過去的名為 doc 的參數即為該屬性的文檔字符串.
 
用法如下:
class C(object):
    def getX(self):
        print 'get x'
        return self.__x
    def setX(self, value):
        print 'set x', value
        self.__x = value
    def delX(self):
        print 'del x'
        del self.__x
    x = property(getX, setX, delX, "This is 'x' property.")
運行結果如下:
 
非常方便地就改變了默認的訪問屬性x的方式。又如,我們定義一個只讀property屬性:
class Rect(object):
    def __init__(self, width, heigth):
        self.width = width
        self.heigth = heigth
    def getArea(self):
        return self.width * self.heigth
    area = property(getArea, doc='area of the rectangle')
只需要傳入fget參數就可以,運行如下:
  
屬性area為只讀,任何重新綁定和刪除的操作都會報錯。這是因為我們只定義了fget方法。
 
properties所做的事情與那些特殊方法__getattr__, __setattr__, __delattr__ 等是極其相似的,
不過同樣的工作它干起來更簡單更快捷.  區別在於:在經典類中,當你想要改變屬性的訪問方式時,
只能重載__getattr__,__setattr__方法,不過這樣會對所有的屬性訪問方式進行改動;而使用
property方法就可以在不影響其它屬性的前提下,任意地對某個屬性的訪問方式進行改動,這樣做
更加靈活。
 
如果要用python語言來描述property功能實現的話,可以把property對象定義為這樣一個descritptor是:
class Property(object):
    "模擬在Objects/descrobject.c文件中的PyProperty_Type()函數"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        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)
 
關於property()方法的幾點聲明:
1. 它不適用於經典類,但你在經典類中使用的時候,也不會報錯,表面上好像OK,但實際上是不會調用
參數中你設置的訪問函數的。比如,當你設置一個新的屬性時,經典類只是傳統的在__dict__上加上了它,
而不去調用fset函數進行設置。也許你可以在__setattr__函數中修復這一問題,但代價太高。
 
2. property()函數的四個參數,應該為methods(帶self參數的那種),而不是function.
 
3. 當你使用類去訪問屬性的時候,property設置的函數是不會被調用的。只有用實例去訪問才會調用。

4.2 函數和方法,綁定與非綁定

Python的面向對象特性是基於函數的,函數的實現是需要使用到non-data descriptor的功能。
 
類字典把方法存放為函數。在類定義中,方法是由def或者lambda聲明的。和一般函數不同的是,方法的第一個
參數是self對象。
 
為了支持方法的調用,在訪問方法屬性的時候,functions使用相應的__get__方法。這就意味着所有的函數都是non-data
descriptor,用於根據類或者對象的調用來返回unbound或者bound的方法。用python語言可以這樣描述:
class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "模擬Objects/funcobject.c文件中的func_descr_get()"
        return types.MethodType(self, obj, objtype)
可以在解釋器中運行一下:
可以看到方法在字典中存放的類型其實是函數對象,bound和unbound方法是兩個不同的類型。
內部的實現其實是一個同一個對象,不同的是這個對象的im_self屬性是否被賦值,或者是設為None。

4.3 super

在支持多繼承的語言中,討論誰是父類,感覺意義不大,尤其是像之類mro的菱形問題,父類是誰就更
說不清了。需要強調的是super不會返回父類,它返回的是代理對象。理對象就是利用委托(delegation)
使用別的對象的方法來實現功能的對象。
 
super返回的是一個定制了__getattribute__方法的對象,是一個代理對象,它可以訪問MRO中的方法。形式如下:
super(cls, instance-or-subclass).method(*args, **kw)
可以轉化為:
right-method-in-the-MRO-applied-to(instance-or-subclass, *args, **kw)
需要注意的是,第二個參數instnce-or-subclass可以是第一個參數的實例。
如果返回了非綁定的方法,調用的時候需要加上第一個self參數。
 
通過descriptor的實現,可以說super也是一個non-data descriptor類。也就是實現了
__get__(self, obj, objtyp=None)的類。
假設descr是C類的一個descriptor,C.descr實現上調用的是descr.__get__(None, C);
如果是實例來調用,c.descr調用的是descr.__get__(c, type(c))。
 
super功能用python語言來描述的話,可以是這樣:
class Super(object):
    def __init__(self, type, obj=None):
        self.__type__ = type
        self.__obj__ = obj
    def __get__(self, obj, type=None):
        if self.__obj__ is None and obj is not None:
            return Super(self.__type__, obj)
        else:
            return self
    def __getattr__(self, attr):
        if isinstance(self.__obj__, self.__type__):
            starttype = self.__obj__.__class__
        else:
            starttype = self.__obj__
        mro = iter(starttype.__mro__)
        for cls in mro:
            if cls is self.__type__:
                break
        # Note: mro is an iterator, so the second loop
        # picks up where the first one left off!
        for cls in mro:
            if attr in cls.__dict__:
                x = cls.__dict__[attr]
                if hasattr(x, "__get__"):
                    x = x.__get__(self.__obj__)
                return x
        raise AttributeError, attr

5. 結尾

在這里,就介紹完了基於descriptor的新式類的新特性。歡迎大家討論。






免責聲明!

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



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