python 描述符

翻譯自https://docs.python.org/2/howto/descriptor.htmlhtml

1.1. 摘要

定義描述器, 總結描述器協議,並展現描述器是怎麼被調用的。展現一個自定義的描述器和包括函數,屬性(property), 靜態方法(static method), 類方法在內的幾個Python內置描述器。經過給出一個純Python的實現和示例應用來展現每一個描述器是怎麼工做的。python

學習描述器不只讓你接觸到更多的工具,還可讓你更深刻地瞭解Python,讓你體會到Python設計的優雅之處。程序員

1.2. 定義和介紹

通常來講,一個描述器是一個有「綁定行爲」的對象屬性(object attribute),它的訪問控制被描述器協議方法重寫。這些方法是 __get__()__set__(), 和 __delete__() 。有這些方法的對象叫作描述器編程

默認對屬性的訪問控制是從對象的字典裏面(__dict__)中獲取(get), 設置(set)和刪除(delete)它。舉例來講, a.x 的查找順序是, a.__dict__['x'] , 而後 type(a).__dict__['x'] , 而後找 type(a) 的父類(按照mro裏的順序,不包括元類(metaclass)).若是查找到的值是一個描述器, Python就會調用描述器的方法來重寫默認的控制行爲。這個重寫發生在這個查找環節的哪裏取決於定義了哪一個描述器方法。注意, 只有在新式類中時描述器纔會起做用。(新式類是繼承自 type 或者 object的類)api

描述器是強大的,應用普遍的。描述器正是屬性, 實例方法, 靜態方法, 類方法和 super 的背後的實現機制。描述器在Python自身中普遍使用,以實現Python 2.2中引入的新式類。描述器簡化了底層的C代碼,併爲Python的平常編程提供了一套靈活的新工具。ide

1.3. 描述器協議

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

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

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

這是全部描述器方法。一個對象具備其中任一個方法就會成爲描述器,從而在被看成對象屬性時重寫默認的查找行爲。oop

若是一個對象同時定義了 __get__() 和 __set__(),它叫作數據描述器(data descriptor)。僅定義了 __get__() 的描述器叫非數據描述器(經常使用於方法,固然其餘用途也是能夠的)

數據描述器和非數據描述器的區別在於:相對於實例的字典的優先級。若是實例字典中有與描述器同名的屬性,若是描述器是數據描述器,優先使用數據描述器,若是是非數據描述器,優先使用字典中的屬性。

下面一個描述器

class descr(object):
    def __init__(self, name='jack'):
        self._name = name
    def __get__(self, instance, owner):
        print('__get__...attr:%s instance:%s owner:%s' % (self._name, instance, owner))
        return self._name
    def __set__(self, instance, value):
        print('__set__...attr:%s instance:%s value:%s' % (self._name, instance, value))
        self._name = value


class A(object):
    a = descr()
    def f(self):
        print('Im A.f')

>>> a = A()
>>> a.a
__get__...attr:jack instance:<__main__.A object at 0x109224950> owner:<class '__main__.A'>
'jack'
>>> a.a='my jack'
__set__...attr:jack instance:<__main__.A object at 0x109224950> value:my jack
>>> a.a
__get__...attr:my jack instance:<__main__.A object at 0x109224950> owner:<class '__main__.A'>
'my jack'

假如f裏也有同名屬性a會是什麼狀況

class descr(object):
    def __init__(self, name='jack'):
        self._name = name
    def __get__(self, instance, owner):
        print('__get__...attr:%s instance:%s owner:%s' % (self._name, instance, owner))
        return self._name
    def __set__(self, instance, value):
        print('__set__...attr:%s instance:%s value:%s' % (self._name, instance, value))
        self._name = value


class A(object):
    a = descr()
    def f(self):
        self.a = 'siri'
        print('Im A.f')

>>> a = A()
>>> a.f()
__set__...attr:jack instance:<__main__.A object at 0x1079a2490> value:siri
Im A.f
>>> a.a
__get__...attr:siri instance:<__main__.A object at 0x1079a2490> owner:<class '__main__.A'>
siri

會發現雖然實例屬性裏雖然沒定義描述器,但仍是走了咱們定義的描述器,這就是上面這句「若是實例字典中有與描述器同名的屬性,若是描述器是數據描述器,優先使用數據描述器」

假如非數據描述器呢,只保留__get__

class descr(object):
    def __init__(self, name='jack'):
        self._name = name
    def __get__(self, instance, owner):
        print('__get__...attr:%s instance:%s owner:%s' % (self._name, instance, owner))
        return self._name
    # def __set__(self, instance, value):
    #     print('__set__...attr:%s instance:%s value:%s' % (self._name, instance, value))
    #     self._name = value


class A(object):
    a = descr()
    def f(self):
        self.a = 'siri'
        print('Im A.f')

>>> a = A()
>>> a.f()
Im A.f
>>> a.a
siri

可見,以上並無走咱們定義的描述器,即上面這句「若是是非數據描述器,優先使用字典中的屬性」

假如描述符對象定義爲對象屬性會怎麼樣?

class A(object):
    a = descr()
    def f(self):
        self.b = descr('siri')
        print('Im A.f')

>>> a = A()
>>> a.f()
Im A.f
>>> a.a
__get__...attr:jack instance:<__main__.A object at 0x103842490> owner:<class '__main__.A'>
jack
>>> a.b
<__main__.descr object at 0x1038424d0>

可見返回了描述器對象的實例,這是因爲object.__getattribute__會將a.b轉換爲type(a).__dict__[‘b’].__get__(a,type(a)),而A類中沒有「b」屬性,因此沒法訪調用到_get__方法,這裏會有一個判斷的過程。

假如類屬性不是描述器,對象屬性是呢,也同樣

class A(object):
    a = ''
    def f(self):
        self.a = descr('siri')
        print('Im A.f')

>>> a = A()
>>> a.f()
Im A.f
>>> a.a
<__main__.descr object at 0x10b63c450>

因此描述器對象通常都用作類屬性描述

要想製做一個只讀的數據描述器,須要同時定義 __set__ 和 __get__,並在 __set__ 中引起一個 AttributeError 異常。定義一個引起異常的 __set__方法就足夠讓一個描述器成爲數據描述器。

1.4. 描述器的調用

描述器能夠直接這麼調用: d.__get__(obj)

然而更常見的狀況是描述器在屬性訪問時被自動調用。舉例來講, obj.d 會在 obj的字典中找 d ,若是 d 定義了 __get__ 方法,那麼 d.__get__(obj) 會依據下面的優先規則被調用。

調用的細節取決於 obj 是一個類仍是一個實例。另外,描述器只對於新式對象和新式類才起做用。繼承於 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) 。

用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

其中重要的幾點:

  • 描述器的調用是由於 __getattribute__()
  • 重寫 __getattribute__() 方法會阻止正常的描述器調用
  • __getattribute__() 只對新式類的實例可用
  • object.__getattribute__() 和 type.__getattribute__() 對 __get__() 的調用不同
  • 數據描述器老是比實例字典優先。
  • 非數據描述器可能被實例字典重寫。(非數據描述器不如實例字典優先)

super() 返回的對象一樣有一個定製的 __getattribute__() 方法用來調用描述器。調用 super(B, obj).m() 時會先在 obj.__class__.__mro__ 中查找與B緊鄰的基類A,而後返回 A.__dict__['m'].__get__(obj, A) 。若是不是描述器,原樣返回 m 。若是實例字典中找不到 m ,會回溯繼續調用 object.__getattribute__() 查找。(譯者注:即在 __mro__ 中的下一個基類中查找)

注意:在Python 2.2中,若是 m 是一個描述器, super(B, obj).m() 只會調用方法 __get__() 。在Python 2.3中,非數據描述器(除非是個舊式類)也會被調用。 super_getattro() 的實現細節在: Objects/typeobject.c ,[del] 一個等價的Python實如今 Guido’s Tutorial [/del] (譯者注:原文此句已刪除,保留供你們參考)。

以上展現了描述器的機理是在 objecttype, 和 super 的 __getattribute__() 方法中實現的。由 object 派生出的類自動的繼承這個機理,或者它們有個有相似機理的元類。一樣,能夠重寫類的 __getattribute__() 方法來關閉這個類的描述器行爲。

1.5. 描述器例子

下面的代碼中定義了一個數據描述器,每次 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

這個協議很是簡單,而且提供了使人激動的可能。一些用途實在是太廣泛以至於它們被打包成獨立的函數。像屬性(property), 方法(bound和unbound method), 靜態方法和類方法都是基於描述器協議的。

1.6. 屬性(properties)

調用 property() 是創建數據描述器的一種簡潔方式,從而能夠在訪問屬性時觸發相應的方法調用。這個函數的原型:

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

下面展現了一個典型應用:定義一個託管屬性(Managed 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
        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() 可以提供很大幫助。

例如,一個電子表格類提供了訪問單元格的方式: Cell('b10').value 。 以後,對這個程序的改善要求在每次訪問單元格時從新計算單元格的值。然而,程序員並不想影響那些客戶端中直接訪問屬性的代碼。那麼解決方案是將屬性訪問包裝在一個屬性數據描述器中:

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

這裏注意property修飾時,屬性名稱和要修飾的方法名稱不能重複,不然會循環調用

class Person(object):
    def __init__(self, name):
        self.name = name
    @property
    def name(self):
        return self.name
    @name.setter
    def name(self, name):
        self.name = name

p = Person('kk')

# 會報錯循環遞歸過深
# File "/Users/Teron/Code/Git/Personal/Test/c.py", line 10, in name
#    self.name = name
# RuntimeError: maximum recursion depth exceeded
# 改成不同便可

class Person(object):
    def __init__(self, name):
        self._name = name
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, name):
        self._name = name

p = Person('kas')

 

1.7. 函數和方法

Python的面向對象特徵是創建在基於函數的環境之上的。非數據描述器把二者無縫地鏈接起來。

類的字典把方法當作函數存儲。在定義類的時候,方法一般用關鍵字 def 和 lambda 來聲明。這和建立函數是同樣的。惟一的不一樣之處是類方法的第一個參數用來表示對象實例。Python約定,這個參數一般是 self, 但也能夠叫 this 或者其它任何名字。

爲了支持方法調用,函數包含一個 __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'] # 存儲成一個function
<function f at 0x00C45070>
>>> D.f             # 從類來訪問,返回unbound method
<unbound method D.f>
>>> d.f             # 從實例來訪問,返回bound method
<bound method D.f of <__main__.D object at 0x00B18C90>>

從輸出來看,綁定方法和非綁定方法是兩個不一樣的類型。它們是在文件 Objects/classobject.c(http://svn.python.org/view/python/trunk/Objects/classobject.c?view=markup) 中用C實現的, PyMethod_Type 是一個對象,可是根據 im_self是不是 NULL (在C中等價於 None ) 而表現不一樣。

一樣,一個方法的表現依賴於 im_self 。若是設置了(意味着bound), 原來的函數(保存在 im_func 中)被調用,而且第一個參數設置成實例。若是unbound, 全部參數原封不動地傳給原來的函數。函數 instancemethod_call() 的實際C語言實現只是比這個稍微複雜些(有一些類型檢查)。

1.8. 靜態方法和類方法

非數據描述器爲將函數綁定成方法這種常見模式提供了一個簡單的實現機制。

簡而言之,函數有個方法 __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.

既然staticmethod將函數原封不動的返回,那下面的代碼看上去就很正常了:

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

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

利用非數據描述器, staticmethod() 的純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的引用做爲第一個參數。無論調用者是對象仍是類,這個格式是同樣的:

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

當一個函數不須要相關的數據作參數而只須要一個類的引用的時候,這個特徵就顯得頗有用了。類方法的一個用途是用來建立不一樣的類構造器。在Python 2.3中, dict.fromkeys() 能夠依據一個key列表來建立一個新的字典。等價的Python實現就是:

class Dict:
    . . .
    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
相關文章
相關標籤/搜索