1. 程式人生 > >Python語言學習講解十六:python之描述符__set__和__get__ 等解釋

Python語言學習講解十六:python之描述符__set__和__get__ 等解釋

注:每週一到週五都會進行相關Python基礎知識更新,歡迎大家提寶貴的意見

一、方法:

首先說下python中存在的幾種方法:物件方法、靜態方法、類方法等,歸屬權分別為obj、cls、cls

其實可以從他們的引數中就可以看的出來

物件方法引數中含有self,這個類似於C++中的this指標。

靜態方法使用@staticmethod來修飾,可以通過類或類的例項物件來呼叫而已.

  1. >>> class Parent:  
  2. class_attr = "class_attr"#######類似於C++中類成員、方法,所有物件是共享記憶體,物件和類都是可以進行呼叫的。
  3.     def
     __init__(self, value):  
  4. self.obj_attr = value
  5.         print self.obj_attr 
  6.     @staticmethod  #####靜態方法
  7.     def static_fn(str):         
  8.         print "static_fnis call...say %s" % str  
  9.     @classmethod   #####類方法
  10.     def class_fn(cls, str):   
  11.         print "class_fnis call...say %s"% str  
      
  12.     def obj_fn(self,str):  #####物件方法  
  13.         print"obj_fn is call...say %s"% str  
  14. >>> p=Parent('obj_attr')
  15. obj_attr
  16. >>> Parent.__dict__
    {'obj_fn': <function obj_fn at 0x02C44B70>, '__module__': '__main__', 'class_attr': 'class_attr', 'class_fn': <classmethod object at 0x02CB40F0>, 'static_fn': <staticmethod object at 0x02C36F50>, '__doc__': None, '__init__': <function __init__ at 0x02C44B30>}
    #####可以看到類中所包含的成員

    >>> p.__dict__
    {'obj_attr': 'obj_attr'} #####可以看到物件中所包含的成員,類成員物件並沒有在物件的dict中
  17. >>> id(p.class_attr)
    46362560 #####在看用物件呼叫類成員的地址輸出
  18. >>> id(Parent.class_attr)
    46362560 #####在看用類呼叫類成員的地址輸出,神奇的發現居然地址是一樣的。那就是驗證了上面所
  19. 說的物件其實都是使用類的物件的地址。所有物件在沒有修改自己類物件成員的時候都是指向類的初始地址。
  20. >>> p.class_attr = "change_attr" #####我們使用物件修改了類成員屬性,然後檢視下具體的情況和地址變化
    >>> p.class_attr'change_attr' #####物件屬性變化了,為何那???原因在於進行此操作的時候,會再物件中新增屬於物件
  21. 的成員change_attr,而且是用新的記憶體地址,於類的相互不影響>>> Parent.class_attr'class_attr' #####屬性無變化了>>> id(p.class_attr)46212960#####記憶體地址變化了>>> id(Parent.class_attr)46362560
  22. >>> p.__dict__{'class_attr': 'change_attr', 'obj_attr': 'obj_attr'}#####成員新增了:change_attr
  23. >>> Parent.class_attr = "p_change_attr"
    >>> p.class_attr'change_attr' #####可見當物件的成員發生了地址新的變化後,修改類成員引用的地址,不會影響物件的成員。如果
  24. 物件此時沒有自己的類成員空間,那麼類修改類成員空間,則物件的類成員也會發生地址空間變化。
屬性可以分為兩類,一類是Python自動產生的,如__class__,__hash__等,另一類是我們自定義的,如上面的hello,name。我們只關心自定義屬性。
類和例項物件(實際上,Python中一切都是物件,類是type的例項)都有__dict__屬性,裡面存放它們的自定義屬性(對與類,裡面還存放了別的東西)。

有些內建型別,如list和string,它們沒有__dict__屬性,所以沒辦法在它們上面附加自定義屬性。

對於class_attr 屬性Parent.class_attr Parent.__dict__['class_attr']是完全一樣的。因為查詢的地方就是__dict__字典中。預設__dict__省略了。沒啥神奇的~~~

Python程式碼  收藏程式碼
  1. >>> Parent.class_attr
  2. 'p_change_attr'  
  3. >>> Parent.__dict__['class_attr']  
  4. 'p_change_attr'  
  5. >>>  

但是對於obj_fn,情形就有些不同了

Python程式碼  收藏程式碼
  1. >>> Parent.obj_fn
  2. <unbound method Parent.obj_fn>  
  3. >>> Parent.__dict__['obj_fn']  
  4. <function obj_fn at 0x00C3AD70>  
  5. >>>   

可以發現,Parent.obj_fn是個unbound method。而Parent.__dict__['obj_fn']是個函式(不是方法)。

推斷:方法在類的__dict__中是以函式的形式存在的(方法的定義和函式的定義簡直一樣,除了要把第一個引數設為self)。那麼Parent.obj_fn針對的是物件的呼叫。

  1. >>> p.obj_fn
    <bound method Parent.obj_fn of <__main__.Parent instance at 0x02C2EE90>>

是一個bound method。

關於unbound和bound到還好理解,我們不妨先作如下設想:方法是要從例項呼叫的嘛(指例項方法,classmethod和staticmethod後面講),如果從類中訪問,如Parent.obj_fnobj_fn沒有和任何例項發生聯絡,也就是沒繫結(unbound)到任何例項上,所以是個unbound,對
  1. p.obj_fn
的訪問方式,obj_fn和p發生了聯絡,因此是bound。

一切的魔法都源自今天的主角:descriptor

查詢屬性時,如p.class_attr,如果Python發現這個屬性class_attr有個__get__方法,Python會呼叫class_attr的__get__方法,返回__get__方法的返回值,而不是返回class_attr(這一句話並不準確,我只是希望你能對descriptor有個初步的概念)。

Python中iterator(怎麼扯到Iterator了?)是實現了iterator協議的物件,也就是說它實現了下面兩個方法__iter__和next()。類似的,descriptor也是實現了某些特定方法的物件。descriptor的特定方法是__get__,__set__和__delete__,其中__set__和__delete__方法是可選的。iterator必須依附某個物件而存在(由物件的__iter__方法返回),descriptor也必須依附物件,作為物件的一個屬性,它而不能單獨存在。還有一點,descriptor必須存在於類的__dict__中,這句話的意思是隻有在類的__dict__中找到屬性,Python才會去看看它有沒有__get__等方法,對一個在例項的__dict__中找到的屬性,Python根本不理會它有沒有__get__等方法,直接返回屬性本身。descriptor到底是什麼呢:簡單的說,descriptor是物件的一個屬性,只不過它存在於類的__dict__中並且有特殊方法__get__(可能還有__set__和__delete)而具有一點特別的功能,為了方便指代這樣的屬性,我們給它起了個名字叫descriptor屬性。

Python程式碼  收藏程式碼
  1. class Descriptor(object):  
  2.     def __get__(self, obj, type=None):  
  3.             return 'get'self, obj, type  
  4.     def __set__(self, obj, val):  
  5.         print 'set'self, obj, val  
  6.     def __delete__(self, obj):  
  7.         print 'delete'self, obj  

這裡__set__和__delete__其實可以不出現,不過為了後面的說明,暫時把它們全寫上。

下面解釋一下三個方法的引數:

self當然不用說,指的是當前Descriptor的例項。obj值擁有屬性的物件。這應該不難理解,前面已經說了,descriptor是物件的稍微有點特殊的屬性,這裡的obj就是擁有它的物件,要注意的是,如果是直接用類訪問descriptor(別嫌囉嗦,descriptor是個屬性,直接用類訪問descriptor就是直接用類訪問類的屬性),obj的值是None。type是obj的型別,剛才說過,如果直接通過類訪問descriptor,obj是None,此時type就是類本身。

三個方法的意義,假設T是一個類,t是它的一個例項,d是T的一個descriptor屬性(牛什麼啊,不就是有個__get__方法嗎!),value是一個有效值:

讀取屬性時,如T.d,返回的是d.__get__(None, T)的結果,t.d返回的是d.__get__(t, T)的結果。

設定屬性時,t.d = value,實際上呼叫d.__set__(t, value),T.d = value,這是真正的賦值,T.d的值從此變成value。刪除屬性和設定屬性類似。

是時候坦白真正詳細的屬性查詢策略 了,對於obj.attr(注意:obj可以是一個類):

1.如果attr是一個Python自動產生的屬性,找到!(優先順序非常高!)

2.查詢obj.__class__.__dict__,如果attr存在並且是data descriptor,返回data descriptor的__get__方法的結果,如果沒有繼續在obj.__class__的父類以及祖先類中尋找data descriptor

3.在obj.__dict__中查詢,這一步分兩種情況,第一種情況是obj是一個普通例項,找到就直接返回,找不到進行下一步。第二種情況是obj是一個類,依次在obj和它的父類、祖先類的__dict__中查詢,如果找到一個descriptor就返回descriptor的__get__方法的結果,否則直接返回attr。如果沒有找到,進行下一步。

4.在obj.__class__.__dict__中查詢,如果找到了一個descriptor(插一句:這裡的descriptor一定是non-data descriptor,如果它是data descriptor,第二步就找到它了)descriptor的__get__方法的結果。如果找到一個普通屬性,直接返回屬性值。如果沒找到,進行下一步。

5.很不幸,Python終於受不了。在這一步,它raise AttributeError


首先,Python判斷name屬性是否是個自動產生的屬性,如果是自動產生的屬性,就按特別的方法找到這個屬性,當然,這裡的name不是自動產生的屬性,而是我們自己定義的,Python於是到t的__dict__中尋找。還是沒找到。

接著,Python找到了t所屬的類T,搜尋T.__dict__,期望找到name,很幸運,直接找到了,於是返回name的值:字串‘name’。如果在T.__dict__中還沒有找到,Python會接著到T的父類(如果T有父類的話)的__dict__中繼續查詢。

總結:

只要記住類中的成員變數是物件的時候,物件又是個修飾器的時候。那麼對成員變數的賦值實際是呼叫他的get和set方法就O了。引數很簡單。