python的__get__、__set__、__delete__(1)

丁壯發表於2018-10-11

內容:
    描述符引導
        摘要
        定義和介紹
        描述符協議
        呼叫描述符
        樣例
        Properties
        函式和方法
        靜態方法和類方法
        
    
摘要
    定義並展示如何呼叫描述符,展示自定義描述符和幾個內建的python描述符,包括函式、屬性、靜態方法和類方法,通過給出一個Python的示例應用來展示描述符是如何工作的.
    熟練掌握描述符不僅讓你擁有python使用的額外技巧,並且可以加深對Python內部如何工作的理解,提升對程式設計的能力,而且體會到python的設計優雅之處

定義和介紹
    一般來說,描述符是帶有“繫結行為”的物件屬性,它的屬性訪問已經被描述符協議中的方法覆蓋了.這些方法是__get__(),__set__(),和__delete__().
    如果一個物件定義了這些方法中的任何一個,它就是一個描述符.

    預設的屬相訪問是從物件的字典中 get, set, 或者 delete 屬性,;例如a.x的查詢順序是:
    a.x -> a.__dict__[`x`] -> type(a).__dict__[`x`] -> type(a)的基類(不包括元類),如果查詢的值是物件定義的描述方法之一,python可能會呼叫描述符方法來過載預設行為,
    發生在這個查詢環節的哪裡取決於定義了哪些描述符方法
    注意,只有在新式類中描述符才會起作用(新式類繼承type或者object class)
    描述符是強有力的通用協議,屬性、方法、靜態方法、類方法和super()背後使用的就是這個機制,描述符簡化了底層的c程式碼,併為Python程式設計提供了一組靈活的新工具

描述符協議

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

    descr.__set__(self, obj, value) -> None

    descr.__delete__(self, obj) -> None

    定義任何上面三個方法的任意一個,這個物件就會被認為是一個描述符,並且可以在被作為物件屬性時過載預設的行為, 如果一個物件定義了__get__() 和 __set__(),它被認為是一個資料描述符.只定義 __get__()被認為是非資料描述符,資料和非資料描述符的區別在於:如果一個例項的字典有和資料描述符同名的屬性,那麼資料描述符會被優先使用,如果一個例項的字典實現了無資料描述符的定義,那麼這個字典中的屬性會被優先使用,實現只讀資料描述符,同時定義__get__()和__set__(),在__set__()中丟擲AttributeError.

描述符呼叫

    描述符可以直接用方法名稱呼叫,比如:d.__get__(obj)
    
    然而,描述符更常用的方式是屬性訪問時被自動呼叫,例如:obj.d 在obj的字典中查詢d,如果d定義了方法__get__(),然後d.__get__(obj)會被通過下面的優先順序列表呼叫
    詳細的呼叫依賴於obj是一個物件還是一個類,不管哪種方式,描述符只工作在新式物件和類,如果一個類是object的子類(繼承object),這個類就是一個新式類
    
    對於物件來說,object.__getattribute__() 把b.x 變為 type(b).__dict__[`x`].__get__(b, type(b)) .優先順序順序:
    資料描述符 > 例項變數 > 非資料描述符,__getattr__()具有最低優先順序(如果實現了的話),C語言的實現可以在 Objects/object.c 中 PyObject_GenericGetAttr() 檢視.
    
    對於類來說,type.__getattribute__() 把 B.x 變為 B.__dict__[`x`].__get__(None, B),程式碼實現為:
    

        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__()會阻止描述符自動呼叫
        __getattribute__()只適用於新式類和物件
        object.__getattribute__()和type.__getattribute__()對__get__()的呼叫不一樣
        資料描述符會過載例項字典
        非資料描述符可能會被例項字典過載
        
     super()返回的物件會使用定製__getattribute__()方法來呼叫描述符,呼叫super(B, obj).m() 會在緊鄰著B的基類A搜尋obj.__class__.__mro__然後返回A.__dict__[`m`].__get__(obj, B),如果不是一個描述符,返回未改變的m
     如果不在字典中,m會呼叫 object.__getattribute__() 查詢

     注意:在python2.2,如果m是一個資料描述符,super(B, obj).m() 會呼叫__get__(),在python2.3,無資料描述符也會執行呼叫,除非是個舊式類,super_getattro() 的細節在Objects/typeobject.c中
     
     上面展示的是描述符在object, type, and super() 的 __getattribute__() 方法中的實現機制,繼承object的類自動實現或者他們有一個元類提供類似的功能,同樣,過載 __getattribute__()可以停止描述符的呼叫
     
描述符例子
    下面的程式碼建立了一個類,每次訪問get或者set都會列印一條資訊.過載__getattribute__()也可以使每個屬性實現這一方法,然而,描述符在檢視特定的屬性時比較有用

        class RevealAccess(object):
            """A data descriptor that sets and returns values
               normally and prints a message logging their access.
            """

            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

        >>> class MyClass(object):
        ...     x = RevealAccess(10, `var "x"`)
        ...     y = 5
        ...
        >>> m = MyClass()
        >>> m.x
        Retrieving var "x"
        10
        >>> m.x = 20
        Updating var "x"
        >>> m.x
        Retrieving var "x"
        20
        >>> m.y
        5

    這個協議很簡單卻又可以提供令人為之一振的可能性.Properties, bound 和 unbound methods, 靜態方法和 類方法 都是基於描述符協議

Properties
    呼叫property()是一種建立資料描述符的方便方法,可以在訪問一個屬性的時候觸發方法的呼叫
    property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

    下面展示一個定義管理屬性x的典型的樣例:

        class C(object):
            def getx(self):return self.__x
            def setx(self, value):self.__x = value
            def delx(self):del self.__x
            x = property(getx, setx, delx, "I`m the `x` 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__)

 
    當使用者介面已經授權訪問屬性,這時候需求發生變化,property()可以提供便利, 例如,一個電子表格類可以通過單元(`b10`)授予對單元格值的訪問權.這時候,對程式的後續改進要求在每次訪問時重新計算單元格的值;然而,程式設計師不希望影響現有客戶端程式碼.解決方案是在屬性資料描述符中封裝對value屬性的訪問:

        class Cell(object):
            . . .
            def getvalue(self):
                "Recalculate the cell before returning value"
                self.recalc()
                return self._value
            value = property(getvalue)

函式和方法
    python的物件導向是建立在函式的基礎上,使用非資料描述符,兩者會結合的非常緊密.

    類的字典將方法比作函式儲存.在一個類的定義中,使用def和lambda來宣告方法,這是用於建立函式的常用工具. 唯一不同之處,就是第一個引數用來表示物件例項,python約定,例項引用可以使self或者this或者其他變數名稱

    為了支援方法呼叫,函式通過__get__()方法來實現屬性訪問時的方法繫結
    這說明所有的函式都是非資料描述符,它返回繫結或者非繫結方法依賴於它被物件還是類呼叫
    
    在python中的實現如下:

        class Function(object):
            . . .
            def __get__(self, obj, objtype=None):
                "Simulate func_descr_get() in Objects/funcobject.c"
                return types.MethodType(self, obj, objtype)

    在直譯器中展示函式描述符如何執行:

        >>> class D(object):
        ...     def f(self, x):
        ...         return x
        ...
        >>> d = D()
        >>> D.__dict__[`f`]  # Stored internally as a function
        <function f at 0x00C45070>
        >>> D.f              # Get from a class becomes an unbound method
        <unbound method D.f>
        >>> d.f              # Get from an instance becomes a bound method
        <bound method D.f of <__main__.D object at 0x00B18C90>>

    輸出說明繫結和未繫結方法是兩種不同型別,PyMethod_Type在 Objects/classobject.c 中實際的C實現是一個具有有兩種不同表現形式的單一物件,依賴於im_self是set還是null(等價C中的None)

    同樣,呼叫方法物件的效果依賴於im_self,如果set(繫結),原函式(儲存在im_func中)被呼叫,它的第一個引數設定為例項.
    如果unbound,所有的引數不做改變的傳給原函式,instancemethod_call()的C實現因為包含一些型別檢查會複雜一些

靜態方法和類方法
    無資料描述符提供一種簡單的機制將函式繫結為方法

    簡單地說,函式的__get__()方法會將函式被作為屬性訪問時轉換為方法,非資料描述符將 obj.f(*args) 呼叫為f(obj, *args).呼叫 klass.f(*args)變為f(*args)

    下面的表格彙總了繫結和它常見的兩種變化

        Transformation    Called from an Object    Called from a Class
        function          f(obj, *args)                 f(*args)
        staticmethod     f(*args)                   f(*args)
        classmethod        f(type(obj), *args)            f(klass, *args)

    呼叫c.f 或者 C.f等價於 object.__getattribute__(c, “f”) 或者 object.__getattribute__(C, “f”),不管從物件還是類中,這個函式都可以訪問到

    不需要self變數的方法適合使用靜態方法
    例如:一個統計包可能包括用於實驗資料的容器類,該類提供了用於計算依賴於資料的平均、平均值、中值和其他描述性統計資料的常規方法.可是可能有一些概念相關但不依賴資料的函式.
    例如:erf(x)是一個不依賴於特定資料集的函式,它可以從一個類或者函式呼叫:s.erf(1.5) –> .9332 或者 Sample.erf(1.5) –> .9332

    靜態方法返回原始函式:

        >>> class E(object):
        ...     def f(x):
        ...         print x
        ...     f = staticmethod(f)
        ...
        >>> print E.f(3)
        3
        >>> print E().f(3)
        3

    python版本使用非資料描述符的實現方法:

        class StaticMethod(object):
            "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

            def __init__(self, f):
                self.f = f

            def __get__(self, obj, objtype=None):
                return self.f

 

    與靜態方法不同,類方法在呼叫函式之前先將類的引用預新增到引數列表中.呼叫者不管是物件還是類,這種格式是相同的

        >>> class E(object):
        ...     def f(x):
        ...         print x
        ...     f = staticmethod(f)
        ...
        >>> print E.f(3)
        3
        >>> print E().f(3)
        3

    這種行為在函式只需要有類引用且不關心任何底層資料的情況下是有用的,類方法的一個用途是用來建立不同的類構造器,在python2.3中,類方法dict.fromkeys()可以使用一個key的列表來建立字典,python的實現方式:

        class Dict(object):
            . . .
            def fromkeys(klass, iterable, value=None):
                "Emulate dict_fromkeys() in Objects/dictobject.c"
                d = klass()
                for key in iterable:
                    d[key] = value
                return d
            fromkeys = classmethod(fromkeys)

 

    現在可以這樣建立一個字典:

        >>> Dict.fromkeys(`abracadabra`)
        {`a`:None, `r`:None, `b`:None, `c`:None, `d`:None}

    classmethod()使用無資料描述符協議實現:

        class ClassMethod(object):
            "Emulate PyClassMethod_Type() in Objects/funcobject.c"

            def __init__(self, f):
                self.f = f

            def __get__(self, obj, klass=None):
                if klass is None:
                    klass = type(obj)
                def newfunc(*args):
                    return self.f(klass, *args)
                return newfunc

 

本文翻譯原文地址:https://docs.python.org/2/howto/descriptor.html#id9

相關文章