基于股票大数据分析的Python入门实战(视频教学版)
上QQ阅读APP看书,第一时间看更新

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中正确地调用到了读写文件和读写数据库的方法。