9.3.2 Python 2.7 中的继承
在Python 2.7中,继承语法稍有不同,ElectricCar类的定义类似于下面这样:
class Car(object): def __init__(self, make, model, year): --snip-- class ElectricCar(Car): def __init__(self, make, model, year): super(ElectricCar, self).__init__(make, model, year) --snip--
函数super()需要两个实参:子类名和对象self。为帮助Python将父类和子类关联起来,这些 实参必不可少。另外,在Python 2.7中使用继承时,务必在定义父类时在括号内指定object。
9.3.3 给子类定义属性和方法
让一个类继承另一个类后,可添加区分子类和父类所需的新属性和方法。
下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法。我们将存储电 瓶容量,并编写一个打印电瓶描述的方法:
class Car(): --snip-- class ElectricCar(Car): """Represent aspects of a car, specific to electric vehicles.""" def __init__(self, make, model, year): """ 电动汽车的独特之处 初始化父类的属性,再初始化电动汽车特有的属性 """ super().__init__(make, model, year) 1 self.battery_size = 70 2 def describe_battery(self): """打印一条描述电瓶容量的消息""" print("This car has a " + str(self.battery_size) + "-kWh battery.") my_tesla = ElectricCar('tesla', 'model s', 2016) print(my_tesla.get_descriptive_name()) my_tesla.describe_battery()
在1处,我们添加了新属性self.battery_size,并设置其初始值(如70)。根据ElectricCar 类 创建的所有实例都将包含这个属性,但所有Car实例都不包含它。在2处,我们还添加了一个名 为describe_battery()的方法,它打印有关电瓶的信息。我们调用这个方法时,将看到一条电动 汽车特有的描述:
2016 Tesla Model S This car has a 70-kWh battery.
对于ElectricCar类的特殊化程度没有任何限制
def ElectricCar(Car): --snip-- def fill_gas_tank(): """电动汽车没有油箱""" print("This car doesn't need a gas tank!")
。模拟电动汽车时,你可以根据所需的准确 程度添加任意数量的属性和方法。如果一个属性或方法是任何汽车都有的,而不是电动汽车特有 的,就应将其加入到Car类而不是ElectricCar类中。这样,使用Car类的人将获得相应的功能,而 ElectricCar类只包含处理电动汽车特有属性和行为的代码。
9.3.4 重写父类的方法
对于父类的方法,只要它不符合子类模拟的实物的行为,都可对其进行重写。为此,可在子 类中定义一个这样的方法,即它与要重写的父类方法同名。这样,Python将不会考虑这个父类方 法,而只关注你在子类中定义的相应方法。
假设Car类有一个名为fill_gas_tank()的方法,它对全电动汽车来说毫无意义,因此你可能 想重写它。下面演示了一种重写方式:
def ElectricCar(Car): --snip-- def fill_gas_tank(): """电动汽车没有油箱""" print("This car doesn't need a gas tank!")
现在,如果有人对电动汽车调用方法fill_gas_tank(),Python将忽略Car类中的方法 fill_gas_tank(),转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华, 并剔除不需要的糟粕。
9.3.5 将实例用作属性
使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文 件都越来越长。在这种情况下,可能需要将类的一部分作为一个独立的类提取出来。你可以将大 型类拆分成多个协同工作的小类。
例如,不断给ElectricCar类添加细节时,我们可能会发现其中包含很多专门针对汽车电瓶 的属性和方法。在这种情况下,我们可将这些属性和方法提取出来,放到另一个名为Battery的 类中,并将一个Battery实例用作ElectricCar类的一个属性:
class Car(): --snip-- 1 class Battery(): """一次模拟电动汽车电瓶的简单尝试""" 2 def __init__(self, battery_size=70): """初始化电瓶的属性""" self.battery_size = battery_size 3 def describe_battery(self): """打印一条描述电瓶容量的消息""" print("This car has a " + str(self.battery_size) + "-kWh battery.") class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """ 初始化父类的属性,再初始化电动汽车特有的属性 """ super().__init__(make, model, year) 4 self.battery = Battery() my_tesla = ElectricCar('tesla', 'model s', 2016) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery()
在1处,我们定义了一个名为Battery的新类,它没有继承任何类。2处的方法__init__()除 self外,还有另一个形参battery_size。这个形参是可选的:如果没有给它提供值,电瓶容量将 被设置为70。方法describe_battery()也移到了这个类中(见3)。
在ElectricCar类中,我们添加了一个名为self.battery的属性(见4)。这行代码让Python 创建一个新的Battery实例(由于没有指定尺寸,因此为默认值70),并将该实例存储在属性 self.battery中。每当方法__init__()被调用时,都将执行该操作;因此现在每个ElectricCar实 例都包含一个自动创建的Battery实例。
我们创建一辆电动汽车,并将其存储在变量my_tesla中。要描述电瓶时,需要使用电动汽车 的属性battery:
my_tesla.battery.describe_battery()
这行代码让Python在实例my_tesla中查找属性battery,并对存储在该属性中的Battery实例 调用方法describe_battery()。 输出与我们前面看到的相同:
2016 Tesla Model S This car has a 70-kWh battery.
这看似做了很多额外的工作,但现在我们想多详细地描述电瓶都可以,且不会导致ElectricCar 类混乱不堪。下面再给Battery类添加一个方法,它根据电瓶容量报告汽车的续航里程:
class Car(): --snip-- class Battery(): --snip-- 1 def get_range(self): """打印一条消息,指出电瓶的续航里程""" if self.battery_size == 70: range = 240 elif self.battery_size == 85: range = 270 message = "This car can go approximately " + str(range) message += " miles on a full charge." print(message) class ElectricCar(Car): --snip-- my_tesla = ElectricCar('tesla', 'model s', 2016) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery() 2 my_tesla.battery.get_range()
1 处新增的方法get_range()做了一些简单的分析:如果电瓶的容量为70kWh,它就将续航里 程设置为240英里;如果容量为85kWh,就将续航里程设置为270英里,然后报告这个值。为使用 这个方法,我们也通过汽车的属性battery来调用它(见2)。 输出指出了汽车的续航里程(这取决于电瓶的容量):
2016 Tesla Model S This car has a 70-kWh battery. This car can go approximately 240 miles on a full charge.
9.3.6 模拟实物
模拟较复杂的物件(如电动汽车)时,需要解决一些有趣的问题。续航里程是电瓶的属性还 是汽车的属性呢?如果我们只需描述一辆汽车,那么将方法get_range()放在Battery类中也许是合 适的;但如果要描述一家汽车制造商的整个产品线,也许应该将方法get_range()移到ElectricCar 类中。在这种情况下,get_range()依然根据电瓶容量来确定续航里程,但报告的是一款汽车的续 航里程。我们也可以这样做:将方法get_range()还留在Battery类中,但向它传递一个参数,如 car_model;在这种情况下,方法get_range()将根据电瓶容量和汽车型号报告续航里程。
这让你进入了程序员的另一个境界:解决上述问题时,你从较高的逻辑层面(而不是语法层 面)考虑;你考虑的不是Python,而是如何使用代码来表示实物。到达这种境界后,你经常会发 现,现实世界的建模方法并没有对错之分。有些方法的效率更高,但要找出效率最高的表示法,需要经过一定的实践。只要代码像你希望的那样运行,就说明你做得很好!即便你发现自己不得 不多次尝试使用不同的方法来重写类,也不必气馁;要编写出高效、准确的代码,都得经过这样 的过程。
9.4 导入类
随着你不断地给类添加功能,文件可能变得很长,即便你妥善地使用了继承亦如此。为遵循 Python的总体理念,应让文件尽可能整洁。为在这方面提供帮助,Python允许你将类存储在模块 中,然后在主程序中导入所需的模块。
9.4.1 导入单个类
下面来创建一个只包含Car类的模块。这让我们面临一个微妙的命名问题:在本章中,已经 有一个名为car.py的文件,但这个模块也应命名为car.py,因为它包含表示汽车的代码。我们将这 样解决这个命名问题:将Car类存储在一个名为car.py的模块中,该模块将覆盖前面使用的文件car.py。从现在开始,使用该模块的程序都必须使用更具体的文件名,如my_car.py。下面是模块 car.py,其中只包含Car类的代码:
car.py 1 """一个可用于表示汽车的类""" class Car(): """一次模拟汽车的简单尝试""" def __init__(self, make, model, year): """初始化描述汽车的属性""" self.make = make self.model = model self.year = year self.odometer_reading = 0 def get_descriptive_name(self): """返回整洁的描述性名称""" long_name = str(self.year) + ' ' + self.make + ' ' + self.model return long_name.title() def read_odometer(self): """打印一条消息,指出汽车的里程""" print("This car has " + str(self.odometer_reading) + " miles on it.") def update_odometer(self, mileage): """ 将里程表读数设置为指定的值 拒绝将里程表往回拨 """ if mileage >= self.odometer_reading: self.odometer_reading = mileage else: print("You can't roll back an odometer!") def increment_odometer(self, miles): """将里程表读数增加指定的量""" self.odometer_reading += miles
在处,我们包含了一个模块级文档字符串,对该模块的内容做了简要的描述。你应为自己 创建的每个模块都编写文档字符串。 下面来创建另一个文件——my_car.py,在其中导入Car类并创建其实例:
my_car.py 1 from car import Car my_new_car = Car('audi', 'a4', 2016) print(my_new_car.get_descriptive_name()) my_new_car.odometer_reading = 23 my_new_car.read_odometer()
1处的import语句让Python打开模块car,并导入其中的Car类。这样我们就可以使用Car类了, 就像它是在这个文件中定义的一样。输出与我们在前面看到的一样:
2016 Audi A4 This car has 23 miles on it.
导入类是一种有效的编程方式。如果在这个程序中包含了整个Car类,它该有多长呀!通过 将这个类移到一个模块中,并导入该模块,你依然可以使用其所有功能,但主程序文件变得整洁 而易于阅读了。这还能让你将大部分逻辑存储在独立的文件中;确定类像你希望的那样工作后, 你就可以不管这些文件,而专注于主程序的高级逻辑了。