2015/9/22 Python基礎(18):組合、派生和繼承


一個類被定義后,目標就是把它當成一個模塊來使用,並把這些對象嵌入到你的代碼中去,同其他數據類型及邏輯執行流混合使用。
有兩種方法可以在你的代碼中利用類。
第一種是組合,就是讓不同的類混合並加入到其他類中,來增強功能和代碼重用性。你可以在一個大點的類中創建你自己的類的實例,實現一些其他屬性和方法來增強原來的類對象。
另一種是派生,通過子類從基類繼承核心屬性,不斷地派生擴展功能實現。

組合
舉例來說,我們想對之前做過的地址本類作加強性設計。如果在設計的過程中,為names、addresses等創建了單獨的類,那么最后我們可能想把這些工作集成到AddrBookEntry類中去,而不是重新設計每一個需要的類。
下面是一個例子

class NewAddrBookEntry(object):
  'new address book entry class'
  def __init__ (self, nm, ph): 
    self.name = Name(nm) 
    self.phone = Phone(ph)
    print 'Created instance for:', self.name 

 

NewAddrBookEntry 類由它自身和其他類組合而成。這就在一個類和其他組成類之間定義了一個“has-a”關系。比如說,我們的NewAddrBookEntry類“有一個”Name類實例和一個Phone實例。
創建復合對象就可以實現這些附加的功能,並且很有意義,因為這些類都不相同。每一個類管理他們自己的名字空間和行為。不過當對象之間有更接近的關系是,派生的概念可能更有意義。

這里沒有寫清楚如何使用組合這個概念,我就上面的部分寫一個例子:

>>> class Name(object):
  def __init__(self, nm):
    self.name = nm

>>> class Phone(object):
  def __init__(self, ph):
    self.phone = ph
>>> class NewAddrBookEntry(object):
  def __init__(self, nm, ph):
    self.name = Name(nm)
    self.phone = Phone(ph)
    print 'Created instance for:', self.name
    print 'Created instance for:', self.name.name
>>> foo = NewAddrBookEntry('Paul', 123456)
Created instance for: <__main__.Name object at 0x02B75FD0>
Created instance for: Paul

 


子類和派生
當類和類之間有顯著的不同,並且(較小的類)是較大的類所需要的組件時,組合表現的不錯,但如果要設計“相同的類但由一些不同的功能”時,派生就是更合理的選擇了。
OOP的更強大的功能之一是能夠使用一個已經定義好的類,擴展它或者對其進行修改,而不會影響系統中使用現存類的其他代碼片段。
OOD允許類特征在子孫類或子類中進行繼承。這些子類從基類(或稱祖先類、超類)繼承他們的核心屬性。而且,這些派生可能會擴展到多代。在一個層次的派生關系中的相關類是父類和子類的關系,同一個父類派生出來的這些類是同胞關系。父類和所有高層類都被認為是祖先。

創建子類
創建子類的語法看起來與普通類沒有區別,一個類名,后跟一個或多個需要從其中派生的父類:

class SubClassName (ParentClass1[, ParentClass2...):
  'optional class documentation string'
  class_suite

 

如果你的類沒有從任何祖先類派生,可以使用object作為父類名字。這也就是新式類的創建。
下面是子類派生的一個例子:

>>> class Parent(object):
  def parentMethod(self):
    print 'calling parent method'

>>> class Child(Parent):
  def childMethod(self):
    print 'calling child method'

>>> p = Parent()
>>> p.parentMethod()
calling parent method
>>> c = Child()
>>> c.childMethod()
calling child method
>>> c.parentMethod()
calling parent method

 

繼承
繼承描述了基類的屬性如何“遺傳”給派生類。一個子類可以繼承它的積累的任何屬性,不管是數據屬性還是方法。

舉例如下,P是一個沒有屬性的簡單類,C從P繼承而來,也沒有屬性:

>>> class P(object):
  pass

>>> class C(P):
  pass

>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> P.add = 123
>>> c.add
123

 

我們給父類添加數據屬性時,子類也繼承到了這個屬性。

假如P添加一些屬性:

>>> class P:
  'P class'
  def __init__(self):
    print 'created an instance of',\
      self.__class__.__name__

>>> class C(P):
  pass

>>> p = P()
created an instance of P
>>> a = P()
created an instance of P
>>> p.__class__
<class __main__.P at 0x02A10490>
>>> P.__bases__
()
>>> P.__doc__
'P class'
>>> c = C()
created an instance of C
>>> c.__class__
<class __main__.C at 0x02BA8880>
>>> C.__bases__
(<class __main__.P at 0x02A10490>,)
>>> C.__doc__
>>> 

 

C沒有聲明__init__()方法,然而在類C的實例c被創建時,還是會有輸出星系。原因在於C集成了P的__init__()。__bases__元組列出父類P。需要的是文檔字符串對類,函數/方法,還有模塊來說都是唯一的,所以特殊屬性__doc__不會從基類中繼承過來。

__bases__類屬性
__bases__類屬性是一個包含其父類的集合的元組。這里的父類是相對所有基類而言的。那些沒有父類的類,__bases__屬性為空。

>>> class A(object):
pass

>>> class B(A):
pass

>>> class C(B):
pass

>>> class D(B, A):
pass

>>> A.__bases__
(<type 'object'>,)
>>> C.__bases__
(<class '__main__.B'>,)
>>> D.__bases__
(<class '__main__.B'>, <class '__main__.A'>)

 

對於D,繼承方向,我們將在之后詳細講解。

通過繼承覆蓋方法
我們在P中再寫一個函數,然后在其子類中對它進行覆蓋。

>>> class P(object):
  def foo(self):
    print 'call P-foo()'

>>> p = P()
>>> p.foo()
call P-foo()
>>> class C(P):
  def foo(self):
    print 'call C-foo()'

>>> c = C()
>>> c.foo()
call C-foo()

 

盡管C繼承了P的方法,但因為C定義了自己的foo()方法,所以P中的foo()方法被覆蓋。
那我們怎么去調用那個別覆蓋的基類的方法呢?
有這樣幾種方法:

>>> P.foo(c)
call P-foo()

這里我們沒有用P的實例調用方法,而是用了P的子類C的實例c來調用。一般我們不會用這種方法調用。一般如下:

>>> class C(P):
  def foo(self):
    P.foo(self)
    print 'call C-foo()'
>>> c = C()
>>> c.foo()
call P-foo()
call C-foo()

這種方法需要我們知道C的父類,還有一個更好的方法是用super()內建函數

>>> class C(P):
  def foo(self):
    super(C, self).foo()
    print 'call C-foo()'

>>> c = C()
>>> c.foo()

 

核心筆記:重寫__init__不會自動調用基類的__init__
類似於上面的覆蓋非特殊方法,當從一個帶構造器__init__()的類派生,如果你不去覆蓋__init__(),它將會被繼承並自動調用。但如果你在子類中覆蓋了__init__(),子類被實例化時,基類的__init__()就不會被自動調用。如果還想調用基類的__init__(),需要向上邊我們說的那樣,明確指出,使用一個子類的實例去調用基類(未綁定)方法:

class C(P):
  def __init__(self):
    P.__init__(self)
    C__init__suite

這是一種很普遍的調用做法,這個規則的意義是,你希望被繼承的類的對象在子類構造器運行前能夠很好地被初始化或做好准備工作,因為它可能需要或設置繼承屬性。
當然我們也可以用super()內建函數替代P的使用,這樣可以不提供基類的名字。

從標准類型派生
經典類中,一個最大的問題是,不能對標准類型進行子類化。后來隨着類型和類的統一和新式類的引入,這一點已經被修正。

1.不可變類型的例子
金融應用中處理一個浮點數的子類,每次你得到一個貨幣值,你都需要四舍五入,變為帶兩位小數位的數值。你的類可以這樣寫:

class RoundFloat(float):
  def __new__(cls, val):
    return float.__new__(cls, round(val, 2))

我們了__new__()特殊方法來定制我們的對象,使之和標准Python浮點數有一些區別:我們使用了round()內建函數對元浮點數進行舍入操作,然后實例化我們的float, RoundFloat。我們是通過調用父類的構造器來創建真實的對象的,float.__new__()。注意所有的__new__()方法都是類方法,我們要顯式傳入類作為第一個參數。
以下是一些樣例輸出

>>> RoundFloat(1.5987)
1.6
>>> RoundFloat(0.4567)
0.46
>>> RoundFloat(-1.2334)
-1.23

 

2.可變類型的例子
子類化一個可變類型於此相似,你可能不需要使用__new__()(甚至不用__init__()),因為通常設置不多。一般情況下,所繼承到的類型默認行為就是你想要的。下例是一個字典類型:

class SortedKeyDict(dict):
  def keys(self):
    return sorted(super(SortedKeyDict, self).keys())

下面是使用新字典的例子:

>>> d = SortedKeyDict({'Anna':68, 'John':86,'Frank':78,'Cindy':88})
>>> print 'By iterator:',[key for key in d]
By iterator: ['Frank', 'John', 'Anna', 'Cindy']
>>> print 'By keys():',d.keys()
By keys(): ['Anna', 'Cindy', 'Frank', 'John']

當然,這種類方法調用有點多此一舉,不如這樣:

def keys(self):
    return sorted(self.keys())

 

多重繼承
Python也允許子類繼承多個基類。這種特性就是通常所說的多重繼承。使用多重繼承時,有兩個不同的方面要記住,一是要找到合適的屬性,二是重寫方法時,如何調用父類方法讓它們發揮作用,同時子類處理好自己的義務。

1.方法解釋順序(MRO)
在Python2.2以前的版本中,算法非常簡單:深度優先,從左至右進行搜索,取得在子類中使用屬性。其他Python算法只是覆蓋被找到的名字,多重繼承則取找到的第一個名字。
而現在的Python應用了C3算法
C3最早被提出是用於Lisp的,應用在Python中時為了解決深度優先搜索算法不滿足本地優先級,和單調性問題。
本地優先級:指聲明時父類的順序,C(A,B),如果訪問C類對象屬性時,應該根據聲明順序,優先查找A類,然后查找B類。
單調性:如果在C的解析順序中,A排在B的前面,那么在C的所有子類里,也必須滿足這個順序。
在Python官網中MRO的作者舉了例

F=type('Food', (), {remember2buy:'spam'})
E=type('Eggs', (F,), {remember2buy:'eggs'})
G=type('GoodFood', (F,E), {})

根據本地優先級在調用G類對象屬性時應該優先查找F類,而在Python2.3之前的算法給出的順序是G E F O,而在心得C3算法中通過阻止類層次不清晰的聲明來解決這一問題,以上聲明在C3算法中就是非法的。

C3算法
判斷mro要先確定一個線性序列,然后查找路徑由序列中類的順序決定,所以C3算法就是生成一個線性序列。
如果繼承至一個基類:
class B(A)
這時B的mro序列為[B,A]

如果繼承至多個基類
class B(A1,A2,A3 ...)
這時B的mro序列 mro(B) = [B] + merge(mro(A1), mro(A2), mro(A3) ..., [A1,A2,A3])
merge操作就是C3算法的核心。
遍歷執行merge操作的序列,如果一個序列的第一個元素,在其他序列中也是第一個元素,或不在其他序列出現,則從所有執行merge操作序列中刪除這個元素,合並到當前的mro中。
merge操作后的序列,繼續執行merge操作,直到merge操作的序列為空。
如果merge操作的序列無法為空,則說明不合法。
例子:

class A(O):pass
class B(O):pass
class C(O):pass
class E(A,B):pass
class F(B,C):pass
class G(E,F):pass

 

A、B、C都繼承至一個基類,所以mro序列依次為[A,O]、[B,O]、[C,O]
mro(E) = [E] + merge(mro(A), mro(B), [A,B])
= [E] + merge([A,O], [B,O], [A,B])
執行merge操作的序列為[A,O]、[B,O]、[A,B]
A是序列[A,O]中的第一個元素,在序列[B,O]中不出現,在序列[A,B]中也是第一個元素,所以從執行merge操作的序列([A,O]、[B,O]、[A,B])中刪除A,合並到當前mro,[E]中。
mro(E) = [E,A] + merge([O], [B,O], [B])
再執行merge操作,O是序列[O]中的第一個元素,但O在序列[B,O]中出現並且不是其中第一個元素。繼續查看[B,O]的第一個元素B,B滿足條件,所以從執行merge操作的序列中刪除B,合並到[E, A]中。
mro(E) = [E,A,B] + merge([O], [O])
= [E,A,B,O]


2.簡單屬性查找示例
下面這個例子將對兩種類的方案不同處做一展示。腳本由一組父類,一組子類,還有一個子孫類組成。
首先是經典類:

>>> class P1: #(object):
  def foo(self):
    print 'called P1-foo()'

>>> class P2: #(object):
  def foo(self):
    print 'called P2-foo()'

>>> class P2(object):
  def foo(self):
    print 'called P2-foo()'
  def bar(self):
    print 'called P2-bar()'

>>> class C1(P1,P2):
  pass

>>> class C2(P1, P2):
  def bar(self):
    print 'called C2-bar()'

>>> class GC(C1, C2):
  pass

 

在經典類中,我們如下執行:

>>> gc = GC()
>>> gc.foo() # GC => C1 =>P1
called P1-foo()
>>> gc.bar() # GC => C1 => P1 => P2
called P2-bar()

 

在這種搜索方式下,foo()是很容易被理解的,而bar()的尋找則是通過GC,C1,P1后,找不到,就到P2中找到,C2根本不會被檢索。
新式類,也就是在聲明時加上(object)
結果如下:

>>> gc = GC()
>>> gc.foo() # GC => C1 => C2 => P1
called P1-foo()
>>> gc.bar() # GC => C1 => C2
called C2-bar()

新式類采用了一種廣度優先的方式,查找順序如圖所示。同時,新式類有一個__mro__屬性,可以告訴你查找順序。

>>> GC.__mro__
(<class '__main__.GC'>, <class '__main__.C1'>, <class '__main__.C2'>, <class '__main__.P1'>, <class '__main__.P2'>, <type 'object'>)

 

為什么新式類的MRO方式和經典類出現了不同呢?這是因為菱形效應,如果繼續沿襲經典類的深度優先搜索,可能會導致不能有效繼承。

關於多重繼承的順序,我暫時也不是很清楚,等之后搞明白了這件事再寫吧。


類、實例和其他對象的內建函數
issubclass()
這是布爾函數,判斷一個類是不是另一個類的子類或者子孫類,語法如下:
issubclass(sub, sup)
True則是sub是sup的不嚴格子類,False則代表sub不是sup的子類。

isinstance()
這個布爾函數判定一個對象是否是另一個給定類的實例。語法如下:
isinstance(obj1, obj2)
isinstance()在obj1是obj2的一個實例或者是obj2子類的一個實例時,返回一個True。這里第二個參數必須是類,不然會得到TypeError.如果第二個參數是類型對象,這是允許的,我們也常常這樣用它:

>>> isinstance(3, int)
True
>>> isinstance(2.2, int)
False

hasattr()、getattr()、setattr()、delattr()
這些函數可以在各種對象下工作,不只是類和實例。
hasattr()函數是布爾型的,它的目的是為了決定一個對象是否有一個特定的屬性,一般用於訪問某些屬性前做一下檢查。getattr()和setattr()函數相應地取得和賦值給對象的屬性,getattr()在你試圖讀取一個不存在的屬性時,會引發AttributeError異常。setattr()將要加入一個新的屬性,要么取代一個已存在的屬性。而delattr()函數會從一個對象中刪除屬性。

dir()和super()
這兩個內建函數之前都已經提到,在此不多加說明。

vars()
該內建函數與dir()相似,只是給定的對象參數必須有一個__dict__屬性。vars()返回一個字典,它包含了對象存儲於其__dict__中的屬性(鍵)和值。


免責聲明!

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



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