python中基於descriptor的一些概念(上)


python中基於descriptor的一些概念(上)

1. 前言

python在2.2版本中引入了descriptor功能,也正是基於這個功能實現了新式類(new-styel class)的對象模型,
同時解決了之前版本中經典類(classic class)系統中出現的多重繼承中的MRO(Method Resolution Order)的問題,
同時引入了一些新的概念,比如classmethod, staticmethod, super,Property等,這些新功能都是基於descriptor
而實現的。總而言之,通過學習descriptor可以更多地了解python的運行機制。我在這也大概寫一個匯總,
寫一下對這些東西的理解。歡迎大家討論。
 
在這里,為文章中使用的詞匯做一下說明:
函數:指的是第一個參數不是self的函數,不在類中定義的函數
方法:指是的第一個參數是self的函數
實例:類的對象,instance
對象模型:就是實現對象行為的整個框架,這里分為經典和新的兩種
 
使用的python版本為python 2.7.2

2. 新式類與經典類

首先來了解一下新式類與經典類的區別,從創建方法上可以明顯的看出:
#新式類
class C(object) :
     pass
#經典類
class B :
     pass
簡單的說,新式類是在創建的時候繼承內置object對象(或者是從內置類型,如list,dict等),而經典類是直
接聲明的。使用dir()方法也可以看出新式類中定義很多新的屬性和方法,而經典類好像就2個:
 
這些新的屬性和方法都是從object對象中繼承過來的。

2.1 內置的object對象

內置的object對象是所有內置,object對象定義了一系列特殊的方法實現所有對象的默認行為。
1. __new__,__init__方法
這兩個方法是用來創建object的子類對象,靜態方法__new__()用來創建類的實例,然后再調用
__init__()來初始化實例。
 
2. __delattr__, __getattribute__, __setattr__方法
對象使用這些方法來處理屬性的訪問
 
3. __hash__, __repr__, __str__方法
print(someobj)會調用someobj.__str__(), 如果__str__沒有定義,則會調用someobj.__repr__(),
 
__str__()和__repr__()的區別:
  • 默認的實現是沒有任何作用的
  • __repr__的目標是對象信息唯一性
  • __str__的目標是對象信息的可讀性
  • 容器對象的__str__一般使用的是對象元素的__repr__
  • 如果重新定義了__repr__,而沒有定義__str__,則默認調用__str__時,調用的是__repr__
  • 也就是說好的編程習慣是每一個類都需要重寫一個__repr__方法,用於提供對象的可讀信息,
  • 而重寫__str__方法是可選的。實現__str__方法,一般是需要更加好看的打印效果,比如你要制作
  • 一個報表的時候等。
可以允許object的子類重載這些方法,或者添加新的方法。

2.2 類的方法

新的對象模型中提供了兩種類級別的方法,靜態方法和類方法,在諸多新式類的特性中,也只有類方法這個
特性, 和經典對象模型實現的功能一樣。
2.2.1 靜態方法
靜態方法可以被類或者實例調用,它沒有常規方法的行為(比如綁定,非綁定,默認的第一個self參數),當有一
堆函數僅僅是為了一個類寫的時候,采用靜態方法聲明在類的內部,可以提供行為上的一致性。
 
創建靜態方法的代碼如下, 使用裝飾符@staticmethod進行創建
 可以看出,不管是 類調用,還是實例調用靜態方法,都是指向同一個函數對象
2.2.2 類方法
也是可以通過類和它的實例進行調用,不過它是有默認第一個參數,叫做是類對象,一般被
命名為cls,當然你也可以命名為其它名字,這樣就你可以調用類對象的一些操作,
代碼如下, 使用裝飾符@classmethod創建

2.3 新式類(new-style class)

新式類除了擁有經典類的全部特性之外,還有一些新的特性。比如__init__發生了變化,
新增了靜態方法__new__
2.3.1 __init__方法
據說在python2.4版本以前,使用新式類時,如果類的初始化方法沒有定義,調用的
時候寫了多余的參數,編譯器不會報錯。我現在的python 2.7會報錯,還是覺得會報錯
比較好點,下面給出新式類和經典類運行這個例子的情況:
 
2.3.2 __new__靜態方法
新式類都有一個__new__的靜態方法,它的原型是 object.__new__(cls[, ...])
cls是一個類對象,當你調用C(*args, **kargs)來創建一個類C的實例時,python的內部調用是
C.__new__(C, *args, **kargs),然后返回值是類C的實例c,在確認
c是C的實例后,python再調用C.__init__(c, *args, **kargs)來初始化實例c。
所以調用一個實例c = C(2),實際執行的代碼為:
c = C.__new__(C, 2)
if isinstance(c, C) :
    C. __init__(c, 23) #__init__第一個參數要為實例對象
object.__new__()創建的是一個新的,沒有經過初始化的實例。當你重寫__new__方法時,可以不
用使用裝飾符@staticmethod指明它是靜態函數,解釋器會自動判斷這個方法為靜態方法。如果
需要重新綁定C.__new__方法時,只要在類外面執行C.__new__ = staticmethod(yourfunc)就可以了。
 
可以使用__new__來實現Singleton單例模式:
class Singleton(object) :
    _singletons = {}
     def __new__(cls) :
         if not cls._singletons.has_key(cls) :             #若還沒有任何實例
            cls._singletons[cls] = object.__new__(cls)   #生成一個實例
         return cls._singletons[cls]                              #返回這個實例
運行結果如下:
 
使用id()操作,可以看到兩個實例指向同一個內存地址。Singleton的所有子類也有這一
特性,只有一個實例對象,如果它的子類定義了__init__()方法,那么必須保證它的
__init__方法能夠安全的同一個實例進行多次調用。
 

2.4. 新式類的實例

除了新式類本身具有新的特性外,新式類的實例也具有新的特性。比如它擁有Property功能,該
功能會對屬性的訪問方式產生影響;還有__slots__新屬性,該屬性會對生成子類實例產生影響;還
添加了一個新的方法__getattribute__,比原有的__getattr__更加通用。
2.4.1 Property
在介紹完descriptor會回過頭來講這個。
2.4.2 __slots__屬性
 
通常每一個實例x都會有一個__dict__屬性,用來記錄實例中所有的屬性和方法,也是通過這個字典,
可以讓實例綁定任意的屬性。而__slots__屬性作用就是,當類C有比較少的變量,而且擁有__slots__屬性時,
類C的實例 就沒有__dict__屬性,而是把變量的值存在一個固定的地方。如果試圖訪問一個__slots__中沒有
的屬性,實例就會報錯。這樣操作有什么好處呢?__slots__屬性雖然令實例失去了綁定任意屬性的便利,
但是因為每一個實例沒有__dict__屬性,卻能有效節省每一個實例的內存消耗,有利於生成小而精
干的實例。
 
為什么需要這樣的設計呢?
在一個實際的企業級應用中,當一個類生成上百萬個實例時,即使一個實例節省幾十個字節都可以節省
一大筆內存,這種情況就值得使用__slots__屬性。
 
怎么去定義__slots__屬性?
__slots__是一個類變量,__slots__屬性可以賦值一個包含類屬性名的字符串元組,或者是可迭代變量,或者
是一個字符串,只要在類定義的時候,使用__slots=aTuple來定義該屬性就可以了:
可以看出實例a中沒有__dict__字典,而且不能隨意添加新的屬性,不定義__slots__是可以隨意添加的:
使用時__slots__時需要注意的幾點:
1.  當一個類的父類沒有定義__slots__屬性,父類中的__dict__屬性總是可以訪問到的,所以只在子
類中定義__slots__屬性,而不在父類中定義是沒有意義的。
 
2. 如果定義了__slots屬性,還是想在之后添加新的變量,就需要把'__dict__'字符串添加到__slots__的
元組里。
 
3. 定義了__slots__屬性,還會消失的一個屬性是__weakref__,這樣就不支持實例的weak reference,
如果還是想用這個功能,同樣,可以把'__weakref__'字符串添加到元組里。
 
4. __slots__功能是通過descriptor實現的,會為每一個變量創建一個descriptor。
 
5. __slots__的功能只影響定義它的類,因此,子類需要重新定義__slots__才能有它的功能。
2.4.3 __getattribute__方法
對新式類的實例來說,所有屬性和方法的訪問操作都是通過__getattribute__完成,
這是由object基類實現的。如果有特殊的要求,可以重載__getattribute__方法,下面
實現一個不能使用append方法的list:
  
2.4.4 實例的方法
經典的與新的對象模型都允許一個實例擁有私有的屬性和方法(可以通過綁定和重綁定)。實例
的私有屬性會覆蓋掉類中定義的同名屬性,舉例說明:
 
然而在python中,隱式調用實例的私有特殊方法時,新的對象模型和經典對象模型表現上不太一樣。
在經典對象模型中,無論是顯示調用還是隱式調用特殊方法,都會調用實例中后綁定的特殊方法。
而在新的對象模型中,除非顯式地調用實例的特殊方法,否則python總是會去調用類中定義的特殊方法,
如果沒有定義的話,就報錯。代碼如下:
經典類:
 
新式類:
  
調用a[1],將產生一個隱式的__getitem__方法的調用,在新式類中,因為類中沒有定義這個方法,也不是
object基類有的方法,所以報錯。需要顯示地調用才可以運行。

2.5 新的對象模型

在新的對象模型中,繼承方式和經典對象模型大體相同,一個關鍵的區別就是新式類能夠從python的內置
類型中繼承,而經典類不行。
2.5.1 多繼承
新式類同樣支持多繼承,但是如果新式類想要從多個內置類型中繼承生成一個新類的話,則這些內置類必須是
經過精心設計,能夠互相兼容的。顯然,python也沒會讓你隨意的從多個內置類中進行多繼承,想創建一個超級類
不是那么容易的。。。通常情況下,至多可以繼承一個內置類,比如list, set, dict等。
2.5.2 MRO(Method Resolution Order, 方法解析順序)
對於下圖的多繼承關系:
b = A(),當調用b.a的時候會發生什么事呢?
在經典對象模型中,方法和屬性的查找鏈是按照從左到右,深度優先的方式進行查找。所以當A的實例b
要使用屬性a時,它的查找順序為:A->B->D->C->A,這樣做就會忽略類C的定義a,而先找到的基類D的
屬性a,這是一個bug,這個問題在新式類中得到修復,新的對象模型采用的是從左到右,廣度優先的方式
進行查找,所以查找順序為A->B->C->D,可以正確的返回類C的屬性a。
經典類:
新式類:
  
這個順序的實現是通過新式類中特殊的只讀屬性__mro__,類型是一個元組,保存着解析順序信息。只能通過
類來使用,不能通過實例調用。
順序還和繼承時,括號中寫的父類順序有關:
2.5.3 協作式調用父類方法
當子類重寫了父類的一個方法時,通常會調用父類的同名方法做一些工作,這是比較常見的使用
方式--使用非綁定語法來調用父類的方法。不過在多繼承中,這種方法有缺餡:
可以看到,基類A的方法重復運行了兩次。怎樣才能確保父類中的方法只被順序的調用一次呢?
在新的對象系統中,有一種特殊的方法super(aclass, obj),可以返回obj實例的一個特殊類型
superobject(超對象, 不是簡單的父類的對象),當我們使用超對象調用父類的方法時,就
能保證只被運行一次:
可以看到,D的父類中所有的foo方法都得到執行,並且基類A的foo方法只執行了一次。如果養成了
使用super去調用父類方法的習慣,那么你的類就可以適應無論多么復雜的繼承調用結構。super()
可以看成是更加安全調用父類方法的一種新方式。
 
 
 
 






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM