Python的继承与多继承小讲
参考自《Python3面向对象编程》[加]Dusty Phillips 著
基本继承
定义一个Contact类
class Contact:
all_contacts = []
def __init__(self, name, email):
self.name = name
self.email = email
Contact.all_contacts.append(self)
创建一个新类Supplier继承自Conract类并添加一个order方法
class Supplier(Contact):
def order(self, order):
print('{} order to {}'.format(order, self.name))
在解释器里进行测试,实例化Contact及Supplier,两者在__init__都接收一个名字和一个邮件地址作为输入参数,但是只有Supplier有一个order功能
In [7]: c = Contact('Some body', 'somebodi@qq.com')
In [8]: s = Supplier('Another body', 'anotherbody@qq.com')
In [9]: print(c.name, c.email, s.name, s.email)
Some body somebodi@qq.com Another body anotherbody@qq.com
In [10]: c.all_contacts
Out[10]: [<__main__.Contact at 0x271d33d8808>, <__main__.Supplier at 0x271d45713c8>]
In [11]: c.order('i need pliers')
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-11-c4ce331f3b02> in <module>
----> 1 c.order('i need pliers')
AttributeError: 'Contact' object has no attribute 'order'
In [12]: s.order('i need pliers')
i need pliers order to Another body
扩展内置类
扩展内置的list使其具有search方法
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
运用扩展后的内置类重新定义Contact类,使其保存联系人列表的类变量all_contacts具有按名字搜索指定联系人的功能
class Contact:
all_contacts = ContactList()
def __init__(self, name, email):
self.name = name
self.email = email
Contact.all_contacts.append(self)
这里我们创建了一个新的ContactList类来扩展Python的内置list,而不是实例化一个普通的列表作为我们的类变量。然后我们实例化这个自类来作为我们的all_contacts列表。接下来我们来测试这个新的列表的搜索功能
In [35]: c1 = Contact('John A', 'johna@qq.com')
In [36]: c2 = Contact('John B', 'johnb@qq.com')
In [37]: c3 = Contact('Jenna C', 'jennac@qq.com')
In [38]: [c.name for c in Contact.all_contacts.search('John')]
Out[38]: ['John A', 'John B']
像上面例子一样,我们可以扩展dict类,给其添加一个找到并返回长度最长的key
class LongNameDict(dict):
def longest_key(self):
longest = None
for key in self:
if not longest or len(key) > len(longest):
longest = key
return longest
简单的测试
In [45]: longkeys = LongNameDict()
In [46]: longkeys['hello'] = 1
In [47]: longkeys['longest yet'] = 5
In [48]: longkeys['hello2'] = 'world'
In [49]: longkeys.longest_key()
Out[49]: 'longest yet'
重写和super
给已经存在的类添加新的行为,使用继承,但是如果需要改变已有的行为则需要使用重写
在Python中几乎任何方法都可以重写
例如
class Friend(Contact):
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
像上面例子即实现了对__init__方法的重写,但是有一些问题。
Contact和Friend类有重复的代码去建立属性,这会导致维护非常复杂,因为我们需要在两个或更多的地方更新代码。更值得警惕的是,我们的Friend忽略了把自己加到all_contacts列表里,这个列表是我们在Contact类里创建的。
我们真正需要的是一种可以直接调用父类代码的方法。这就是super函数的功能;它返回一个父类的实例化对象,允许我们直接调用父类的方法
class Friend(Contact):
def __init__(self, name, email, phone):
super().__init__(name, email)
self.phone = phone
测试Friend类
In [76]: f1 = Friend('yangc', 'yangc@qq.com','123456')
In [77]: print(f1.name, f1.email, f1.phone)
yangc yangc@qq.com 123456
In [78]: Contact.all_contacts.search('yangc')
Out[78]: [<__main__.Friend at 0x271d20acb08>]
多重继承
多重继承是一个敏感的主题。原则上来讲,它非常简单:一个从多个父类继承过来的子类,可以访问所有父类功能。但在实践中,它并没有这么简单。
多重继承最简单、最有用的形式叫做Mixin。一个Mixin通常是一个超类,这个类不是因为自己而存在,而是通过被其他类继承来提供额外功能。这里不做详细讲解而仅通过一个简单的例子来说明,读者可以到网上详细了解。
例子:
假设我们想给Contact类增加一个功能,允许给self.email发送一封电子邮件。发送电子邮件是一个常见的任务,我们可能会在其他类里也使用这个功能。所以我们可以写一个简单的Mixin类并继承它来实现这个发送电子邮件的功能。
class MailSender:
def send_mail(self, message):
print('Sending email to' + self.email)
# 邮件逻辑
这个类不做任何特别的事情,但通过多重继承,我们可以使一个新类具有发送邮件的功能。
class EmailableContact(Contact, MailSender):
pass
测试
In [99]: e = EmailableContact('John Smith', 'jsmith@qq.com')
In [100]: Contact.all_contacts
Out[100]:
[<__main__.Friend at 0x271d20acb08>,
<__main__.EmailableContact at 0x271d51a9088>]
In [101]: e.send_mail('Hell0 world!')
Sending email tojsmith@qq.com
如上,我们的新类EmailableContact既有Contact类的所有属性和功能,又有MailSender类的发送邮件的功能。
但是,当我们要调用显式调用超类方法时,多重继承会变得非常混乱,因为这里有多个父类,该调用哪一个?调用的顺序如何?这就是我们接下来要探讨的另一个问题。
钻石问题
(为什么叫钻石问题是因为该问题所涉及的类继承关系在图上显示是一个钻石形状,在Python中所有多重继承都是钻石继承)
如果我们要给Friend类增加一个家庭地址,除了在编写Friend类时传入参数初始化类变量外,我们还有一个可选的选择同上面增加发送邮件的功能一样,就是新增一个AddressHolder类,并让Friend继承他,AddressHolder类如下
class AddressHolder:
def __init__(self, street, city, state):
self.street = street
self.city = city
self.state = state
现在我们的Friend就可以通过继承自Contact和AddressHolder类来获得名字、邮件、地址等属性。
但是我们发现,在这里我们需要对两个父类的__init__方法进行初始化,而且两个方法参数不同,我们目前能想到的一个可行的方法就是分别调用父类的初始化方法来初始化当前的Friend类,具体如下
class Friend(Contact, AddressHolder):
def __init__(self, name, email, phone, street, city, state):
Contact.__init__(self, name, email)
AddressHolder.__init__(self, street, city, state)
self.phone = phone
在这里我们直接调用了每一个父类的__init__方法并显式传递self参数,理论上是可行的,但是通过继承关系我们发现,Friend调用了Contact类的__init__,之后再调用AddressHolder的__init__,而Contact、AddressHolder都继承自object类,这意味着在这个过程中,我们隐式地调用了object类两次!这在当前情况下是无害的,但是在一些情境里可能会带来灾难!
想象一种可能的情况是:如果我们需要在object类初始化时进行数据库的链接,那么在上诉情况下很明显,Friend类在实例化时,object类会进行两次数据库链接!事实上我们仅需进行一次数据库链接。 !
那么我们在什么情况下该调用object呢?或者说在实例化一个Friend类时我们对其超类调用的顺序应该是怎样?应该是下述那种情况呢?
- Friend→Contact→Object→AddressHolder
- Friend→Contact→AddressHolder→Object
此时对该问题就产生了分歧
我们通过另外一个例子来进一步说明该问题
定义一个基类有一个方法叫call_me,该基类有两个子类重写了该方法,在其方法内部调用了父类的call_me方法,然后另外有一个类重继承了上面两个类并同样重写了该方法,同样,在其方法内部调用了父类的call_me方法。在每个类的内部有一个统计该类被调用了多少次的类变量。具体代码如下
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 Left 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 Subclass')
self.num_sub_calls += 1
这个例子中桶Friend例子一样,通过类名来调用父类方法,并且每一次被调用,把调用的信息打印出来,并且在类里更新一个变量来显示该类被调用了多少次,下面我们进行测试
In [103]: s = Subclass()
In [104]: s.call_me()
Calling method on Base Class
Calling method on Left Subclass
Calling method on Base Class
Calling method on Right Subclass
Calling method on Subclass
In [105]: print(s.num_sub_calls, s.num_left_calls, s.num_right_calls, s.num_base_calls)
1 1 1 2
如上所示,我们在实例化一个Subclass类并调用其call_me方法时,基类的call_me方法被调用了两次!想象一个场景:如果基类的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 Left 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 Subclass')
self.num_sub_calls += 1
改变很小,我们仅仅把直接调用换成了调用super()方法,下面让我们执行它,看看执行结果
In [107]: s = Subclass()
In [108]: s.call_me()
Calling method on Base Class
Calling method on Right Subclass
Calling method on Left Subclass
Calling method on Subclass
In [109]: print(s.num_sub_calls, s.num_left_calls, s.num_right_calls, s.num_base_calls)
1 1 1 1
如上面结果显示,基类BaseClass只被调用了一次。但情况为什么会是这样子?在super调用之后我们都执行了一行print语句,所以输出被打印出来的顺序就是每一个方法实际上被执行的顺序,下面我们来进行进一步分析。
首先,Subclass的call_me方法调用了super().call_me(),其实就是引用了LeftSubClass的call_me()方法。然后LeftSubClass的call_me()调用了super().call_me(),但是这时super()引用了RightSubClass的call_me()(我们可以这么理解:由于Subclass有俩两个父类方法,所以再调用一次super不是指当前类的父类,而是指“下一个”父类),之后RightSubClass的call_me()调用了super().call_me(),此时的“下一个”指的就是基类BaseClass。
用spuer关键字代替直接用类名执行父类方法之后,我们就保证了每个父类方法仅被执行了一次,就不会出现上诉所讲的“链接两次数据库”、“往银行中存两次钱”等现象,但是这时又出现了一个问题,如果父类的方法名相同但是参数不同怎么办?
不同的参数集合
回到Friend多重继承的例子,此时出现的一个问题是:Friend的两个父类的参数集合不同,在Friend中调用super()._ init _()后,我们如何知道哪些参数哪些是传给哪些父类的呢?这时我们就需要使用关键字参数来解决问题,传递的关键字参数在其每一个需要使用它的父类中被取用,所以我们并不需要关心super尝试去首先初始化哪个类,因为通过关键字传递参数之后,在super初始化时总能找到对应的参数(也不用关系参数的顺序)。
Friend多重继承的使用super的正确版本
class Contact:
all_contacts = []
def __init__(self, name='', email='', **kwargs):
super().__init__(**kwargs)
self.name = name
self.email = email
Contact.all_contacts.append(self)
class AddressHolder:
def __init__(self, street='', city='', state='', **kwargs):
super().__init__(**kwargs)
self.street = street
self.city = city
self.state = state
class Friend(Contact, AddressHolder):
def __init__(self, phone='', **kwargs):
super().__init__(**kwargs)
self.phone = phone
通过设置空字符串为参数默认值,我们把所有的参数变成了关键字参数。而且我们也保证了一个**kwargs参数,该参数是一个参数字典,负责在调用super的过程中,把参数传递给“下一个”类。
总结:如果我们在多继承的过程中需要用到多个同名的父类函数并且要保证每个父类函数仅需使用一次,其参数列表还不同,我们可以考虑使用super关键字实现对“下一个”父类函数的调用,并且使用关键字参数在这个过程中传递不同的参数值,将参数传递给“下一个”类。。
也就是说,如果我们要向上传递参数(这里的“上”指的是当前类的父类继承关系),我们就需要把参数放在 **kwargs中以便“下一个”类可以引用它。拿Friend来举例,如果其父类需要使用到phone参数,则我们需要把phone参数也放到 **kwargs中。
关于钻石继承的问题就如上面所讲。