python多重繼承的鑽石問題


如下,我們已經有了一個從Contact類繼承過來的Friend類

class ContactList(list):
    def search(self, name):
        '''Return all contacts that contain the search value
           in their name.'''
        matching_contacts = []
        for contact in self:
            if name in contact.name:
                matching_contacts.append(contact)
        return matching_contacts

class Contact:
    all_contacts = ContactList()
  
    def __init__(self, name, email):
        self.name = name
        self.email = email
        Contact.all_contacts.append(self)

class Friend(Contact):
    '''通過super得到父類對象的實例,並且調用這個對象的__init__方法,
       傳遞給它預期的參數,然后這個類做了自己的初始化,即設置phone屬性'''
    def __init__(self, name, email, phone):
        super().__init__(name, email)
        self.phone = phone

如果要給Friend類增加一個住址的方法,住址信息包括街道、城市、國家等。我們可以把這些字符串直接傳遞給Friend中的__init__方法,另外也可以把這些字符串先存放在一個元組或者字典里面,然后再把他作為單一的參數傳遞給__init__方法。

另一種方法就是,創建一個新的Address類來專門包括這些字符串,並且把這個類的一個實例傳給Friend類的__init__方法。這樣做的好處是在其他的如建築、商業、組織中重用這個Address類。

class AddressHolder:
    def __init__(self, street, city, state, code):
        self.street = street
        self.city = city
        self.state = state
        self.code = code

現在問題來了,在已經存在的從Contact類繼承過來的Friend類中如何增加一個住址。

最好的方法是多重繼承,但是這樣會有兩個父類的__init__方法需要被初始化,並且他們要通過不同的參數進行初始化,如何來做呢?讓我們從一個天真的方法開始,對上述代碼的Friend進行改寫:

class Friend(Contact, AddressHolder):
    def __init__(self, name, email, phone, street, city, state, code):
        Contact.__init__(self, name, email)
        AddressHolder.__init__(self, street, city, state, code)
        self.phone = phone        

上述從技術層面上是可以工作的,但是存在一些問題。

首先,如果我們忽略顯式地調用初始化函數可能會導致一個超類未被初始化。在這里並不明顯,但是在另一些場景會導致程序崩潰,比如把數據插入到一個未連接的數據庫里。

第二,由於這些類的層次結果,可能會導致某個超類被調用多次。如下圖所示。

       

從上圖中,Friend中的__init__首先調用了Contact中的__init__,隱私初始化了object(所有類都繼承於object)。Friend然后又調用AddressHolder的__init__,又一次隱式初始化了object超類,父類被創建了兩次。在我們的這個情況下,它是無害的,但是在一些場景中,會帶來災難。(每一個方法的調用順序可以通過__mro__修改,這里略)

 

再看如下的一個例子,我們有一個基類,該基類有一個call_me方法,有兩個子類重寫了這個方法,然后第3個類通過多重繼承擴展了兩個方法。這稱為鑽石繼承

       

從技術上來說,在python3的所有多重繼承都是鑽石繼承,因為所有的類都從object繼承,上圖中的object.__init__同樣是一個鑽石問題。把上圖轉化成代碼如下:

class BaseClass:
    num_base_calls = 0
    def call_me(self):
        print("Calling method on Base Class")
        self.num_base_calls += 1

class LeftSubclass(BaseClass):
    num_left_calls = 0
    def call_me(self):
        BaseClass.call_me(self) print("Calling method on Lef Subclass")
        self.num_left_calls += 1

class RightSubclass(BaseClass):
    num_right_calls = 0
    def call_me(self):
        BaseClass.call_me(self) print("Calling method on Right Subclass")
        self.num_right_calls += 1

class Subclass(LeftSubclass, RightSubclass):
    num_sub_calls = 0
    def call_me(self):
        LeftSubclass.call_me(self)
        RightSubclass.call_me(self) print("Calling method on Subcalss")
        self.num_sub_calls += 1

調用並得到如下輸出:

$ python -i zuanshi.py 
>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Lef Subclass
Calling method on Base Class
Calling method on Right Subclass
Calling method on Subcalss
>>> print(s.num_sub_calls, s.num_left_calls, 
... s.num_right_calls, s.num_base_calls)
1 1 1 2

基類的call_me被調用了兩次。但這不是我們想要的,如果在做實際的工作,這將導致非常嚴重的bug,如銀行存款存了兩次。

對於多重繼承,我們只想調用“下一個”方法,而不是父類的方法。實際上,下一個方法可能不屬於當前類或者當前類的父類或者祖先類。關鍵字super可以解決這個問題,如下是代碼重寫:

class BaseClass:
    num_base_calls = 0
    def call_me(self):
        print("Calling method on Base Class")
        self.num_base_calls += 1

class LeftSubclass(BaseClass):
    num_left_calls = 0
    def call_me(self):
        super().call_me() print("Calling method on Lef Subclass")
        self.num_left_calls += 1

class RightSubclass(BaseClass):
    num_right_calls = 0
    def call_me(self):
        super().call_me() print("Calling method on Right Subclass")
        self.num_right_calls += 1

class Subclass(LeftSubclass, RightSubclass):
    num_sub_calls = 0
    def call_me(self):
        super().call_me() print("Calling method on Subcalss")
        self.num_sub_calls += 1

執行結果如下:

>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Right Subclass
Calling method on Lef Subclass
Calling method on Subcalss
>>> print(s.num_sub_calls, s.num_left_calls, 
... s.num_right_calls, s.num_base_calls)
1 1 1 1

首先,Subclass的call_me方法調用了super().call_me(),其實就是引用了LeftSubclass.call_me()方法。然后LeftSubclass.call_me()調用了super().call_me(),但是這時super()引用了RightSubclass.call_me()。需要特別注意:super調用並不是調用LeftSubclass的超類(就是BaseClass)的方法。它是調用RightSubclass,雖然它不是LeftSubclass的父類!這就是下一個方法,而不是父類方法。RightSubclass然后調用BaseClass,並且通過super調用保證在類的層次結構中每一個方法都被執行一次。

 

參考:

1、《Python3 面向對象編程》 [加]Dusty Philips 著

 


免責聲明!

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



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