python的__get__、__set

  • 2020 年 1 月 19 日
  • 筆記

內容:     描述符引導         摘要         定義和介紹         描述符協議         調用描述符         樣例         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