3.2 通过继承扩展新的功能
通过继承可以复用已有类的功能,并可以在无需重新编写原来功能的基础上对现有功能进行扩展。在实际应用中,会把通用性的代码封装到父类,通过子类继承父类的方式优化代码的结构,避免相同的代码被多次重复编写。
3.2.1 继承的常见用法
继承的语法是在方法名后加个括号,在括号中写要继承父类的名字。在Python语言中,由于object类是所有类的基类,因此如果定义一个类时没有指定继承哪个类,就默认继承object类。在下面的InheritanceDemo.py范例程序中演示了继承的一般用法。
1 # !/usr/bin/env python 2 # coding=utf-8 3 class Employee(object): # 定义一个父类 4 def __init__(self,name): 5 self.__name=name 6 def get_name(self): 7 return self.__name 8 def set_name(self,name): 9 self.__name=name 10 def login(self): # 父类中的方法 11 print("Employee In Office") 12 def changeSalary(self,newSalary): 13 self._salary=newSalary 14 def get_Salary(self): 15 return self._salary 16 # 定义一个子类,继承Employee类 17 class Manager(Employee): 18 def login(self): # 在子类中覆盖父类的方法 19 print("Manager In Office") 20 print("Check the Account List") 21 def attendWeeklyMeeting(self): 22 print("Manager attend Weekly Meeting") 23 # 使用类 24 manager=Manager("Peter") 25 print(manager.get_name()) # Peter 26 manager.login() # 调用子类的方法,Manager In Office 27 manager.changeSalary(30000) 28 print(manager.get_Salary()) # 30000 29 manager.attendWeeklyMeeting()
在第3行中定义了名为Employee的员工类,同时指定它继承自默认的object类,事实上,这句话等同于class Employee()。在这个父类里,定义了员工类的通用方法,比如在第4行定义的构造函数中设置了员工的名字,在第6行和第8行开始分批定义了获取和设置名字属性的方法,在第12行和第14行分别定义了更改和获取工资的方法。
正是因为在Employee父类中封装了诸如设置工资等的通用性方法,所以子类Manager里的代码就相对简单。具体来说,在第17行定义Manager类时,是通过括号的方式指定该类继承自Employee类,在其中可以复用父类公有的和受保护的方法。此外,在第18行中覆盖(也叫覆写或重写)了父类中的login方法,并在第21行定义了专门针对子类的attendWeeklyMeeting方法。
在第24行中实例化了一个名为manager对象,因为在Manager子类里没定义__init__方法,所以这里调用的是父类Employee里的__init__方法,可从第25行的打印语句中看到。同样,在第27和第28行中,manager对象也复用了定义在父类(即Employee类)里的方法。
从第26行login方法的打印结果来看,这里执行的是子类里的login代码,这说明如果子类覆盖了父类的方法,那么最终会执行子类的方法。
在第29行中,调用了子类特有的attendWeeklyMeeting方法,结果会毫无疑问地输出“Manager attend Weekly Meeting”。
3.2.2 受保护的属性和方法
在3.2.1小节的范例程序中,除了在父类中用到了__name这个私有变量外,还用到了带一个下画线的_salary受保护的变量,而带一个下画线开头的方法叫受保护的方法。这类受保护的属性和方法能在本类和子类中被用到。在下面的ProtectedDemo.py范例程序中来看一下如何合理地使用受保护的属性和方法。
1 # !/usr/bin/env python 2 # coding=utf-8 3 class Shape: # 定义父类 4 _size=0 # 受保护的属性 5 def __init__(self,type,size): 6 self._type=type 7 self._size=size 8 def _set_type(self,type): # 受保护的方法 9 self._type=type 10 def _get_type(self): # 受保护的方法 11 return self._type 12 class Circle(Shape): # 定义子类 13 def set_size(self,size): 14 self._size=size # 覆盖了父类的_size属性 15 def printSize(self): 16 print(self._size) 17 class anotherClass: # 定义不相干的一个类 18 pass # 如果是空方法,则需要加个pass,否则会报错 19 # 使用子类 20 c=Circle("Square",2) 21 c._set_type("Circle") 22 print(c._get_type()) 23 c.printSize() 24 anotherClass._set_type("Circle") # 会报错
在第3行开始定义父类Shape的部分,在其中的第4行定义了名为_size的受保护的属性,同时在第8行和第10行中定义了两个以单下画线开头的受保护的方法。在第12行开始定义Shape类的子类Circle部分,在其中的第14行和第16行用到了父类定义的_size这个受保护的变量。
由于受保护的变量能在本类和子类里被使用,因此在第20行初始化子类时,其实是用子类的_size覆盖掉了父类的_size,同时,在第21行和第22行的调用中,我们可以看到子类能调用父类中受保护的方法。但是要注意的是,受保护的方法不能在非子类中被调用,比如在第24行中,因为anotherClass不是Shape的子类,所以调用_set_type时会报错。
前文讲述过,需要把仅在本类里用到的属性和方法封装成私有的,基于“封装”特性的同样考虑,这里的“获取和设置形状种类”的方法,它的有效范围是在“形状基类”和对应的子类里,而其他的类不该调用它们,因此对于这类的属性和方法,就不应该定义成“公有的”,而应该定义成“受保护的”。
3.2.3 慎用多重继承
在Java等语言中,一个类只能继承一个父类,这叫“单一继承”。但在Python语言中,一个子类可以继承多个父类,这叫“多重继承”。
这种做法看似提供了很大的便利,但如果项目里的代码量很多,使用多重继承会增加代码的维护成本,所以如果没有特殊需求,最好只使用“单一继承”,而不要使用“多重继承”。在下面的MoreParentsDemo.py范例程序中,我们来看一下多重继承带来的困惑。
1 # !/usr/bin/env python 2 # coding=utf-8 3 class FileHandle(object): # 处理文件的类 4 def read(self,path): 5 print("Reading File") 6 # 读文件 7 def write(self,path,value): 8 __path=path 9 print("Writing File") 10 # 写文件 11 class DBHandle(object): # 处理数据库的类 12 def read(self,path): 13 print("Reading DB") 14 # 读数据库 15 def write(self,path,value): 16 __path=path 17 print("Writing DB") 18 # 写数据库 19 # Tool同时继承了两个类 20 # class Tool(FileHandle,DBHandle): 21 class Tool(DBHandle,FileHandle): 22 def businessLogic(self): 23 print("In Tool") 24 tool=Tool() 25 tool.read("c:\\1.txt")
在第3行和第11行的FileHandle和DBHandle这两个类中,都定义了read和write这两个方法,且它们的参数相同。在第21行中的Tool类同时继承了这两个类,请注意第20行和第21行代码的差别,它们在继承两个父类时,次序有差别。
如果在多重继承时改变了继承的次序,那么通过第25行的输出语句,会发现前面的类方法会覆盖掉后面类的同名方法,比如当前打印时,会输出Reading DB。这是因为DBHandle类的read方法会覆盖掉FileHandle类的同名方法。
如果注释掉第21行的代码,同时去除调第20行的注释,就会发现输出的是Reading File。这是因为,在第20行的代码中,多重继承的次序是先FileHandle再DBHandle,于是FileHandle类的read方法会覆盖掉DBHandle类的同名方法。
如果我们的本意是通过多重继承同时在Tool引入读写文件和数据库的方法,但从效果上来看,由于两个父类中的方法同名了,出现方法的覆盖了,因此就和我们使用多重继承的本意不符了。
遇到这类情况,如果还要继续使用多重继承,那么就不得不改变其中一个类的方法名,但这样会增加代码的维护难度,与其这样,就不如不用多重继承,从根本上来避免这类困惑。
3.2.4 通过“组合”来避免多重继承
在多重继承的范例中,想要通过继承多个类在本类中引入多个功能。如果在这类应用场景中,子类和父类之间没有从属关系,就不该用继承,应该用“组合”,即在一个类中组合多个类,从而引入其他类提供的方法。在下面的CompositionDemo.py范例程序中来看一下“组合”多个类的用法。
1 # !/usr/bin/env python 2 # coding=utf-8 3 # 省略原来定义的FileHandle和DBHandle代码 4 # 改写后的Tool类 5 class Tool(object): 6 def __init__(self,fileHandle): 7 self.fileHandle=fileHandle 8 self.dbHandle=DBHandle() 9 def calDataInFile(self,path): 10 self.fileHandle.read(path) 11 # 统计文件里的数据 12 def calDataInDB(self,path): 13 self.dbHandle.read(path) 14 # 统计文件里的数据 15 # 使用类 16 fileHandle= FileHandle() 17 tool=Tool(fileHandle) 18 tool.calDataInFile("c:\\1.txt") # 输出Reading File 19 tool.calDataInDB("localhost:3309/myDB") # 输出Reading DB
在第5行定义的Tool的__init__方法中,通过两种方式引入了FileHandle和DBHandle这个类:第一种方式是在第6行中,通过输入参数传入FileHandle类型的对象;第二种方式是直接在第8行中生成DBHandle类型的对象。通过这两种方式在Tool类中“组合”两个工具类后,即可在第10行和第13行使用。
在第18行和第19行调用tool对象的两个方法时,就会发现没有再出现之前看到的“方法被覆盖”的现象,通过输出结果可以看到,在Tool中正确地调用到了读写文件和读写数据库的方法。