什麼是描述符
python描述符是一個“繫結行為”的物件屬性,在描述符協議中,它可以通過方法重寫屬性的訪問。這些方法有 __get__(), __set__(), 和__delete__()。如果這些方法中的任何一個被定義在一個物件中,這個物件就是一個描述符。
描述符的呼叫
描述符作為屬性訪問是被自動呼叫的。
對於類屬性描述符物件,使用type.__getattribute__,它能把Class.x轉換成Class.__dict__[‘x’].__get__(None, Class)。
對於例項屬性描述符物件,使用object.__getattribute__,它能把object.x轉換為type(object).__dict__[‘x’].__get__(object, type(object))。
描述符講解
下面我們具體通過例項來詳細說明描述符的使用
先定義一個描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class RevealAccess(object): 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 |
上面實現了__get__和__set__。所以這是一個描述符物件。而且是一個資料描述符物件,非資料描述符物件只實現__get__方法。這2者之間有一些區別,下面會講到。
再定義一個呼叫描述符物件的類
1 2 3 4 5 |
class MyClass(object): x = RevealAccess(10, 'var "x"') y = 5 print MyClass.x |
訪問 MyClass.x 輸出
1 2 |
Retrieving var "x" 10 |
發現訪問x會去呼叫描述符的__get__方法。這就達到了描述符的作用,可以改變物件屬性的訪問,使用描述符的方法。因為如果解析器發現x是一個描述符的話,其實在內部是通過type.__getattribute__(),它能把MyClass.x轉換為MyClass.__dict__[“x”].__get__(None,MyClass)
來訪問。
1 2 3 4 |
print MyClass.__dict__["x"].__get__(None, MyClass) # 輸出 Retrieving var "x" 10 |
描述符的物件定義為類屬性,如果定義成物件屬性會有什麼不同嗎?下面我們試驗一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MyClass(object): x = RevealAccess(10, 'var "x"') def __init__(self): self.y = RevealAccess(11, 'var "y"') print type(MyClass.x) # 輸出 """ Retrieving var "x" <type 'int'>; """ test = MyClass() print test.y # 輸出 """ <__main__.RevealAccess object at 0x1004da410>; """ |
從上面的輸出,可以看到訪問類屬性的確呼叫了描述符的__get__方法,看到輸出的結果是int型別。而呼叫例項屬性並沒有訪問__get__方法。而是直接返回描述符的例項物件。之所以是這樣是因為當訪問一個例項描述符物件時,object.__getattribute__會將test.y轉換為type(test).__dict__[‘y’].__get__(test,type(test))
。
而MyClass類中沒有“y”屬性,所以無法訪呼叫到_get__方法,這裡會有一個判斷的過程。但這個例項物件仍然是一個描述符物件。所以最好定義描述符物件為類屬性。當然不是不可以定義為例項屬性,請看下面
當定義的類屬性描述符物件和例項屬性有相同的名字時
1 2 3 4 5 6 |
class MyClass(object): x = RevealAccess(10, 'var "x"') def __init__(self, x): self.x = x |
然後呼叫
1 2 3 4 5 6 7 8 |
test = MyClass(100) print test.x # 輸出 """ Updating var "x" Retrieving var "x" 100 """ |
可見依然呼叫了描述符的方法。按照常理,應該訪問 test.__dict__[‘x’],然後是type(test).__dict__[‘x’]。由於我們定義了例項屬性x。應該只輸出100。可這裡從輸出結果看的的確確的訪問了描述符的方法。那麼這是為什麼呢?
其實這裡主要是因為當python發現例項物件的字典中有與定義的描述符有相同名字的物件時,描述符優先,會覆蓋掉例項屬性。python會改寫預設的行為,去呼叫描述符的方法來代替。我們可以輸出類和例項物件的字典看看
1 2 3 4 5 6 7 8 9 10 11 12 |
test = MyClass(100) print test.__dict__ """ 輸出 {} """ print MyClass.__dict__ """ 輸出 {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'MyClass' objects>, 'x': <__main__.RevealAccess object at 0x1004da350>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x1004cce60>} """ |
從輸出中發現例項物件的字典中根本就沒有x物件,即使我們在類中定義了self.x。而類的字典中則有x描述符物件。這主要就是因為描述符優先。
上面我們定義的描述符有__get__和__set__2個方法,所以是一個資料描述符,非資料描述符只有一個__get__方法,通常用於方法。此外,非資料描述符的優先順序低於例項屬性。下面看一個例子,我們去掉__set__方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class RevealAccess(object): def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, objtype): print 'Retrieving', self.name # self.val="test" return self.val # def __set__(self, obj, val): # print 'Updating', self.name # self.val = val class MyClass(object): x = RevealAccess(10, 'var "x"') def __init__(self, x): self.x = x test = MyClass(100) print test.x “”“ 100 “”“ print test.__dict__ “”“ {'x': 100} “”“ print MyClass.__dict__ “”“ {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'MyClass' objects>, 'x': <;__main__.RevealAccess object at 0x1005da310>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x1005ccd70>} “”“ print MyClass.x """ Retrieving var "x" 10 """ |
從上面的輸出,可以看出非資料描述符不會覆蓋掉例項屬性。而且優先順序比例項屬性低。這也是和資料描述符的一個區別。
綜上所述,對於描述符的呼叫有以下幾點需要注意
- 描述符被 getattribute 方法呼叫
- 覆蓋__getattribute__會讓描述符無法自動呼叫
- 描述符只適用於新式類,即繼承object的類
- object . getattribute 和 type . getattribute 呼叫__get__方法不一樣
- 資料描述符優先於例項的字典,對於相同名字的會覆蓋
- 例項的字典優先於非資料描述符。但不會覆蓋。
- 對於資料描述符,python中property就是一個典型的應用。
對於非資料描述符,其主要用於方法。如靜態方法和類方法。看原始碼可以看到只實現了描述符協議中的__get__方法,而沒有實現__set__和__del__。
如下面這樣模擬靜態方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class StaticMethod(object): def __init__(self, f): self.f = f def __get__(self, obj, objtype=None): return self.f class MyClass(object): @StaticMethod def get_x(x): print("static") return x print MyClass.get_x(100) """ static 100 “”“ |
呼叫MyClass.get_x(100)相當於
1 |
MyClass.__dict__["get_x"].__get__(None, MyClass)(100) |
我們知道在python中,一切皆是物件。每一個定義的方法其實都是一個物件。在這裡我們可以通過dir()檢視每一個方法裡的屬性和方法。看下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Desc(object): def test1(self): print("test1") def test2(): print("test2") print(dir(test2)) """輸出太長不貼了,但從輸出中可以看到有__get__""" print(dir(Desc.test1)) """ ['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__func__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'im_class', 'im_func', 'im_self'] """ |
從dir的輸出,可以看到,每個方法物件都包含一個__get__方法。因此可以說每一個方法都是一個非資料描述符。通常我們通過點操作符呼叫方法時,內部都是呼叫這個__get__方法。
參考 https://docs.python.org/2.7/h…
以上就是本人對描述符的一些理解,有什麼不正確的地方還請不吝指出,謝謝!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式