理解 Python 中的类特性

PythonBeginner
立即练习

介绍

在这个实验中,你将对 Python 中的关键面向对象编程 (OOP) 概念获得实践性的理解。我们将从封装 (encapsulation) 开始,学习如何将数据和方法捆绑在类中,并通过私有属性 (private attributes) 来控制对数据的访问。

接下来,你将实现继承 (inheritance) 来构建类之间的关系,这有助于代码重用。我们还将探索多态性 (polymorphism),它允许不同类的对象被统一对待。最后,你将使用 super() 方法来有效地调用父类 (parent class) 的方法,并练习多重继承 (multiple inheritance),以了解一个类如何可以从多个父类继承。

这是一个指导性实验 (Guided Lab),它提供分步说明以帮助你学习和实践。请仔细遵循说明来完成每个步骤并获得实践经验。历史数据显示,这是一个 初学者 级别的实验,完成率达到 100%。学习者对它的正面评价率达到了 100%

使用基础类探索封装

在这一步,我们将探索 封装 (encapsulation),这是一个核心的 OOP 原则。封装涉及将数据(属性)和操作该数据的方法捆绑到一个单一单元——类中。它还限制了对对象内部状态的直接访问,这有助于防止数据被意外修改。

在 Python 中,我们使用命名约定来表示一个属性是“私有的”。在属性前加上一个下划线(例如 _name)表示它仅供内部使用。虽然这并非严格强制执行,但它是一个开发者会遵守的强约定。

我们将通过创建两个独立的类 DogCat 开始,以观察它们的结构方式。

首先,在 WebIDE 左侧的文件浏览器中找到文件 animal_classes.py。打开它并添加以下 Python 代码。此代码定义了 Dog 类和 Cat 类,每个类都有一个私有 _name 属性以及与之交互的方法。

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## 单一下划线前缀表示一个“私有”属性。
        self._name = name

    ## 用于获取私有属性值的公共方法。
    def get_name(self):
        return self._name

    ## 用于设置私有属性值的公共方法。
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## 只有当脚本被直接执行时,此代码块才会运行。
if __name__ == "__main__":
    ## 创建 Dog 类的一个实例
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## 使用 setter 方法更改狗的名字
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## 创建 Cat 类的一个实例
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

添加代码后,保存文件。

现在,让我们运行脚本以查看封装的实际效果。在 WebIDE 中打开终端并执行以下命令:

python animal_classes.py

你将看到以下输出,它表明我们正在通过公共的 get_nameset_name 方法与私有的 _name 属性进行交互。

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

实现继承和多态

在上一个步骤中,你可能已经注意到 DogCat 类共享了许多相同的代码(__init__get_nameset_name)。这是一个使用 继承 (inheritance) 的绝佳机会。继承允许一个新类(子类或 subclass)从一个现有类(父类或 superclass)继承属性和方法,从而促进代码重用。

我们还将介绍 多态性 (polymorphism),它的意思是“多种形式”。在 OOP 中,它指的是不同类能够以各自独特的方式响应相同的(方法)调用。

让我们重构代码。我们将创建一个父类 Animal 来容纳通用代码,并让 DogCat 从它那里继承。say 方法在每个类中都不同,它将演示多态性。

打开 animal_classes.py 文件,并用以下代码替换其全部内容:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog 继承自 Animal
class Dog(Animal):
    ## 这会覆盖 Animal 类中的 say() 方法
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat 继承自 Animal
class Cat(Animal):
    ## 这也会覆盖 say() 方法
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

保存文件。注意 DogCat 类现在是如何变得更简洁的。它们从 Animal 继承了 __init__get_nameset_name 方法。它们各自提供了自己的 say 方法版本,这是方法重写 (method overriding) 的一个例子。

现在,从终端运行更新后的脚本:

python animal_classes.py

输出将是:

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

make_animal_speak 函数接收任何具有 say 方法的对象。即使我们向它传递不同类型的对象(AnimalDogCat),它也能正常工作,因为每个对象都知道如何以自己的方式执行 say 操作。这就是多态性的威力。

使用 super() 方法扩展功能

当子类覆盖了其父类的方法时,它有时需要扩展父类的方法,而不仅仅是替换它。super() 函数提供了一种在子类内部调用父类方法的方式。

这在 __init__ 方法中非常常见。子类通常需要在执行完父类的初始化步骤后,再执行自己特有的初始化步骤。

让我们向 DogCat 类添加唯一的属性。Dog 将有一个 age(年龄),Cat 将有一个 color(颜色)。我们将使用 super() 来确保父类 Animal__init__ 方法仍然被调用,以设置 _name 属性。

修改 animal_classes.py 文件,用以下代码替换其内容:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## 调用父类的 __init__ 方法来处理 'name' 属性
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## 我们也可以使用 super() 来调用父类的 say() 方法
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## 调用父类的 __init__ 方法
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

保存文件。在这个版本中,Dog.__init__Cat.__init__ 首先调用 super().__init__(name)。这会执行 Animal.__init__ 中的代码,从而设置 _name 属性。之后,它们继续执行自己特有的初始化(self.age = ageself.color = color)。

从终端运行脚本:

python animal_classes.py

输出展示了 __init__ 调用的链式过程以及扩展后的 say 方法:

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

练习多重继承

Python 允许一个类从多个父类继承。这被称为多重继承 (multiple inheritance)。它可以成为混合来自不同来源的功能的强大工具,但它也带来了复杂性,特别是在它们具有相同名称时,Python 如何决定使用哪个父类的方法。

这种搜索顺序被称为方法解析顺序 (Method Resolution Order, MRO)。Python 使用一种称为 C3 线性化 (C3 linearization) 的算法来确定一致且可预测的 MRO。

让我们用一个新的例子来探索这一点。从文件浏览器中打开 multiple_inheritance.py 文件,并添加以下代码:

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child 继承自 A,然后是 B
class Child_AB(ParentA, ParentB):
    pass

## Child 继承自 B,然后是 A
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## .mro() 方法显示了方法解析顺序
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

保存文件。在这里,Child_AB 继承自 ParentA,然后是 ParentBChild_BA 则以相反的顺序继承。当调用一个方法时,Python 会按照 MRO 指定的顺序进行查找。

从终端运行脚本:

python multiple_inheritance.py

你将看到以下输出:

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

从输出中,你可以观察到:

  • child1.speak() 调用了 ParentA 中的方法,因为 ParentAChild_AB 的 MRO 中排在前面。
  • child2.speak() 调用了 ParentB 中的方法,因为 ParentBChild_BA 的 MRO 中排在前面。
  • child2.common_method() 调用了在 Child_BA 中直接定义的那一个版本,因为 Python 首先在那里找到了它,然后再检查父类。

理解 MRO 对于预测多重继承场景下的行为至关重要。

总结

在这个实验中,你亲手实践了 Python 中面向对象编程的四个基本概念。

你从封装 (encapsulation) 开始,学习了通过约定俗成的方式使用私有属性来保护类数据,并提供公共方法进行访问。然后,你重构了代码以使用继承 (inheritance),创建了一个父类 Animal,以减少 DogCat 子类中的代码重复。

在实现继承的过程中,你看到了多态 (polymorphism) 的实际应用,因为 DogCat 对象对相同的 say() 方法调用做出了不同的响应。你学会了使用 super() 方法来调用和扩展父类的功能,尤其是在 __init__ 方法内部。最后,你探索了多重继承 (multiple inheritance) 以及方法解析顺序 (Method Resolution Order, MRO) 在决定调用哪个父类方法中的重要性。