编写类时,并非总是要从空白开始。如果你要编写的类是另一个现成类的特殊版本,可使用继承。一个类继承另一个类时,它将自动获得另一个类的所有属性和方法;原有的类称为父类,而新类称为子类。子类继承了其父类的所有属性和方法,同时还可以定义自己的属性和方法。

9.3.1 子类的方法init()

创建子类的实例时,Python首先需要完成的任务是给父类的所有属性赋值。为此,子类的方法init()需要父类施以援手。

例如,下面来模拟电动汽车。电动汽车是一种特殊的汽车,因此我们可以在前面创建的Car类的基础上创建新类ElectricCar,这样我们就只需为电动汽车特有的属性和行为编写代码。

下面来创建一个简单的ElectricCar类版本,它具备Car类的所有功能:

  1. class ElectricCar(Car):
  2. """电动汽车的独特之处"""
  3. def __init__(self, make, model, year):
  4. """初始化父类的属性"""
  5. super().__init__(make, model, year)
  6. my_byd = ElectricCar('比亚迪', '汉EV', 2020)
  7. print(my_byd.get_descriptive_name())

首先是Car类的代码(见本章9.2)。创建子类时,父类必须包含在当前文件中,且位于子类前面。在第一行处,我们定义了子类ElectricCar。定义子类时,必须在括号内指定父类的名称。方法init()接受创建Car实例所需的信息。

第五行处的super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让Python调用ElectricCar的父类的方法init(),让ElectricCar实例包含父类的所有属性。父类也称为超类(superclass),名称super因此而得名。

为测试继承是否能够正确地发挥作用,我们尝试创建一辆电动汽车,但提供的信息与创建普通汽车时相同。在第七行处,我们创建ElectricCar类的一个实例,并将其存储在变量mybyd中。这行代码调用ElectricCar类中定义的方法init(),后者让Python调用父类Car中定义的方法_init()。我们提供了实参’比亚迪’、’汉EV’和2020。

除方法init()外,电动汽车没有其他特有的属性和方法。当前,我们只想确认电动汽车具备普通汽车的行为:


2020 比亚迪 汉Ev


ElectricCar实例的行为与Car实例一样,现在可以开始定义电动汽车特有的属性和方法了。

9.3.2 给子类定义属性和方法

让一个类继承另一个类后,可添加区分子类和父类所需的新属性和方法。

下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法。我们将存储电瓶容量,并编写一个打印电瓶描述的方法:

  1. class ElectricCar(Car):
  2. """电动汽车的独特之处"""
  3. def __init__(self, make, model, year):
  4. """初始化父类的属性"""
  5. super().__init__(make, model, year)
  6. self.battery_size = 70
  7. def describe_battery(self):
  8. """打印一条描述电瓶容量的消息"""
  9. print("这辆车还有" + str(self.battery_size) + "-kWh的电量剩余。")
  10. my_byd = ElectricCar('比亚迪', '汉EV', 2020)
  11. print(my_byd.get_descriptive_name())
  12. my_byd.describe_battery()

在第六行处,我们添加了新属性self.battery_size,并设置其初始值(如70)。根据ElectricCar 类创建的所有实例都将包含这个属性,但所有Car实例都不包含它。在第八行处,我们还添加了一个名为describe_battery()的方法,它打印有关电瓶的信息。我们调用这个方法时,将看到一条电动汽车特有的描述:


2020 比亚迪 汉Ev
这辆车还有70-kWh的电量剩余。


对于ElectricCar类的特殊化程度没有任何限制。模拟电动汽车时,你可以根据所需的准确程度添加任意数量的属性和方法。如果一个属性或方法是任何汽车都有的,而不是电动汽车特有的,就应将其加入到Car类而不是ElectricCar类中。这样,使用Car类的人将获得相应的功能,而ElectricCar类只包含处理电动汽车特有属性和行为的代码。

9.3.3 重写父类的方法

对于父类的方法,只要它不符合子类模拟的实物的行为,都可对其进行重写。为此,可在子类中定义一个这样的方法,即它与要重写的父类方法同名。这样,Python将不会考虑这个父类方法,而只关注你在子类中定义的相应方法。

假设Car类有一个名为fill_gas_tank()的方法,它对全电动汽车来说毫无意义,因此你可能想重写它。下面演示了一种重写方式:

class ElectricCar(Car): 
    """电动汽车的独特之处""" 
    def __init__(self, make, model, year): 
        """初始化父类的属性""" 
        super().__init__(make, model, year) 
        self.battery_size = 70

    def describe_battery(self): 
        """打印一条描述电瓶容量的消息""" 
        print("这辆车还有" + str(self.battery_size) + "-kWh的电量剩余。")

    def fill_gas_tank(self): 
        """电动汽车没有油箱""" 
        print("这是辆电车,没有油箱!")

my_byd = ElectricCar('比亚迪', '汉EV', 2020) 
print(my_byd.get_descriptive_name())
my_byd.fill_gas_tank()

现在,如果有人对电动汽车调用方法fill_gas_tank(),Python将忽略Car类中的方法fill_gas_tank(),转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。

9.3.4 将实例用作属性

使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分作为一个独立的类提取出来。你可以将大型类拆分成多个协同工作的小类。

例如,不断给ElectricCar类添加细节时,我们可能会发现其中包含很多专门针对汽车电瓶的属性和方法。在这种情况下,我们可将这些属性和方法提取出来,放到另一个名为Battery的类中,并将一个Battery实例用作ElectricCar类的一个属性:

class Battery:
    def __init__(self, battery_size=70):
        self.battery_size = battery_size

    def describe_battery(self): 
        """打印一条描述电瓶容量的消息""" 
        print("这辆车还有" + str(self.battery_size) + "-kWh的电量剩余。")


class ElectricCar(Car): 
    """电动汽车的独特之处""" 
    def __init__(self, make, model, year): 
        """初始化父类的属性""" 
        super().__init__(make, model, year) 
        self.battery = Battery()

    def describe_battery(self): 
        """打印一条描述电瓶容量的消息""" 
        self.battery.describe_battery()

    def fill_gas_tank(self): 
        """电动汽车没有油箱""" 
        print("这是辆电车,没有油箱!")

my_byd = ElectricCar('比亚迪', '汉EV', 2020) 
print(my_byd.get_descriptive_name())
my_byd.describe_battery()

在第一行处,我们定义了一个名为Battery的新类,它没有继承任何类。第二行处的方法init()除self外,还有另一个形参battery_size。这个形参是可选的:如果没有给它提供值,电瓶容量将被设置为70。方法describe_battery()也移到了这个类中。

在ElectricCar类中,我们添加了一个名为self.battery的属性。这行代码让Python创建一个新的Battery实例(由于没有指定尺寸,因此为默认值70),并将该实例存储在属性self.battery中。每当方法init()被调用时,都将执行该操作;因此现在每个ElectricCar实例都包含一个自动创建的Battery实例。

我们创建一辆电动汽车,并将其存储在变量my_byd中。要描述电瓶时,需要使用电动汽车的属性battery:

self.battery.describe_battery()

这行代码让Python在实例my_byd中查找属性battery,并对存储在该属性中的Battery实例调用方法describe_battery()。
输出与我们前面看到的相同:


2020 比亚迪 汉Ev
这辆车还有70-kWh的电量剩余。


这看似做了很多额外的工作,但现在我们想多详细地描述电瓶都可以,且不会导致ElectricCar类混乱不堪。下面再给Battery类添加一个方法,它根据电瓶容量报告汽车的续航里程:

class Battery:
    def __init__(self, battery_size=70):
        self.battery_size = battery_size

    def describe_battery(self): 
        """打印一条描述电瓶容量的消息""" 
        print("这辆车还有" + str(self.battery_size) + "-kWh的电量剩余。")

    def get_range(self): 
        """打印一条消息,指出电瓶的续航里程""" 
        if self.battery_size == 70: 
            range = 240 
        elif self.battery_size == 85: 
            range = 270 
        message = f"这辆车在充满电的情况下可以开{str(range)}公里。"
        print(message)


class ElectricCar(Car): 
    """电动汽车的独特之处""" 
    def __init__(self, make, model, year): 
        """初始化父类的属性""" 
        super().__init__(make, model, year) 
        self.battery = Battery()

    def describe_battery(self): 
        """打印一条描述电瓶容量的消息""" 
        self.battery.describe_battery()

    def fill_gas_tank(self): 
        """电动汽车没有油箱""" 
        print("这是辆电车,没有油箱!")

my_byd = ElectricCar('比亚迪', '汉EV', 2020) 
print(my_byd.get_descriptive_name())
my_byd.describe_battery()
my_byd.battery.get_range()

第九行处新增的方法get_range()做了一些简单的分析:如果电瓶的容量为70kWh,它就将续航里程设置为240公里;如果容量为85kWh,就将续航里程设置为270公里,然后报告这个值。为使用这个方法,我们也通过汽车的属性battery来调用它。

输出指出了汽车的续航里程(这取决于电瓶的容量):


2020 比亚迪 汉Ev
这辆车还有70-kWh的电量剩余。
这辆车在充满电的情况下可以开240公里。


9.3.5 模拟实物

模拟较复杂的物件(如电动汽车)时,需要解决一些有趣的问题。续航里程是电瓶的属性还是汽车的属性呢?如果我们只需描述一辆汽车,那么将方法get_range()放在Battery类中也许是合适的;但如果要描述一家汽车制造商的整个产品线,也许应该将方法get_range()移到ElectricCar类中。在这种情况下,get_range()依然根据电瓶容量来确定续航里程,但报告的是一款汽车的续航里程。我们也可以这样做:将方法get_range()还留在Battery类中,但向它传递一个参数,如car_model;在这种情况下,方法get_range()将根据电瓶容量和汽车型号报告续航里程。

这让你进入了程序员的另一个境界:解决上述问题时,你从较高的逻辑层面(而不是语法层面)考虑;你考虑的不是Python,而是如何使用代码来表示实物。到达这种境界后,你经常会发现,现实世界的建模方法并没有对错之分。有些方法的效率更高,但要找出效率最高的表示法,需要经过一定的实践。只要代码像你希望的那样运行,就说明你做得很好!即便你发现自己不得不多次尝试使用不同的方法来重写类,也不必气馁;要编写出高效、准确的代码,都得经过这样的过程。