Python 类的使用

Hakuna 2025-02-07 2025-02-07 4951 字 25 minutes OOP Python

大家好,欢迎来到《基于 Python 的面向对象编程》系列的第 篇文章!在这个系列里,我们一起了解面向对象编程的基本概念,看看 Python 是怎么实现类的,还有怎么把这些知识用到实际开发中。希望大家一起轻松学习!

相关文章链接


在面向对象编程中,我们有几个比较重要的工作:开发者首先根据需求定义相关的类,这些类充当现实世界实体或概念的蓝图或模板,包括相关的一组属性(变量)和方法(函数)。其次,使用者通过实例化(调用类的构造函数,即 __init__ 方法),从这些类创建对象。此时,每个对象都会继承已定义类的结构和行为,从而,使用者可以通过公共接口(即类所提供的公开方法)来完成相关的工作。

从开发者视角来看,在面向对象编程中,一般应该遵循四大基本原则:封装、继承、多态和抽象。封装使得对象可以将其状态(属性)和实现细节隐藏在内部,只通过公共接口与外界交互。继承允许类间共享和扩展功能,使得代码更加模块化并减少重复。多态性提供了接口的多样化实现,使得同一操作可适用于不同的对象,具体行为取决于对象的实际类型。抽象则帮助开发者集中于高层设计,忽略底层的具体细节,通过抽象类或接口形成一套共有的操作框架。在整个开发过程中,单元测试和调试确保了代码的可靠性和功能的正确实现,而设计模式则提供了解决常见问题的有效方法。

flowchart TB start(((开始设计))) --> A(定义类) A -->|封装属性和方法| B(创建对象) B -->|实例化对象| C(调用方法) A -->|继承其他类| D[[使用继承]] D --> C A -->|实现多态性| E[[使用多态]] E --> C A -->|定义抽象基类| F[[使用抽象]] F --> C C -->|对象之间的交互| G(对象交互) G --> H(维护和扩展) H -->|反馈更新| I(更新类定义) I -->|改进封装| A I -->|优化继承关系| D I -->|增强多态表现| E I -->|扩展抽象类| F H --> J{{单元测试}} J --> K([调试]) K -->|确认功能与需求对齐| H A --> L{应用设计模式} L -->|优化类设计| A L -->|简化解决方案| I style A fill:#FF7F50 style B fill:#FF7F50 style C fill:#FF7F50 style G fill:#FF7F50 style H fill:#FF7F50 style I fill:#FF7F50 style K fill:#c8c8c8 style J fill:#c8c8c8 style L fill:#F7EEDD

前面我们已经介绍了类定义的基本情况,后面将重点聚焦于类的使用,包括创建对象、继承和多态。

创建对象

在面向对象编程中,实例化是将类模板转换为具体对象的过程,这是面向对象程序运行的基础。通过实例化,我们可以从抽象的类定义创建具体的对象实例,使得相同的类模板可以被用来创建多个独立的对象,每个对象都有其独立的属性和状态,但共享相同的方法。这些实例在内存中占有具体的空间,并拥有类定义的结构和行为。

下图展示了同一个类模板(Character)可以被用来创建多个独立的对象(Object1Object2Object3),且每个对象都有其独立的属性和状态,但共享相同的方法。

classDiagram class Character { -name: string -health: int -experience: int } Character : +__init__(name, health, experience) Character : +gain_experience(points) %% 实例化对象 Object1 "1" *-- "1" Character : Instance Object2 "1" *-- "1" Character : Instance Object3 "1" *-- "1" Character : Instance %% 对象的独立属性值 Object1 : name = "安其拉" Object1 : health = 100 Object1 : experience = 0 Object2 : name = "孙悟空" Object2 : health = 90 Object2 : experience = 10 Object3 : name = "亚瑟" Object3 : health = 80 Object3 : experience = 20 note "每个对象具有自己的独立属性值,\n显示对象的独立状态。这表明不同实例的独立性。"

实例化的核心在于创建一个类的具体实例,这个过程涉及到类构造函数的调用。如前所述,在 Python 中,构造函数通过 __init__ 方法实现。当咱们创建一个类的新实例时,Python 会自动调用这个类的 __init__ 方法。实例化一个类的具体机制可以参考特殊方法的定义中关于自动调用机制的解释。

创建一个类的实例非常简单,只需要调用类名并传入构造函数所需要的参数,比如使用表达式 new_object = MyClass()。下面展示了一个具体的示例:

# 创建 Character 类的一个实例
my_character = Character("Arthur", 100, 0)

在这个例子中,我们创建了一个名为 my_character 的新对象,并传递了三个参数给 __init__ 方法:"Arthur" 作为角色的名字,100 作为初始健康值,0 作为初始经验值。

一旦实例化完成,就可以使用这个对象的属性和方法了:

# 访问对象属性
print(my_character.name)       # 输出: Arthur
print(my_character.health)     # 输出: 100
print(my_character.experience) # 输出: 0

# 调用 gain_experience 方法来更新经验值
my_character.gain_experience(50) # 输出:Arthur gains 50 experience points.

除了以上传统的实例化方式,在面向对象编程中还有比较高级的实例化技术,或者称为实例化模式。这方面需要大家了解面向对象编程中的设计模式概念,比如工厂模式和单例模式。设计模式理念可以参考 Gang of Four 的 Design Patterns: Elements of Reusable Object-Oriented Software

继承

类继承是面向对象编程中的一个核心概念,它允许新的类(称为派生类或子类)继承现有类(称为基类或父类)的属性和方法,而无需重新编写相同的代码。此外,子类可以扩展或修改从父类继承的行为,使得子类对象不仅拥有父类的行为,还可以有新的或改变的行为。继承机制是实现软件重用的一种重要方式。通过继承,开发者可以构建出一个层次化的类结构,使得程序的结构更加清晰,同时增强了代码的可维护性。

比如前面的角色扮演游戏,其中包括不同类型的角色(骑士、法师和弓箭手)。这些角色都具有一些共同的特征和行为:每个角色都有生命值(HP)、攻击力(ATK)和防御力(DEF)。此外,每个角色都能进行攻击和防御等基本行为。在这种情况下,可以通过创建一个公共的父类来封装这些共同的属性和方法,然后让所有具体的角色类继承这个父类。

object

在 Python 中,所有的类默认继承自一个名为 object 的内置基类,这是所有类的最终父类。这个基类是 Python 类层次结构的根,提供了一些基本的方法和属性,使得所有对象都具有一些共通的行为。我们可以在 Python 解释器中通过 dir(object) 的方式查看该基类的所有方法:

>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

其官方实现可以查看 object.c

通过继承自 object 类,所有的 Python 类都自动获得这些方法。我们可以根据需要重写这些方法来改变其类的行为。例如,通常会重写 __init__ 方法来初始化新对象,重写 __str____repr__ 以提供更具可读性或调试友好性的字符串表示。我们来看一个简单的例子:

class MyClass:
    pass

# MyClass 自动继承自 object
print(isinstance(MyClass(), object))    # 输出: True
print(isinstance(MyClass(), MyClass))   # 输出: True
print(issubclass(MyClass, object))      # 输出:True  

这里,我们定义了一个什么也不干(没有自定义新属性及其相应的方法)的 MyClass 类。从上面的输出结果可以看出:

  • MyClass() 实例是 object 类的一个实例,这表明所有在 Python 中定义的类,无论是否显式继承其他类,都默认继承自 object 类。
  • MyClass() 实例也是它自身类 MyClass 的实例,这是面向对象编程中的一个基本概念:对象总是属于它的直接类或其父类。
  • issubclass(MyClass, object) 返回 True,确认了 MyClassobject 的子类。这强调了所有自定义类都继承自 Python 的 object 基类。

以下示例展示了 MyClass 实例继承自 object 类的一些默认方法,并演示了它们的行为:

obj1 = MyClass()
print(str(obj1)) # 输出类似于: <__main__.MyClass object at 0x000001641905A240>
print(repr(obj1)) # 输出类似于: <__main__.MyClass object at 0x000001641905A240>

obj2 = MyClass()
print(obj1 == obj2)  # 默认情况下,这将返回 False,因为它们是不同的实例(内存地址不同)。
print(hash(obj1), hash(obj2))  # 返回不同的哈希值,通常是基于它们的 id。输出类似于: 133108140574 133108140565

这些示例表明,即使是简单的类定义也继承了许多基本行为,它们为类的实例提供了标准的接口。尽管 object 类为我们提供了这些基础方法的默认实现,但往往我们需要根据具体需求重写这些方法,以实现更复杂的功能和行为。这不仅体现了继承机制的力量,也强调了面向对象设计中定制行为以满足特定需求的重要性。

单继承

在 Python 中,类的继承允许我们定义一个类来继承另一个已存在的类的属性和方法。继承语法很简单,主要包括在类定义时指定要继承的父类。这是 Python 中实现继承的基本语法结构:

class BaseClass:
    # 父类中的方法和属性
    pass

class DerivedClass(BaseClass):
    # 派生类中的方法和属性
    pass

这里的 BaseClass 是父类(也称作基类),而 DerivedClass 是从 BaseClass 派生的子类。子类继承了父类的所有方法和属性,可以使用这些方法和属性就像它们是在子类中定义的一样。由于 DerivedClass(BaseClass) 中,只有 BaseClass 一个父类,因此,这样的继承方式也被称为单继承(Single inheritance),即一个子类只继承自一个父类。

假设我们想要在 Character 类的基础上创建几个具体的角色类,如 Knight, MageArcher。我们可以通过如下方式实现:

class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health
    
    def display_info(self):
        print(f"{self.name}, Health: {self.health}")

class Knight(Character):
    def __init__(self, name, health, armor):
        super().__init__(name, health)
        self.armor = armor

    def display_info(self):
        super().display_info()
        print(f"Armor: {self.armor}")

class Mage(Character):
    def __init__(self, name, health, mana):
        super().__init__(name, health)
        self.mana = mana

    def display_info(self):
        super().display_info()
        print(f"Mana: {self.mana}")

在这个例子中,KnightMage 是从 Character 类派生的。它们通过调用 super().__init__(name, health) 继承了 Character 类的构造方法,这样可以保证基类的初始化逻辑被正确执行。此外,派生类还扩展了自己特有的属性如 armormana,并且重写了 display_info() 方法来展示额外的信息。

使用 super() 函数在继承中是一个非常重要的实践。当一个类继承自另一个类时,子类通常需要初始化它从父类继承的部分。super() 函数用于调用父类(基类)的方法,确保了基类的初始化代码可以执行。比如该示例中的 Knight 类。当创建一个 Knight 类的实例时,首先会调用它的构造器 __init__(self, name, health, armor)。此构造器接收三个参数:namehealth,和 armor。由于在 Knight 类的构造器中,super().__init__(name, health) 的存在,super() 函数会返回对父类 Character 的临时对象引用,然后调用其 __init__() 方法。父类 Character 的构造器接收两个参数:namehealth。这两个属性在父类中被初始化。这意味着每个 Knight 实例都会在其父类部分具有 namehealth 属性,并且这些属性在父类的构造器中被设置。父类的构造器执行完成后,控制权返回到 Knight 类的构造器。接下来的行 self.armor = armor 初始化 Knight 类特有的属性 armor。这是在 Knight 实例中添加的新属性,而不影响父类 CharacterKnight 类的构造器完成执行,此时,一个具有 namehealth,和 armor 属性的 Knight 对象被成功创建。这个初始化过程保证了 Knight 类的对象不仅拥有其独特的属性(如 armor),而且还继承了 Character 类的所有功能和属性(如 namehealth)。

此外,super() 函数的使用还保证了多重继承中能够使用 C3 线性化算法(一个确定方法解析顺序的算法)来确保所有基类都被适当地初始化。super() 调用不仅会查找直接父类,还会按照方法解析顺序(Method Resolution Order, MRO)处理多继承中可能的复杂关系。这里出现了几个比较重要的概念:多重继承、C3 线性化算法、方法解析顺序。这些概念的理解对深入了解 Python 类的继承有着重要的作用,下面我们将逐一探讨这些概念,以确保对 Python 类继承机制有一个全面的理解。

多重继承

多重继承允许一个类同时继承多个父类。在 Python 中,实现多重继承的语法非常直观。咱们只需要在定义类时,在类名后的括号中列出所有要继承的父类,各个父类之间用逗号分隔。下面是一个简单的示例:

class Base1:
    def method1(self):
        print("Method from Base1")

class Base2:
    def method2(self):
        print("Method from Base2")

# 多重继承
class Derived(Base1, Base2):
    def method_derived(self):
        print("Method from Derived")

# 创建 Derived 类的实例
instance = Derived()
instance.method1()  # 调用来自 Base1 的方法
instance.method2()  # 调用来自 Base2 的方法
instance.method_derived()  # 调用来自 Derived 的方法

在这个例子中,Derived 类同时继承了 Base1Base2。因此,Derived 类的实例可以访问所有父类中定义的方法。我们可以通过如下方式查看 Derived 类的实例访问父类的顺序:

print(Derived.__mro__)  #输出:(<class '__main__.Derived'>, <class '__main__.Base1'>, <class '__main__.Base2'>, <class 'object'>)

输出的结果的意思是,如何 Derived 类的实例需要访问某个属性或者方法时,它会首先在 Derived 类中查找相应的属性,如果有,则直接使用 Derived 类中定义的该属性或方法。如果在 Derived 类中找不到,解释器接着会查看 Base1 类,如果在 Base1 类中找到了所需的属性或方法,就会使用它。如果 Base1 也没有,则继续在 Base2 类中查找,依此类推,直到找到为止或最终到达基类 object

比如,当我们尝试调用 instance.method2() 时,解释器首先会在 Derived 类中查找名为 method2 的方法。由于 Derived 类中没有定义这个方法,解释器会按照 __mro__ 所列的顺序继续查找。接下来,它会检查 Base1 类,但该类同样没有定义 method2。因此,解释器继续在 Base2 类中查找,并成功找到了 method2 方法,随后执行该方法并输出 Method from Base2

如果尝试调用 instance.method3(),过程会类似,但因为 Base2 类中没有定义该方法,解释器将继续搜索。由于在继承链的任何类中都没有定义 method3,最终搜索到基类 object。在 object 类中仍未找到 method3,此时解释器将抛出 AttributeError 异常,提示 'Derived' object has no attribute 'method3',说明没有找到对应的方法。

这一过程展示了 Python 是如何通过 MRO 确定属性或方法的查找顺序以及如何处理未找到的属性或方法。那么这个 MRO 顺序是如何确定的呢?这就需要了解 C3 线性化算法。同学们感兴趣可以参考The Python 2.3 Method Resolution OrderC3 linearizationPython’s super() considered super!等资料。

参考资料