Python HOWTO之屬性描述符

這篇文章主要是對官方文檔Python HOWTO之屬性描述符的翻譯。由于英語水平有限,基本上都是意譯。

摘要

本篇的內容主要是定義屬性描述符(descriptor),概述一下描述符協議的內容。通過自定義的的一個描述符和Python內建的描述符(functions, properties, static methods, class methods)來演示屬性描述符是如何調用的。同時會給出相同功能的Python實現代碼和一個簡單的程序。

屬性描述符不僅給一個大的工具集(暫時沒發現是什么)提供了接口,它還能加深理解Python的工作原理和優雅的設計思想。

定義和介紹

一般來說,一個描述符是一個有“綁定行為”的對象屬性,這個屬性訪問被描述符協議中的方法所覆蓋。這些方法是__get__(),__set__()和__delete__()。如果某個對象定義了其中一個,那么這個對象就可以被叫做描述符。

訪問屬性默認通過get,set或者是delete來操作對象屬性字典來實現。例如,a.x有一個查找隊列,從a.__dict__['x']開始,然后是type(a).__dict__['x'],接著是type(a)的基類(metaclass除外),以此類推。如果查找的是一個定義了描述符方法的對象,那么Python會覆蓋默認行為而去調用描述符方法。發生在優先級隊列的哪個位置取決于定義的描述符方法。注意,屬性描述符只適用于新式類(從object或者typ繼承的類)。

屬性描述符是一個強大的通用協議。它是properties, methods, static methods, class methods 和super()的調用原理。它貫穿整個Python,并且用來實現2.2版本中引進的新式類。屬性描述符簡化了底層的C代碼,還為日常Python編程提供了新的工具集。

描述符協議

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

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

descr.__delete__(self, obj) --> None

上面的三個方法就是協議的全部內容了。定義其中任意一個方法的對象就被稱為屬性描述符,能夠覆蓋默認的屬性查找規則。

如果一個對象同時定義了__get__和__set__方法,它被稱做數據描述符(data descriptor)。只定義__get__方法的對象則被稱為非數據描述符(non-data descriptor,一般用在函數方法上,其他用法也是可能的)。

數據和非數據描述符的區別在于如果某個實例屬性字典中有項和描述符同名,那么屬性訪問的優先級是不同的。數據描述符的優先級比實例字典中項的高,非數據描述符則相反。

舉個例子說明一下優先級問題:

class DataDesc(object):

    def __init__(self, name=None):
        self.name = name
        self.value = None

    def __get__(self, obj, type=None):
        return self.value

    def __set__(self, obj, value):
        self.value = value


class NonDataDesc(object):

    def __init__(self, name=None):
        self.name = name
        self.value = None

    def __get__(self, obj, type=None):
        return self.value


class DataTest(object):
    x = DataDesc()


class NonDataTest(object):
    x = NonDataDesc()

>>> d = DataTest()
>>> nd = NonDataTest()
>>> d.__dict__['x'] = 2
>>> nd.__dict__['x'] = 2
>>> print d.__dict__, nd.__dict__
{'x': 2} {'x': 2}
>>> print d.x, nd.x
None 2

如果想要構造一個只讀的數據描述符,同時定義__get__和__set__方法,并且__set__調用時引發一個AtrributeError異常。

屬性描述符調用

一個屬性描述符可以通過它的方法名直接調用。比如,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()找到。

對于類,訪問是調用type.__getattribute__(),其中將B.x轉換成B.__['x'].__get__(None, 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

需要記住下面幾個重要的點:

  1. 描述符通過__getattribute__()被調用
  2. 重寫__getattribute__()能夠改變自動的調用
  3. __getattribute__()只適用于新式類
  4. object.__getattribute__()和type.__getattribute__()調用__get__()的方式不同
  5. 數據描述符總是覆蓋實例字典
  6. 非數據描述符可能被實例字典覆蓋

super()返回的對象有一個自定義的__getattribute__()。調用super(B, obj).m()在obj.__class__.__mro__查找到緊跟在B后面的基類A,然后返回A.__dict__['m'].__get__(obj, B)。如果不是一個描述符,m被原封不動的返回。如果不在字典中,m轉而去調用object.__getattribute__()查找。

注意,在Python2.2中,運行super(B, obj).m()時,如果m是一個數據描述符,將會只調用__get__()。在Python2.3中,除了是舊式類,非數據描述符也會得到調用。具體實現在Objects/typeobject.c的super_getattro()中。

綜上所述,描述符機制嵌入到了object、type和super()的__getattribute__()方法中。如果類需要這個機制,必須繼承自object或者是有metaclass提供類似的功能。同樣的,也可以通過重寫__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

使用property()能夠把數據描述符變成屬性調用。形式如下:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

一個典型的用法:

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.")

也可以使用裝飾器:

class C(object):
  @property
  def x(self):
    return self.__x

  @x.setter
  def setx(self, value):
    self.__x = value

  @x.deleter
  del delx(self):
    self.__x

>>> c = C()
>>> c.x = 2
>>> c.x
2
>>> del c.x

proptery()是C實現的,我們這里給出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__)

一個電子表格類可能通過Cell('b10').value訪問某個單元,后面希望改進成每次訪問都重新計算。但是開發者不想直接改變現有的屬性訪問代碼。那么便可以用proptery數據描述符封裝屬性訪問。

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

Functions and methods

Python的面向對象特征是建立在基于函數的環境上。使用非數據描述符,兩者能夠無縫融合。

類字典中用函數(function)形式存儲方法(method)。在類的定義中,用def和lambda定義方法,這也是定義函數的方式。方法和普通函數唯一的區別是方法的第一個參數預留給了對象實例。按照Python的慣例,實例引用一般用self表示,當然也有可能用this或者其他變量表示。

為了支持方法調用,函數中包含了__get__()屬性。這意味著,所有的函數都是非數據描述符。對象和類的方法,__get__()返回值是不同的,分別綁定(bound)和非綁定(unbound)方法。如果用純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>>

bound和unbound方法是兩個不同的類型。C實現中只是一個相同的對象的兩種不同表現,區別就在于im_self被設置了或者是NULL值,具體實現位于Objects/classobject.cPyMethod_Type

同樣地,調用方法時有沒有im_self效果是不同的。如果被設置了,表明是bound方法,原始函數(存在im_func中)被調用,當然第一個參數被設置成對象實例。如果是unbound方法,所有參數原封不動地傳給原始函數。C實現instancemethod_call()會更加復雜,因為有很多類型檢測。

Static methods and class methods

函數有__get__()屬性,所以當它們被當成屬性訪問時會被轉變成方法。非數據描述符將obj.f(*args)轉換成了f(obj, *args)。調用klass.f(*args)變成了f(*args)。

下面這個表格總結了這轉變方式,以及兩個變種staticmethod和classmethod。

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等價于object.__getattribute__(c, "f"),調C.f等于object.__getattribute__(C, "f")。所以,對象和類對靜態方法的調用方式是統一的。靜態方法不需要self。

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

>>> print E.f(3)
3
>>> print E().f(3)
3

純Pythond的staticmethod()實現可能是這樣的:

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(klass, x):
          return klass.__name__, x
     f = classmethod(f)

>>> print E.f(3)
('E', 3)
>>> print E().f(3)
('E', 3)

如果函數只需要類引用而不關心底層的數據,那么類方法就會很有用。一個使用classmethod的例子是創建類構造器。在Python2.3中dict.fromkeys()從關鍵字列表中創建一個新的字典。純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的純Python實現可能是這樣的:

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

終于結束了,這篇斷斷續續的翻譯了好幾天,幾次都想放棄了,但還是忍著翻譯了下來,算是收獲了許多。學習是沒有捷徑的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文翻譯自python descriptor guide 摘要 本文定義了描述符,總結了其中的協議,并且介紹如何調...
    大蟒傳奇閱讀 1,189評論 0 5
  • 簡介 Python 中,一切皆對象。 當我們訪問某個對象屬性時,在不同的情況下,Python 對屬性的訪問機制有所...
    Whyn閱讀 1,383評論 1 3
  • 概述 了解和熟悉python中的屬性訪問順序,有助于我們閱讀源碼,編寫高質量代碼,對python機制有個更深的理解...
    落羽歸塵閱讀 1,046評論 1 3
  • 今天,第一天在幼兒園你上班。說累有不覺得累。反而覺得老師們的語氣才讓我覺得很累。身為老師不強求每一位學生都...
    和你相遇17閱讀 323評論 0 0