- python中基於descriptor的一些概念(上)
- 1. 前言
- 2. 新式類與經典類
- 2.1 內建的object對象
- 2.2 類的方法
- 2.3 新式類(new-style class)
- 2.3.1 __init__方法
- 2.3.2 __new__靜態方法
- 2.4. 新式類的執行個體
- 2.4.1 Property
- 2.4.2 __slots__屬性
- 2.4.3 __getattribute__方法
- 2.4.4 執行個體的方法
- 2.5 新的物件模型
- 2.5.1 多繼承
- 2.5.2 MRO(Method Resolution Order, 方法解析順序)
- 2.5.3 協作式調用父類方法
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.22. 新式類與經典類首先來瞭解一下新式類與經典類的區別,從建立方法上可以明顯的看出:#新式類
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()可以看成是更加安全調用父類方法的一種新方式。
通過 為知筆記 發布