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 著

 

posted @ 2017-12-29 10:48  anovana  阅读(2823)  评论(0编辑  收藏  举报