继承

编写课程时,您不必总是从头开始。 如果您正在编写的类是您编写的另一个类的专用版本,则可以使用继承。 当一个类继承自另一个类时,它会继承第一个类的属性和方法。 原类称为父类,新类称为子类。 子类可以继承其父类的任何或所有属性和方法,但也可以自由定义自己的新属性和方法。

子类的 __init__() 方法

当您基于现有类编写新类时,您通常会希望从父类调用 __init__() 方法。 这将初始化在父类 __init__() 方法中定义的任何属性,并使它们在子类中可用。

例如,让我们为电动汽车建模。 电动汽车只是一种特定类型的汽车,因此我们可以将新的 ElectricCar 类基于我们之前编写的 Car 类。 然后我们只需为电动汽车特有的属性和行为编写代码。

让我们从制作一个简单版本的 ElectricCar 类开始,它完成了 Car 类所做的一切:

class Car: (1)
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles


class ElectricCar(Car): (2)
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year): (3)
        """Initialize attributes of the parent class."""
        super().__init__(make, model, year) (4)


my_leaf = ElectricCar('nissan', 'leaf', 2024) (5)
print(my_leaf.get_descriptive_name())

我们从汽车❶开始。 创建子类时,父类必须是当前文件的一部分,并且必须出现在文件中的子类之前。 然后我们定义子类 ElectricCar ❷。 父类的名称必须包含在子类定义的括号中。 __init__() 方法接收创建 Car 实例所需的信息。

super() 函数 ❹ 是一个特殊的函数,允许您从父类调用方法。 这一行告诉 Python 从 Car 调用 __init__() 方法,它为 ElectricCar 实例提供该方法中定义的所有属性。 super 这个名字来自于将父类称为超类,将子类称为子类的约定。

我们通过尝试使用我们在制造普通汽车时提供的相同类型的信息来制造电动汽车来测试继承是否正常工作。 我们创建了一个 ElectricCar 类的实例并将其分配给 my_leaf ❺。 这一行调用了 ElectricCar 中定义的 __init__() 方法,这反过来告诉 Python 调用父类 Car 中定义的 __init__() 方法。 我们提供参数“nissan”、“leaf”和 2024。

除了 __init__() 之外,还没有电动汽车特有的属性或方法。 在这一点上,我们只是确保电动汽车具有适当的汽车行为:

2024 Nissan Leaf

ElectricCar 实例就像 Car 的实例一样工作,所以现在我们可以开始定义特定于电动汽车的属性和方法。

给子类定义属性和方法

一旦有了从父类继承的子类,就可以添加任何必要的新属性和方法来区分子类和父类。

让我们添加一个特定于电动汽车(例如电池)的属性和一个报告此属性的方法。 我们将存储电池大小并编写一个打印电池描述的方法:

class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles


class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery_size = 40 (1)

    def describe_battery(self): (2)
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")


my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.describe_battery()
1 我们添加一个新属性 self.battery_size 并将其初始值设置为 40 。 此属性将与从 ElectricCar 类创建的所有实例相关联,但不会与 Car 的任何实例相关联。
2 我们还添加了一个名为 describe_battery() 的方法,用于打印有关电池的信息 。 当我们调用这个方法时,我们得到一个明确特定于电动汽车的描述:
2024 Nissan Leaf
This car has a 40-kWh battery.

扩展 ElectricCar 类的没有任何限制。 您可以根据需要添加任意数量的属性和方法来为电动汽车建模,达到您需要的任何准确度。 可以属于任何汽车的属性或方法,而不是特定于电动汽车的属性或方法,应该添加到 Car 类而不是 ElectricCar 类。 然后,任何使用 Car 类的人都可以使用该功能,而 ElectricCar 类将只包含特定于电动汽车的信息和行为的代码。

重写父类中的方法

您可以覆盖父类中不适合您尝试用子类建模的任何方法。 为此,您在子类中定义一个方法,其名称与您要在父类中覆盖的方法同名。 Python会忽略父类的方法,只关注你在子类中定义的方法。

假设类 Car 有一个名为 fill_gas_tank() 的方法。 此方法对纯电动汽车毫无意义,因此您可能希望重写此方法。 这是一种方法:

lass ElectricCar(Car):
    --snip--

    def fill_gas_tank(self):
        """Electric cars don't have gas tanks."""
        print("This car doesn't have a gas tank!")

现在,如果有人试图用电动汽车调用 fill_gas_tank(),Python 将忽略 Car 中的方法 fill_gas_tank() 并改为运行此代码。 当你使用继承时,你可以让你的子类保留你需要的东西并覆盖父类不需要的东西。

将实例用作属性

当用代码对现实世界中的某些东西进行建模时,您可能会发现您正在向类中添加越来越多的细节。 您会发现您的属性和方法列表越来越多,并且您的文件变得越来越长。 在这些情况下,您可能会认识到一个类的一部分可以写成一个单独的类。 你可以把你的大班分成小班一起工作; 这种方法称为组合。

例如,如果我们继续向 ElectricCar 类添加细节,我们可能会注意到我们正在添加许多特定于汽车电池的属性和方法。 当我们看到这种情况发生时,我们可以停止并将这些属性和方法移动到一个名为 Battery 的单独类中。 然后我们可以使用 Battery 实例作为 ElectricCar 类中的属性:

class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles


class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=40): (1)
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self): (2)
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")


class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery() (3)


my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
1 我们定义了一个名为 Battery 的新类,它不从任何其他类继承。 __init__() 方法除了 self 之外还有一个参数 battery_size 。 这是一个可选参数,如果未提供任何值,则将电池大小设置为 40。
2 describe_battery() 方法也已移至此类。
3 在 ElectricCar 类中,我们现在添加一个名为 self.battery 的属性。 这一行告诉 Python 创建一个新的 Battery 实例(默认大小为 40,因为我们没有指定值)并将该实例分配给属性 self.battery。 每次调用 __init__() 方法时都会发生这种情况; 任何 ElectricCar 实例现在都会自动创建一个 Battery 实例。

我们创建了一辆电动汽车并将其分配给变量 my_leaf。 当我们要描述电池时,我们需要通过汽车的电池属性来工作:

my_leaf.battery.describe_battery()

这一行告诉 Python 查看实例 my_leaf,找到它的电池属性,并调用与分配给该属性的电池实例关联的方法 describe_battery()。

输出与我们之前看到的相同:

2024 Nissan Leaf
This car has a 40-kWh battery.

这看起来需要做很多额外的工作,但现在我们可以尽可能详细地描述电池,而不会弄乱 ElectricCar 类。 让我们向 Battery 添加另一个方法,根据电池大小报告汽车的续航里程:

class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles


class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=40):
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 40:
            range = 150
        elif self.battery_size == 65:
            range = 225

        print(f"This car can go about {range} miles on a full charge.")


class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery()


my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
my_leaf.battery.get_range() (1)
1 新方法 get_range() 执行一些简单的分析。 如果电池容量为 40 kWh,get_range() 将范围设置为 150 英里,如果容量为 65 kWh,则将范围设置为 225 英里。 然后它报告这个值。 当我们要使用这个方法时,我们又要通过汽车的电池属性来调用它。

输出根据电池大小告诉我们汽车的续航里程:

2024 Nissan Leaf
This car has a 40-kWh battery.
This car can go about 150 miles on a full charge.

模拟实物

当您开始为更复杂的事物(例如电动汽车)建模时,您会遇到一些有趣的问题。 电动汽车的续航里程是电池的属性还是汽车的属性? 如果我们只描述一辆车,保持方法 get_range() 与 Battery 类的关联可能没问题。 但是,如果我们要描述制造商的整个汽车系列,我们可能希望将 get_range() 移动到 ElectricCar 类。 get_range() 方法在确定范围之前仍会检查电池大小,但它会报告特定于与其关联的汽车类型的范围。 或者,我们可以保持 get_range() 方法与电池的关联,但向其传递一个参数,例如 car_model。 然后 get_range() 方法将根据电池大小和汽车型号报告一个范围。

这将带您进入程序员成长过程中的一个有趣点。 当你为这些问题而苦苦挣扎时,你是在更高的逻辑层次上思考,而不是专注于语法层次。 您不是在考虑 Python,而是在考虑如何用代码表示现实世界。 当你达到这一点时,你会意识到对现实世界情况进行建模通常没有正确或错误的方法。 有些方法比其他方法更有效,但需要实践才能找到最有效的表示形式。 如果您的代码如您所愿地工作,那么您做得很好! 如果您发现自己正在拆分类并使用不同的方法多次重写它们,请不要气馁。 为了编写准确、高效的代码,每个人都会经历这个过程。

自己试试
9-6.Ice Cream Stand

冰淇淋摊是一种特殊的餐厅。 编写一个名为 IceCreamStand 的类,它继承您在练习 9-1(第 162 页)或练习 9-4(第 166 页)中编写的 Restaurant 类。 该课程的任何一个版本都可以使用; 只选择你更喜欢的那个。 添加一个名为 flavors 的属性,用于存储冰淇淋口味列表。 编写一个显示这些口味的方法。 创建 IceCreamStand 的实例,并调用此方法。

9-7.Admin

管理员是一种特殊的用户。 编写一个名为 Admin 的类,它继承您在练习 9-3(第 162 页)或练习 9-5(第 167 页)中编写的 User 类。 添加一个属性 privileges,它存储一个字符串列表,如“可以添加帖子”、“可以删除帖子”、“可以禁止用户”等。 编写一个名为 show_privileges() 的方法,列出管理员的权限集。 创建 Admin 的实例,并调用您的方法。

9-8.Privileges

编写一个单独的权限类。 该类应该有一个属性 privileges,它存储练习 9-7 中描述的字符串列表。 将 show_privileges() 方法移动到此类。 在 Admin 类中创建一个 Privileges 实例作为属性。 创建一个新的 Admin 实例并使用您的方法显示其权限。

9-9.Battery Upgrade

使用本节中的最终版本的 electric_car.py。 向 Battery 类添加一个名为 upgrade_battery() 的方法。 此方法应检查电池大小并将容量设置为 65(如果尚未设置)。 做一个默认电池大小的电动车,调用一次get_range(),升级电池后再调用第二次get_range()。 您应该会看到汽车的续航里程有所增加。