在编程重复的代码是很不好的,不应该在很多地方有多份相同或类似的代码副本。
把具有相似功能的代码片段或者对象合并起来的方法有多种,这一章主要讲继承。
继承允许我们在两个或多个类之间创建一种“是一个”的关系,这种关系把共通的特性抽象到一个超类,特有的细节存于子类中。
基本继承
从就上来讲,每一个类都使用了继承,所有的Python类都是一个叫做object
的特殊类的子类。这个类提供了非常少的数据和行为(这些它提供的行为都是以双下划线开头的方法,这些方法都只供内部使用。)
如果不指明我的类的继承,那这个类自动从object
继承过来。我们也可以公开声明从object
类继承。:
class MyClass(object):
pass
这个object
就是MyClass
的超类,也叫父类,指被继承的类。子类是继承过来的类,如MyClass
。我们可以说一个子类来源于父类。或者说这个子类扩展了父类。
那么如何应用,最基本的功能就是使用继承为父类添加功能。
我们从一个简单的通讯录开始,这个通讯录可以记录一些人的姓名和电子邮件地址。contact类负责维护一个类变量中所有联系人的列表,并初始化姓名和地址:
class Contact:
all_contacts = []
def __init__(self,name,email):
self.name = name
self.email = email
Contact.all_contacts.append(self)
all_contacts这个列表是一个类变量,它属于类定义的一部分,被所有这个类的实例共享。这意味着有且只有一个Contact.all_contacts列表。我们在任意一个对象中调用self.all_contacts,会引用这个列表。在初始化函数中的代码Contact.all_contacts.append(self)
保证了无论何时我们创建一个新的contact类,这个列表都会自动把这个对象添加进来。
这是一个很简单的类,它允许我们记录关于联系人的一些信息。如果我们的某些联系人同时是需要从他们那里订购东西的供应商该怎么办。我们可以给Contact添加一个order方法,但这样会导致所有的联系人都可以订购东西。所以我们可以新建一个Supplier类,和Contact类一样,但是它有额外的order方法:
class Supplier(Contact):
def order(self,order):
print("If this wera a real system we would send ""{} order to {} ".format(order,self.name))
#注意此处也可以这样写:
#print("If this wera a real system we would send %s order to %s "%(order,self.name))
Python一共有两种格式化输出语法,一种是类似于C语言printf的方式,称为 Formatting Expression
>>>'%s %d-%d' % ('hello', 7, 1) 'hello 7-1'
另一种是类似于C#的方式,称为String Formatting Method Calls
>>> '{0} {1}:{2}'.format('hello', '1', '7') 'hello 1:7' >>>print( 'test:{0:10f}'.format(math.pi)) 'test: 3.141593'
.format( , , )中分别表示输出内容。数字(0, 1, …)即代表format()里面的元素。字符串的参数使用{NUM}进行表示,0, 表示第一个参数,1, 表示第二个参数, 以后顺次递加
我们测试这段代码:
>>> c = Contact('tom','tom@123.com')
>>> s = Supplier('tim','tim@111.com')
>>> c.name;c.email;s.name;s.email
'tom'
'tom@123.com'
'tim'
'tim@111.com'
>>> c.all_contacts
[<__main__.Contact object at 0x000001BCDD37EFD0>, <__main__.Supplier object at 0x000001BCDD37E550>]
>>> s.all_contacts
[<__main__.Contact object at 0x000001BCDD37EFD0>, <__main__.Supplier object at 0x000001BCDD37E550>]
>>> s.order('help')
If this wera a real system we would send help order to tim
现在我们的Supplier类可以做任何Contact类可以做的事,还可以提供order的方法。这就是继承的魅力。
扩展内置类
这种继承最有趣的应用就是给内置的类增加功能。之前看到的Contact类中,我们把联系人添加到所有联系人的列表里,如果我们想通过名字搜索怎么办?那么,我们可以给Contact类添加一个方法用于搜索,但是似乎这个方式实际上属于列表本身。我们可以使用继承来做:
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)
我们创建了一个新的ContactList来扩展内置的list,而不是实例化一个普通的列表来作为我们的变量。然后我们实例化这个子类来作为all_contacts列表,就可以像下面这样测试这个新的搜索功能:
>>>c1 = Contact("John A","johna@example.com")
>>>c2 = Contact("John B","johnb@example.com")
>>>c3 = Contact("Jenna C","jennac@example.com")
>>>[c.name for c in Contact.all_contacts.search('John')]
['John A','John B']
上面的例子中,我们拓展了内置List类,Python中大部分的数据类型都可拓展,如object\list\set\dict\file\str,其他的int\float偶尔也会拓展。
重写和super
给已存在的类添加新的行为,继承是很好的方式,但是如果要改变行为呢?我们的contact类只允许一个名字和一个电邮,这对大部分人都适用,但是我们要给亲戚朋友加一个电话项目该怎么办?
在第2章里,可以在联系人构建之后手动添加一个电话属性,但是这样每个都手动添加未免过于繁杂,所以在这里就要采用重写__init__
函数方法来完成。重写就是在子类中用一个和超类相同名字的方法来改变或者覆盖超类的方法。这样程序会自动执行子类的方法而跳过超类的方法。例如:
class Friend(Contact):
def __init__(self,name,email,phone):
self.name = name
self.email = email
self.phone = phone
不仅只有__init__
可以被重写,任何方法都可以被重写。但是:
我们的Contact和Friend类有重复的代码去建立name和email,这会导致维护复杂,因为我们需要在两个或者更多的地方更新代码。更值得警惕的是,Friend类忽略了把自己加到all_contacts列表里,这个列表使我们在Contact里创建的。
所以,我们应该采用一种调用父类的方法,这就是super函数的功能:它返回一个父类的实例化对象,允许我们直接调用父类的方法:
class Friend(Contact):
def __init__(self,name,email,phone):
super().__init__(name,email)
self.phone = phone
这个例子中首先通过super得到父类的对象的实例,并且调用这个对象的__init__
方法,传递给它预期的参数。然后这个类做了自己的初始化,即设置phone属性。
- super()可以在任何方法里调用,不只是
__init__
方法。 - 这意味着通过重写和调用super(),可以修改所有的方法。
- 可以再方法内的任何位置调用super(),不必总在第一行调用。
- 例如把传入的参数传给超类之前,我们可能需要先spuer()一下。
多重继承
多重继承是一个敏感的话题。从原理上来说很简单:一个从多个父类继承过来的子类,可以访问所有父类的功能。实践中,它很不好用,而且老司机给了我们警告:
根据经验,如果你认为你需要多重继承,你有可能是错误的;但如果你知道你需要它,那你可能是正确的。
多重继承最简单、最有用的形式叫做mixin。一个mixin通常是一个超类,这个超类不是因为自己而存在,而是通过被其他类继承来提供额外的功能。比如:假设我们想给我们的Contact类增加一个功能,允许给self.email发送一封电子邮件。发送电子邮件是一个非常常见的任务,我们可能会想在其他的类里使用这个功能。所以我们可以写一个简单的mixin类来为我们做这个发电子邮件的功能。
class MailSender:
def send_mail(self,message):
print("Sending mail to " + self.email)
#发送邮件实际代码
这个类,不作任何事情,意义就是被继承,我们定义一个新的类,这个类既是Contact又是MailSender:
class EmailableContact(Contact,MailSender):
pass
多重继承就像类定义里的参数列表。括号里包含两个或多个基类,这些基类用逗号隔开。
我们测试一下这个新的混合类来看一下mixin怎么发挥作用的:
>>> e = EmailableContact("jack","jack@com")
>>> Contact.all_contacts
[<__main__.EmailableContact object at 0x000002AC73611470>]
>>> e.send_mail("hello")
Sending mail to jack@com
Contact类的初始化函数任然会把一个新的联系人添加到all_contacts这个列表中,并且mixin可以发送电子邮件给self.email。所以一切看起来都很正常。
让我们想想除了mixin还有什么选择:
- 我们可以使用单一继承并且把send_mail函数添加到子类里。这种方法的缺点是这个发送邮件的功能会重复出现在其他任何需要发送电子邮件的类里。
- 我们可以创建一个独立的python函数用来发送电子邮件,并且当需要发送电子邮件的时候,需要传入一个正确的电子邮件地址作为参数。
- 我们可以使用monkey-patch(后面会讲)的方法。可以通过定一个函数让其接受self作为参数,并且作为一个属性把这个函数传递给一个已经存在的类来实现。
当混合多个不同类的时候,多重继承可以很好的工作,但是,当我们要调用超类的时候,这会变得很混乱。因为这里会有多个父类,你怎么知道你调用它们的顺序呢?
假设我们现在要给Friend类新增一个家庭地址,这意味着要加入一些街道,城市,国家等字符串信息。我们可以把这些字符串作为参数直接传递给__init__
方法,也可以把字符串先保存在一个元组或列表里,然后把它作为单一字符串传递给__init__
方法。如果没有其他功能被添加,这是很好的方法。
另一个选择就是创建一个Address类来专门保存这些信息,并把其一个实例传递给Friend的__init__
方法。这样的好处是,我们可以对信息做一些处理,例如打印地图等。而且可以在其他问题里(建筑,商业等)使用这个Address类。
采用继承也可以完成,我们新建一个类叫做AddressHolder,因为一个Friend不是一个Address,应该说一个Friend是一个AddressHolder。同样这个类可以用在其他问题里。
class AddressHolder:
def __init__(self,street,city,state,code):
self.street = street
self.city = city
self.state = state
self.code = code
我们只用把所有数据放入实例化变量的时候赋给了初始化方法。
(待续)