2024-04-16    2024-08-27     42165 字  85 分钟

想象你走进一家星巴克咖啡店。这里有许多不同种类的咖啡杯,每个咖啡杯都具有不同的特性。例如:

  • 容量:有小杯(Tall)、中杯(Grande)、大杯(Venti)等。
  • 颜色:一些是经典的绿色和白色星巴克标志,其他可能是特别版或季节性设计。
  • 材质:有些是一次性的纸杯,有些则是可以多次使用的陶瓷杯或不锈钢杯。
  • 形状:有些杯子是标准的圆柱形,而有些可能有独特的设计,如节日特别款或限量版。

用另外一个词来描述这些咖啡杯的特性就是“属性”。这些咖啡杯有其主要用途,即盛装咖啡供顾客饮用,这就是它们的“方法”。在不同的时间,顾客可能会根据个人喜好或需要选择不同的咖啡杯。比如,一个需要长时间工作的顾客可能选择一个大杯的拿铁(Latte)以维持更长时间的精神状态;另一个顾客可能只是想要一小杯意式浓缩咖啡(Espresso)快速提神。

在这个例子中,我们可以看到每个咖啡杯都是一个具体的“对象(Object)”,具备独特的属性和功能。这些咖啡杯虽然多样,但都属于更广泛的“咖啡杯”类别,每个都为顾客的不同需求服务。

我们再来看一个例子。大家都很喜欢游戏,可能都玩过角色扮演类的游戏(RPG)。假设有一个名为“英雄与怪兽”的角色扮演游戏。在这个游戏中,玩家可以选择扮演不同的角色,例如骑士、法师或弓箭手,这些角色的目的是去打游戏世界中充满的各种怪兽,获取掉落的装备、提升自己的等级。我们知道,不管是玩家角色还是怪兽,都应该具有一些共有的特性,比如生命值(HP)、魔法值(MP)、攻击力(ATK)和防御力(DEF)等等,同时也会有一些共有的技能,比如攻击(attack)、防御(defend)和使用技能(use_skill)。当然,如果游戏中所有角色都具有相同的属性和方法,那么对于玩家来说就没有吸引力了。所以,一般情况下,玩家角色和怪兽都会有自身的一些固有属性以及技能。比如说,对于玩家角色,骑士可能具有更高的防御力,法师具有更强的魔法攻击,而弓箭手可能攻击速度更快;骑士的“盾墙”提高防御,法师的“火球术”提供强大的范围攻击,而弓箭手的“瞄准射击”将造成重大单体伤害。对于怪兽,龙可能会喷火,而哥布林可能会进行群攻;每种怪兽都有自己的攻击方式和生存策略。那么我们可以用如下的图来表示各个角色本身的特性和技能,以及它们之间的存在的关系,

classDiagram
    class 角色 {
      +生命值(HP)
      +魔法值(MP)
      +攻击力(ATK)
      +防御力(DEF)
      +攻击(attack)()
      +防御(defend)()
      +使用技能(use_skill)()
    }
    class 骑士 {
      +更高的防御力
      +盾墙(提高防御)()
    }
    class 法师 {
      +更强的魔法攻击
      +火球术(范围攻击)()
    }
    class 弓箭手 {
      +攻击速度更快
      +瞄准射击(单体重伤)()
    }
    class 怪兽 {
      +特有的属性
      +特有攻击方式()
      +生存策略()
    }
    class 地图 {
      +地形类型
      +障碍物
      +可能的事件()
    }
    角色 <|-- 骑士
    角色 <|-- 法师
    角色 <|-- 弓箭手
    角色 <|-- 怪兽
  

从以上描述可以看出,游戏中的每个角色和物体都可以是一个对象,具有自己的属性和行为。同时,它们之间也可以存在一种“继承”的关系,如具体的玩家角色(骑士、法师、弓箭手)和怪兽都可以拥有角色的属性和技能。游戏环境,如地图,与角色有着明显的区别,因此,可以单独作为一个对象。类似的,为了增加游戏的趣味性,我们还可以设计多张地图,每个地图上都可以生成不同类型的怪兽。而这些子地图除了本身的特性和功能外,也可以继承于地图里的共有的属性和功能。

有了这样的概念,我们就可以回到编程上来了。现代编程中有一种面向对象编程(Object-Oriented Programming,OOP,或者称为对象导向编程)的思想。该思想以对象为中心,强调数据(对象)和操作数据的方法(函数)的组合,提供了一种自然而直观的方式来映射现实世界的实体和它们之间的关系。在面向对象编程中,类和对象的概念类似于现实世界中的“种类”和“个体”。类定义了一组属性(即数据)和操作这些数据的方法(即算法),而对象则是根据这些类创建的实例。

借助于面向对象编程思想,我们可以将游戏中的每个角色和物体设计为一个独立的类,每个类具有自己的属性和方法。这不仅使代码结构更加清晰,也便于后期维护和扩展。例如,如果我们想在角色扮演游戏中增加一个新的玩家角色或怪兽类型,我们只需要定义一个新的类,继承自基本的角色类,并为其添加独特的属性和行为就可以了。

此外,面向对象编程也让数据结构和算法的实现更为直观。数据结构在面向对象编程中可以被看作是具有各种操作的对象集合,如列表、栈、队列等。算法则是这些结构上的一系列操作,用于实现搜索、排序和其他复杂的数据处理功能。通过类的继承和多态性,我们可以设计出更高效、更易于管理和维护的代码,从而提高软件的质量和性能。

因此,掌握面向对象编程的基本原则和技巧,对于理解和使用现代软件技术至关重要。无论是开发复杂的游戏,还是构建企业级应用程序,面向对象的方法都为程序员提供了强大的工具,以模拟复杂系统中的各种实体和动态交互。通过面向对象的视角审视数据结构与算法,可以帮助我们更好地理解这些概念的实际应用和潜在价值。

下面我们将基于 Python 语言对面向对象编程中的类和对象的定义、继承、封装和多态性等关键概念以及面向对象编程在描述数据结构与算法中的应用进行较为详细探讨。

面向对象编程的基本概念

类和对象

在面向对象编程中,类(Class)和对象(Object)是核心概念。类可以被理解为一个蓝图(blueprint)或模板(template),它描述了一组具有共同特征和行为的对象的结构和行为。类定义了对象的数据以及可以对这些数据执行的操作。具体来说,类中定义的数据称为属性,而操作数据的函数称为方法。

对象则是根据类的定义创建的实例(Instance)。每个对象都拥有类中定义的属性和方法,但各个对象的属性值可以各不相同。例如,在一个游戏中,可以有一个名为 Character 的类,它定义了属性如生命值(health)、力量(strength)和防御力(defense),以及方法如攻击或防御。基于 Character 类,可以创建多个对象(如骑士、法师、弓箭手),每个对象都具有自己独特的属性值和实现同一方法的方式。

通过使用类和对象,面向对象编程允许我们创建模块化的代码,其中每个模块都包含执行特定任务所需的所有数据和方法。

属性和方法

在面向对象编程中,属性(Attributes)和方法(Methods)是构成类的基本元素。属性是类中定义的变量,它们代表了对象的状态或特征。方法则是类中定义的函数,它们描述了对象可以执行的行为。

属性(Attributes)是附属于对象的数据点,它们定义了对象的特性。例如,在游戏角色类 Character 中,属性可能包括生命值、力量和防御力。这些属性存储了与每个角色对象相关的数据,如骑士可能拥有高防御力而法师拥有高魔法值。

方法(Methods)则是定义在类中的操作,它们通过使用或修改对象的属性来实现特定的功能。方法通常执行计算,操作属性的值,或者实现对象之间的交互。例如,在 Character 类中,一个名为 attack 的方法可能会减少另一个角色的 health 属性,模拟战斗中的攻击行为。另一个名为 defend 的方法可能会临时提高角色的 defense 属性,表示角色正在防御。

属性和方法共同定义了一个类的行为和数据结构,使得对象不仅仅是数据的集合,而是可以执行具体操作的独立实体。在面向对象的设计中,通过精心设计类的属性和方法,可以创建出灵活、可重用且易于维护的软件组件。这种封装的特性,即将数据(属性)和操作数据的逻辑(方法)捆绑在一起,是面向对象编程的核心优势之一。

构造器和析构器

在面向对象编程中,构造器(Constructors)和析构器(Destructors)是两种特殊类型的方法,它们在对象的生命周期中扮演着关键角色。构造器用于初始化新创建的对象,而析构器则在对象被销毁前执行清理工作。

构造器(Constructors)通常在创建对象时自动调用,以便设置对象的初始状态。构造器的主要任务是初始化对象拥有的属性,为它们赋予合适的初始值。在 Python 中,构造器方法通常被称为 __init__。例如,在一个游戏角色类 Character 中,构造器可能会接受参数如健康值、力量和防御力,并将它们赋值给对象的相应属性:

class Character:
    def __init__(self, health, strength, defense):
        self.health = health
        self.strength = strength
        self.defense = defense

此代码段定义了 Character 类的构造器,它初始化了三个属性:healthstrengthdefense。当创建 Character 类的新实例时,必须提供这些参数。

析构器(Destructors)是在对象即将被销毁时自动调用的方法,用于执行必要的清理操作。这可能包括释放资源、关闭文件句柄或断开网络连接等。在 Python 中,析构器方法通常被称为 __del__。虽然 Python 有自动垃圾收集机制来处理内存管理,但在某些情况下,显式定义析构器来清理资源仍然是必要的:

class Character:
    def __del__(self):
        print("Character has been destroyed.")

这个析构器实现很简单,仅仅是在对象销毁时打印一条消息。在实际应用中,析构器的使用应该非常谨慎,因为很多时候,我们并不能预测 Python 的垃圾收集机制的执行时间。

继承、封装与多态

继承(Inheritance)、封装(Encapsulation)和多态(Polymorphism)是面向对象编程的核心特性,因为有了这些特性及其结合,基于面向对象思想的编程方式可以很大程度上帮助我们降低代码的复杂性,从而增强代码的可重用性和可维护性。

继承是一种使一个类(称为子类或派生类)能够继承另一个类(称为父类或基类)的属性和方法的机制。子类继承了父类的所有特征,并可以添加新的特征或修改继承来的行为以满足新的需求。例如,如果有一个基类 Vehicle,它有通用属性如车轮子数和方法如启动和停止,一个子类 Car 可以继承 Vehicle,并添加特有属性如车门数和方法如倒车等。通过使用继承,我们可以创建一个层次结构的类,使得代码更易于管理和扩展,同时避免了代码的重复。

封装是一种设计策略,用来将数据(属性)和功能(方法)捆绑在一起作为一个独立的单元或对象,并对对象的信息进行隐藏和保护。这意味着类的内部工作方式可以独立于外部界面,外部代码不能直接访问对象的内部结构。封装的一个主要好处是它减少了系统各部分之间的相互依赖性,增加了代码的安全性。在某些编程语言中(如 C++),可以通过使用访问控制关键字,如私有(private)、受保护(protected)和公开(public),控制类成员的可访问性。Python 采用了更为灵活(也有人认为是更不正式)的方式来表示私有或受保护的属性和方法。默认情况下,Python 中的类的所有成员都是公开的,可以从类的外部直接访问。如果想要表示某个变量或方法只有类及其子类可以访问,可以通过在其变量名或方法名前加一个下划线(_)。若要创建一个私有成员,可以在名称前添加两个下划线(__)。

多态是面向对象编程中的一个重要特性,它允许使用统一的接口表示不同的基础形态(数据类型)。这意味着可以通过同一接口调用不同类的方法,而实际执行的是与调用对象的实际类型相对应的方法。换句话说,多态让我们可以用同一个接口来调用不同类的同名方法,而程序在运行时会自动选择合适的方法来执行,这个选择基于对象的实际类型。这就像是不同的人在听到“开始”这个指令时,根据各自的职责开始不同的任务:警察可能开始巡逻,教师可能开始上课,学生们开始学习。

在编程中,多态通常通过“重载(Overloading)”(多个同名函数但参数不同)和“重写(Overriding)”(子类重新定义继承自父类的行为)来实现。

关于多态,我们来看一个具体的例子。假设我们有一个基类叫做 Animal,它有一个方法叫做 speak()。然后,我们有两个继承自 Animal 的子类,分别是 DogCat

# 定义一个基类 Animal
class Animal:
    def speak(self):
        raise NotImplementedError("Subclasses must implement this method")

# 定义 Dog 类继承自 Animal
class Dog(Animal):
    def speak(self):
        return "Bark!"

# 定义 Cat 类继承自 Animal
class Cat(Animal):
    def speak(self):
        return "Meow!"

# 函数,用于调用任何 Animal 类型的 speak 方法
def make_animal_speak(animal):
    return animal.speak()

# 创建 Dog 和 Cat 的实例
my_dog = Dog()
my_cat = Cat()

# 测试这些实例
dog_sound = make_animal_speak(my_dog)  # 输出 "Bark!"
cat_sound = make_animal_speak(my_cat)  # 输出 "Meow!"

在该程序中, Animal 类提供了 speak() 方法的基本形式。Dog 类重写了 speak() 方法,以输出 "Bark!"Cat 类也重写了 speak() 方法,以输出 "Meow!"。可以看出,无论传递给 make_animal_speak() 函数的是 Dog 对象还是 Cat 对象,相应的 speak() 方法都能正确调用,展示出不同的行为。这说明了多态的核心优势:能够使用通用的接口来处理不同类型的对象,而具体的行为则由对象的实际类型决定。

Python 中的类实现

Python 是一种动态的、高级的编程语言,其设计哲学强调代码的可读性和简洁的语法。这些特性使得 Python 非常适合面向对象编程。Python 中的一切都可以被视为对象,从最基本的数据类型(如整数和字符串)到函数、类、甚至是模块。每个对象不仅包含数据(即属性),还包含绑定到这些数据上的操作(即方法)。这种设计极大地提升了语言的灵活性,允许开发者以统一的方式处理各种数据类型。

在 Python 中,类体的结构和基本语法是构建面向对象程序的核心。了解如何正确地定义和使用类是利用 Python 进行面向对象编程的关键。下面我们将详细地介绍类的定义、类体结构、基本语法及其组成部分。

类的声明

在 Python 中,使用关键字 class 来定义一个类。类名通常遵循大驼峰命名法(CapWords convention),即每个单词的首字母大写,不使用下划线。类定义的基本语法如下:

class ClassName:
    # 类体定义

class 这个关键字标志着接下来的代码块将定义一个新的类。类定义以类名开始,类名后面紧跟一个冒号 :,宣告了类体的开始。类体是一个缩进的代码块,包含了类的方法和属性定义。

Python 中类本身也是一个对象。每当我们使用 class 关键字定义新类时,Python 实际上是在创建一个对象。这个对象(类)自身具有创建新对象(实例)的能力,因此,类本质上是对象的“工厂”。这是 Python 中一切皆对象理念的一个重要体现。

类对象允许我们进行如下操作:

  • 属性访问:访问类变量或通过类方法定义的计算属性。
  • 函数调用:调用类方法。
  • 实例化:通过调用类(如调用函数那样)来创建新的实例。

自定义的类在 Python 中的地位与内置类型如 intstr 相同。正如可以创建 intstr 的实例一样,我们也可以创建由自定义类定义的类型的实例。这种设计将自定义类型提升到与 Python 的内置类型同等的层次,使得使用自定义类型就像使用内置类型一样。

类体的定义

除了类的声明(class MyClass:),类体是定义类的行为和状态的核心部分,它包含了数据属性(变量)和函数(方法)。

  • 属性(Attributes)主要包括两类:
    • 类变量:在类中定义,为类的所有实例所共享。用于存储与类相关的数据,而非单个实例相关的数据。
    • 实例变量:通常在构造器(__init__ 方法)中通过 self 参数定义。实例变量是每个类的实例独立拥有的数据。
  • 方法(Methods)大致可以分为以下四类:
    • 实例方法:定义操作实例数据的函数。第一个参数总是 self,它代表类的一个实例本身。
    • 类方法:使用 @classmethod 装饰器定义,其第一个参数是 cls,代表类本身。类方法可以访问类变量,但不能访问任何单独实例的数据。
    • 静态方法:使用 @staticmethod 装饰器定义,不接受 selfcls 参数。静态方法不能访问类或实例的任何属性,主要用于放置不需要访问任何实例或类数据的工具性函数。
    • 特殊方法(Magic Methods):如 __init__(构造器)、__str__(转换为字符串)、__repr__(官方字符串表示)等。这些方法有特殊的名称和用途,是由 Python 的语法和行为规则预定义的。

我们仍然以游戏角色为例,来进一步解释和理解类体的内容:

class Character:
    level_up_experience = 100  # 类变量,所有实例共享,设定升级所需的经验值

    def __init__(self, name, health, experience):
        self.name = name           # 实例变量,每个角色的名字
        self.health = health       # 实例变量,每个角色的健康点数
        self.experience = experience  # 实例变量,每个角色的经验值

    def attack(self, other):
        """实例方法,角色可以攻击其他角色"""
        if other.health > 0:
            damage = 10
            other.health -= damage
            print(f"{self.name} attacks {other.name} causing {damage} damage points.")
            self.gain_experience(10)

    @classmethod
    def change_level_up_experience(cls, new_experience):
        """类方法,允许修改升级所需的经验值"""
        cls.level_up_experience = new_experience
        print(f"Level up experience is now set to {new_experience}.")

    @staticmethod
    def game_info():
        """静态方法,提供游戏的一些基本信息,不依赖于类或实例的状态"""
        return "This is a role-playing game."

    def gain_experience(self, points):
        """实例方法,角色通过活动获得经验值"""
        self.experience += points
        print(f"{self.name} gains {points} experience points.")
        if self.experience >= Character.level_up_experience:
            self.level_up()

    def level_up(self):
        """实例方法,角色升级"""
        self.health += 50
        print(f"{self.name} levels up! Health increased to {self.health}.")

    def __str__(self):
        """特殊方法,定义角色的字符串表示形式"""
        return f"Character({self.name}, Health: {self.health}, Experience: {self.experience})"

下图描述了 Charactor 类的组成:

classDiagram
    class Character {
        -level_up_experience int
        -name string
        -health int
        -experience int
        +__init__(name, health, experience)
        +attack(other)
        +gain_experience(points)
        +level_up()
        +__str__() string
        +game_info() string
        +change_level_up_experience(new_experience)
    }

    note "角色类定义说明"
    note for Character "level_up_experience:类变量,在所有实例之间共享的数据。\nname,health,experience:实例变量,每个实例独立拥有的数据。\nattack(),gain_experience(),level_up():实例方法,操作实例数据的方法。\nchange_level_up_experience():类方法,操作类级数据的方法,不依赖于特定的实例。\ngame_info():静态方法,不需要类或实例数据的独立函数。\n__init__(),__str__():特殊方法,提供了类的特殊操作和与Python内建操作的接口。"
  

Character 类的类体定义基本上包含了 Python 类体定义的常见组成部分,如类变量、实例变量、实例方法、类方法、静态方法和特殊方法。每个组成部分都扮演着特定的角色,共同定义了一个游戏中角色的行为和状态。

类变量的定义

level_up_experienceCharacter 类中是类变量,用于定义所有角色升级所需的经验值。在 Python 中,类变量是定义在类的所有实例之间共享的变量。这些变量通常用于存储与类相关的数据,而非单个实例独有的数据。因为它们的行为与实例变量有显著不同,尤其是在涉及继承和方法访问这些变量时,因此,有必要进一步了解类变量的定义规则。

类变量应在类定义的顶层定义,即在任何方法之外。其赋值与在 Python 中定义全局变量或局部变量的方式相似,采用标准赋值语法,即使用赋值符号(=)进行赋值。这样,类变量属于类的命名空间,而不是某个具体实例的命名空间。这与实例变量不同,实例变量在类的构造器(如 __init__ 方法)中定义,使用 self 关键字并且每个实例维护自己的一份拷贝。修改一个实例的实例变量不会影响到其他实例。类变量的共享特性决定了类变量的值在一个实例中被修改时,这个修改对所有其他实例都将可见。这种行为使类变量适用于跨多个实例需要保持一致的数据。访问方式上,类变量可以通过类本身直接访问,也可以通过类的任何实例访问。然而,推荐使用直接访问的方式,即以类名称来访问类变量(如,Character.level_up_experience),以区分类变量和实例变量,并避免在继承中可能出现的混淆。由于类变量在类的命名空间中,因此类变量可以在类中的任何方法(实例方法、类方法或静态方法)中使用。其实,在类方法中访问类变量尤为常见,因为类方法通常处理与类状态相关的操作。例如,在该示例中,类方法 change_level_up_experience 修改了类变量 level_up_experience 的值,达到更新升级所需要的经验值的目的。

实例变量的定义

namehealth 以及 experienceCharacter 类中均是实例变量。name 变量用于定义角色的名字,为每个角色实例所独有。health 变量用于定义角色的健康点数,代表角色的当前健康状态。而 experience 用于定义角色的经验值,当经验值达到一定数值时,角色将升级。

如前所述,这些实例变量在类的构造器( __init__ 方法)中初始化,它们为每个角色实例提供了独立的状态数据。注意,在构造器参数中有一个比较特殊的参数,即 self 关键字。self 在 Python 中代表当前对象的实例,是实例方法(包括构造器)的第一个参数。通过使用 self,我们可以在类的其他方法中引用实例变量。比如说:

class Character:
    def __init__(self, name, health, experience):
        self.name = name           # 实例变量,存储角色名
        self.health = health       # 实例变量,存储角色健康点数
        self.experience = experience  # 实例变量,存储角色经验值

    def display_info(self):
        # 方法中访问实例变量
        print(f"Name: {self.name}, Health: {self.health}, Experience: {self.experience}")

这里的 display_info 方法中,我们通过 self.nameself.healthself.experience 分别访问了对应的实例变量。假设我们有一个 Character 类的实例 hero:

hero = Character("Hero", 100, 50)

那么我们在使用 display_info 方法时(hero.display_info()),将输出: Name: Hero, Health: 100, Experience: 50

除了传统的位置参数(name, health, experience)和 self 这个比较特殊的参数外,由于类中的方法实质上就是函数,因此,我们可以在其中设置和使用不同类型的参数,包括关键字参数和默认参数及可变参数。其效果与普通函数中的参数效果一致。具体可以参考理解 Python 函数不同类型的参数及其使用。下面分别列举了使用默认参数、关键字参数以及可变参数的三个例子:

# 默认参数的使用意味着当创建类的实例时,可以不用提供所有参数,只提供那些需要与默认值不同的参数。
class Character:
    def __init__(self, name, health=100, experience=0):
        self.name = name
        self.health = health
        self.experience = experience

# 可以只提供名字,健康值和经验值将使用默认值
hero = Character("Hero")
# 关键字参数在构造器中的使用允许在不修改构造器定义的情况下传递额外的数据。这些数据可以用于动态设置实例属性或进行其他初始化任务。
class Character:
    def __init__(self, name, **kwargs):
        self.name = name
        self.health = kwargs.get('health', 100)
        self.experience = kwargs.get('experience', 0)
        self.inventory = kwargs.get('inventory', [])

# 创建角色时可以传递任何额外的参数
hero = Character("Hero", health=150, experience=10, inventory=["sword", "shield"])
# 在不确定会传递多少参数,或者想要接受任意数量参数时可以使用可变参数。
class Character:
    def __init__(self, name, *args):
        self.name = name
        self.abilities = list(args)  # 将传入的能力转换为列表

# 创建一个角色,可以列举出任意多的能力
hero = Character("Hero", "strength", "agility", "wisdom")

继承、封装与多态中,我们介绍过 Python 的类成员默认是公开的,但也提供了一些约定的方式实现实例变量和方法的访问控制。比如运用一个下划线(_)前缀来表示变量或方法是受保护的;双下划线(__)前缀用来声明私有成员。这里,我们再举两个例子,以便加深印象。

class Character:
    def __init__(self, name):
        self._name = name  # 受保护的实例变量

    def _display_name(self):
        # 受保护的方法
        return f"This character is named {self._name}."

# 外部仍然能访问
hero = Character("Hero")
print(hero._name)  # 打印出:Hero
print(hero._display_name()) # 打印出:This character is named Hero.
class Character:
    def __init__(self, name):
        self.__name = name  # 私有实例变量

    def __display_name(self):
        # 私有方法
        return f"This character is named {self.__name}."

# 外部尝试访问或修改会失败
hero = Character("Hero")
# print(hero.__name)  # 报错,无法访问
# print(hero.__display_name())  # 报错,无法访问
print(hero._Character__name) # 打印出: Hero
print(hero._Character__display_name()) # 打印出:This character is named Hero.

可以看出,运用 _ 来表示受保护的变量真的是一种约定,没有任何的约束,外部仍然可以轻松地直接访问。相对而言,使用 __ 前缀来声明私有成员时,约束性稍微强了一些。我们不能直接访问该类的私有成员。如果,实在是有必要,我们只能通过在变量或者方法名前加上以 _ 开头的类名的方式访问,如 hero._Character__namehero._Character__display_name()

实例方法的定义

接着我们介绍类体中的实例方法的定义。下面这段代码呈现的是 Character 类的实例方法部分,以便查看。

class Character:
    def attack(self, other):
        """实例方法,角色可以攻击其他角色"""
        if other.health > 0:
            damage = 10
            other.health -= damage
            print(f"{self.name} attacks {other.name} causing {damage} damage points.")
            self.gain_experience(10)

    def gain_experience(self, points):
        """实例方法,角色通过活动获得经验值"""
        self.experience += points
        print(f"{self.name} gains {points} experience points.")
        if self.experience >= Character.level_up_experience:
            self.level_up()

    def level_up(self):
        """实例方法,角色升级"""
        self.health += 50
        print(f"{self.name} levels up! Health increased to {self.health}.")

Character 类中,attack()gain_experience()level_up() 均是实例方法。在 Python 中,实例方法本质上是定义在类中的函数,它们用于操作或者访问实例的属性(即对象的状态)。实例方法在定义时,必须至少包括一个参数,通常是 self,它代表类的实例本身。此外,实例方法可以接收额外的参数,参数类型可以是位置参数、关键字参数以及可变参数。这些参数在方法被调用时传递。例如, attack() 方法中,除了self这个必须的参数外,还接受了 other 这个表示另一个角色实例的参数。attack() 方法允许一个角色实例(self)攻击另一个角色实例(other)。该方法首先检查被攻击者的健康值(health)是否大于 0,如果是,则执行攻击,减少被攻击者的健康值,并增加攻击者的经验值(通过调用 gain_experience() 方法)。期间,该方法使用 print() 函数打印了攻击动作的详情,包括攻击者和被攻击者的名字,以及造成的伤害点数。

方法体中可以通过 self.attribute_name 的方式访问或修改实例的属性,如 attack() 方法中的 self.name 访问了 Character 类的一个实例的角色名称;或者通过 self.method_name() 的方式调用同一个类的其他方法,如 gain_experience() 方法中,self.level_up() 调用了 level_up() 方法,以此实现增加角色的健康值(health),并给出新的健康值的目的。

值得说明的是,该示例中的实例方法展示了一种常规的定义方式。其实,Python 允许实例方法访问和调用类的各种组件,这包括访问和修改类变量,调用类方法和静态方法,以及访问私有和受保护的属性和方法。这使得 Python 的面向对象编程时非常灵活。

类方法的定义

类方法在 Python 中是通过在方法定义前使用 @classmethod 装饰器来标识的。这种方法不是针对类的实例,而是针对类本身进行操作。它们常用于需要访问或修改类级别属性(即类变量)的场景。比如 Character 类中的 change_level_up_experience() 方法:

class Character:
    level_up_experience = 100  # 类变量,所有实例共享,设定升级所需的经验值

    @classmethod
    def change_level_up_experience(cls, new_experience):
        """类方法,允许修改升级所需的经验值"""
        cls.level_up_experience = new_experience
        print(f"Level up experience is now set to {new_experience}.")

从该类方法的定义方式上看,我们可以注意到如下几个关键元素:第一,@classmethod 装饰器。这个装饰器是必需的,它告诉 Python 这个方法是一个类方法。第二,第一个参数 cls。类方法的第一个参数通常命名为 cls,这代表了类本身。这个参数由 Python 在调用类方法时自动提供,类似于实例方法中的 self 参数,但 cls 指向的是类而不是实例。

此外,类方法的方法体中也可以通过 cls.class_variable_name 或者 cls.class_method() 的访问和修改类变量或者调用其他的类方法。我们来看一下下面这个示例:

class Character:
    total_characters = 0  # 类变量,记录游戏中角色的总数
    level_up_experience = 100  # 类变量,设定升级所需的经验值

    def __init__(self, name, experience=0):
        self.name = name
        self.experience = experience
        self.level = 1
        Character.total_characters += 1  # 每创建一个角色,总数加一

    @classmethod
    def change_level_up_experience(cls, new_experience):
        """类方法,修改所有角色的升级经验阈值"""
        cls.level_up_experience = new_experience
        print(f"Updated level up experience to {new_experience} for all characters.")

    @classmethod
    def reset_game(cls):
        """类方法,重置游戏状态,包括角色总数和默认经验值"""
        cls.total_characters = 0
        cls.level_up_experience = 100
        print("Game has been reset. All character counts and experiences are set to default.")

    @classmethod
    def check_and_reset_game(cls):
        """检查是否需要重置游戏,并调用重置"""
        if cls.total_characters > 10:  # 假设超过10个角色就重置游戏
            cls.reset_game()

    def gain_experience(self, points):
        """实例方法,增加经验并判断是否升级"""
        self.experience += points
        if self.experience >= Character.level_up_experience:
            self.level_up()

    def level_up(self):
        """实例方法,处理角色升级逻辑"""
        self.level += 1
        print(f"{self.name} has leveled up to level {self.level}!")

# 使用示例
for i in range(12):  # 创建多个角色
    char = Character(f"Hero{i}")
Character.check_and_reset_game()  # 检查并可能重置游戏

在这个例子中,我们增加了一个表示总的角色数量的类变量(total_characters),以及可以重置游戏状态的类方法(reset_game())和检查是否需要重置游戏的类方法(check_and_reset_game())。在 change_level_up_experience 类方法中,我们使用 cls.level_up_experience 的方式来访问类变量 level_up_experience。通过 level_up_experience = new_experience 来更新升级所需的经验值。在 reset_game 中,我们通过 cls.total_characters = 0 以及 cls.level_up_experience = 100 的方式重置了总的角色数量和升级所需的经验值。在 check_and_reset_game() 类方法中,我们首先检查了是否需要重置游戏的标准(假设超过 10 个角色就重置游戏),如果满足条件,就通过 cls.reset_game() 方式调用类方法 reset_game() 来重置游戏,将总的角色数量和升级所需的经验值均设定为初始值。

静态方法的定义

在 Python 中,静态方法通过 @staticmethod 装饰器来定义,后面跟着一个普通的函数定义。注意,静态方法既不需要类 (cls) 也不需要实例 (self) 的引用来调用,它们就像是类中的普通函数。静态方法的主要用途是在它们的方法体内执行一些功能,而这些功能与类的具体实例或者类本身的状态无关。这使得静态方法成为实现与类功能相关但独立于类实例的方法的理想选择。比如 Character 类中的 game_info() 方法:

class Character:
    @staticmethod
    def game_info():
        """静态方法,提供游戏的一些基本信息,不依赖于类或实例的状态"""
        return "This is a role-playing game."

在使用场景上,静态方法通常用于以下情景:

  • 用于大型类中,为类提供相关功能,但又不需要类或实例的状态(即,属性或方法)时。
  • 作为工具函数,用于执行某些任务,比如格式化数据,处理输入等,这些不直接与类的任何实例的内部状态相关的操作。

相较于其他方法,静态方法还有一个明显的特征。在使用静态方法时,我们并不需要实例化对象。比如说在 Character 类这个例子中,我们可以通过 print(Character.game_info()) 直接打印出该游戏的信息(This is a role-playing game.),而无需创建 Character 类实例。这种机制直接的好处是,可以节省内存的使用。

为了加深影响,我们可以再看一个例子:

class MathUtility:
    @staticmethod
    def add_numbers(a, b):
        return a + b

    @staticmethod
    def multiply_numbers(a, b):
        return a * b

MathUtility 类中定义了两个静态方法:add_numbers()multiply_numbers()。我们可以按照如下方式使用这两个静态方法:

# 使用静态方法
result_add = MathUtility.add_numbers(10, 5)  # 无需创建类实例
result_multiply = MathUtility.multiply_numbers(10, 5)

print("Addition:", result_add)  # 输出 15
print("Multiplication:", result_multiply)  # 输出 50

如果 add_numbers()multiply_numbers() 为普通的实例方法:

class MathUtility:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add_numbers(self):
        return self.a + self.b

    def multiply_numbers(self):
        return self.a * self.b

那么,我们要使用这两个方法时,只能先对 MathUtility 类进行实例化,然后调用这两方法:

# 创建 MathUtility 类的实例
math_util = MathUtility(10, 5)

# 使用实例方法
result_add = math_util.add_numbers()  # 调用实例方法
result_multiply = math_util.multiply_numbers()

print("Addition:", result_add)  # 输出 15
print("Multiplication:", result_multiply)  # 输出 50

在此情景下,如果仍然按照静态方法的使用方式,比如:

result_add = MathUtility.add_numbers(10, 5)  # 没有创建类实例

通常会抛出以下错误:

TypeError: MathUtility.add_numbers() takes 1 positional argument but 2 were given

特殊方法的定义

Character类中,从命名方式看,__init____str__ 方法与其他方法有着显著的区别,该类方法以两个下划线开始,以两个下划线结束,下划线之间的名称也似乎有固定的名称。这种命名方式在 Python 中有着特殊的意义,标识了 Python 的一类特殊方法(Special methods),有时也被称为“魔法方法(Magic Methods)”或“双下方法(Dunder Methods)”。这些特殊方法使得用户定义的对象可以实现和修改一些 Python 语言内置的行为或者实现与 Python 语言的交互。比如说,通过适当地定义这些特殊方法,一个类的实例可以模仿内置类型的行为。这包括但不限于执行算术运算(如使用 __add__ 实现加法)、支持迭代(通过 __iter____next__)、自动进行属性访问(借助 __getattr____setattr__),乃至于作为函数被调用(使用 __call__)。这些特殊方法的实现可以极大地提升对象的表现力,使得自定义对象不仅能够和 Python 的内置类型媲美,还能提供清晰、直观且高效的接口。例如,在 Character 类中,__init__ 方法用于初始化新实例,而 __str__ 方法则提供了对象的字符串表示,这对于打印输出和调试是非常有用的。

特殊方法的主要目的是让咱们的对象适应 Python 的语法规则,而不是自定义单独的方法来实现所有功能。例如,通过实现 __add__ 方法,新定义的对象可以使用加号 + 运算符来进行加法运算。这种方法的使用,增加了代码的可读性和简洁性,使得自定义类型在行为上更像是 Python 的内置类型。

从特征上讲,所有的特殊方法具有一些通用特性,比如说,可以实现自动调用。特殊方法的设计使它们在特定的语言结构和语法中自动调用。例如,当咱们想打印一个对象时,Python 会自动寻找并调用该对象的 __str__ 方法。又如所有的特殊方法都有一个共同的命名模式,两个前导下划线和两个尾随下划线,例如 __getitem__。这种命名约定不仅帮助 Python 可以正确识别它们,而且也使得它们在视觉上与普通方法区分开来。

所以说,在 Python 中,当我们实现或修改特殊方法时,实际上是在利用面向对象编程中的多态性概念,通过重写这些方法为咱们的类定制一些行为。因为特殊方法,如 __add____str____getitem__ 等,已经在更高级别的基类(如 object 类)中预定义了,通过在新的类中重新定义这些方法,实际上是在重写继承自基类的方法。至于要重写那些特殊方法,需要根据新类将要实现哪些行为来确定。比如说,根据类的预期功能决定实现相应的特殊方法。如果新类代表一个序列类型,我们可能会实现 __len____getitem__ 方法来支持序列协议。或者,如果想让新类的实例能够通过调用方式使用,那么我们应该实现 __call__ 方法。

下面我们将对一些常用的特殊方法进行介绍。

构造和初始化

在 Python 中,__init__ 是一个极其关键的特殊方法,主要负责在对象的创建后立即进行初始化。这个方法的核心功能是设置对象的初始状态,具体包括属性的赋值和执行必要的初始化操作。作为对象生命周期中第一个自动调用的方法,__init__ 扮演了构造器的角色。通过 __init__ 方法,创建对象时所需的数据可以作为参数传递进来。这些参数随后被用于初始化对象的各个属性或进行启动时必须的任何操作。这样的设计确保了对象在使用前已经被正确地配置和准备好,为对象的后续使用提供了一致的基础。注意,由于__init__ 方法的主要目的是初始化对象,因此,不应该返回任何值。其基本的语法结构如下:

class ClassName:
    def __init__(self, param1, param2):
        self.param1 = param1
        self.param2 = param2

我们仍以 Character 类为例。

class Character:
    def __init__(self, name, health, experience):
        self.name = name           # 实例变量,每个角色的名字
        self.health = health       # 实例变量,每个角色的健康点数
        self.experience = experience  # 实例变量,每个角色的经验值

    def __str__(self):
        """特殊方法,定义角色的字符串表示形式"""
        return f"Character({self.name}, Health: {self.health}, Experience: {self.experience})"

在此例子中,Character 类有三个属性,即,name, health, experience。当创建一个新的 Character 实例时,需要提供 name, health, 以及 experience 三个参数。这些参数在 __init__ 方法中被接收并赋值给相应的实例属性。以下示例展示了 __init__ 方法的使用:

arthur = Character("Character", 100, 0)

print(arthur) # 自动调用 Character 类中的 __str__ 特殊方法,输出 Character(Arthur, Health: 100, Experience: 0)

在此情景下,我们创建了一个名为 arthur、健康值为 100, 经验值为 0 的 Character 对象。当 Character 类的实例被创建时,__init__ 方法自动执行,接收传递的参数并将它们设置为对象的属性。print(arthur) 时,会自动调用 Character 类中的 __str__ 特殊方法,输出 Character(Arthur, Health: 100, Experience: 0)。这里展示了 __str__ 方法的使用方式。接下来,我们将继续探讨如何使用 __str__ 方法来提供对象的字符串表示,增强类的可用性和调试友好性。

对象表示

对象表示(Object Representation)是指如何将一个对象的状态以文本形式表达出来。在 Python 中,对象的表示形式主要用于调试和日志记录,帮助开发者理解对象的当前状态或者简单地输出对象信息。正确的对象表示能够提高代码的可读性和维护性,在处理复杂的数据结构时尤为有用。

Python 提供了两种主要的方法来定义对象的字符串表示:一种时我们刚才遇到的 __str__ 方法,另一种是类似的 __repr__ 方法。这两种方法有些许的不同,比如,__str__ 方法主要用于对象的“非正式”或可打印的字符串表示,面向的对象一般是最终的用户;而 __repr__ 方法通常用于开发者开发和调试过程中,为对象提供了“正式”的字符串表示。在使用上,当我们使用 print() 函数或 str() 函数时,Python 将自动调用对象的 __str__ 方法。如上面的 print(Arthur) 。而 __repr__ 方法通常是当我们在 Python 解释器中直接查看对象或使用 repr() 函数时才被调用。

我们适当拓展 Charater 来说明 __str____repr__ 这两个特殊方法的区别以及它们的具体使用场景:

class Character:
    def __init__(self, name, level, items=None):
        self.name = name
        self.level = level
        self.items = items if items is not None else []

    def __str__(self):
        return f"{self.name}, Level {self.level}"

    def __repr__(self):
        return f"Character('{self.name}', {self.level}, {self.items})"

这里,__str__ 方法提供了一个用户友好的字符串表示,适用于最终用户需要的输出,如在游戏界面或日志文件中显示角色信息:

character = Character("Aragorn", 5, ["Sword", "Shield"])
print(str(character))  # 输出: Aragorn, Level 5

__repr__ 方法则提供一个更正式的字符串表示,其目标是明确无误,甚至可以用来重新创建该对象:

character = Character("Aragorn", 5, ["Sword", "Shield"])
print(repr(character))  # 输出: Character('Aragorn', 5, ['Sword', 'Shield'])

在这个例子中,__repr__ 输出了足够的信息,包括角色的名字、等级和具体的装备列表。这样的信息量使得开发者可以使用这个字符串来理解或重构对象的精确状态。

属性访问

在面向对象编程中,当咱们定义一个类时,通常会包含实例属性和类属性,如 Character 类中的 nameexperiencelevellevel_up_experience。这些属性实际上是可以通过实例或类访问的变量,它们存储了对象的内部状态。因为它们直接影响到对象的行为和数据的一致性,因此,管理这些属性的访问和修改是面向对象编程中一个重要的环节。

直接访问

在 Python 中,属性访问主要有两种基本方式。一种是直接访问和修改属性。我们可以直接通过对象来获取或设置属性的值。例如,想访问 Character 类的实例对象中的 name 属性,我们可以通过 instance.name 的方式来访问和修改该属性:

arthur = Character("Arthur", 100, 0)
print(arthur.name)

这种方法虽然简单快捷,但它不能控制外部对属性的修改。所以说直接访问属性可能会破坏封装性。

让我们通过前面提到的 Character 类来探讨为什么直接访问属性可能会破坏封装性。

class Character:
    def __init__(self, name, health, experience):
        self.name = name           # 角色的名字
        self.health = health       # 角色的健康点数
        self.experience = experience  # 角色的经验值

在这个类中,属性 name, health, 和 experience 都是公开的,意味着它们可以在类的外部被直接访问和修改:

# 创建角色实例
arthur = Character("Arthur", 100, 0)

# 直接修改属性
arthur.health = -50   # 将健康点数设置为负数
arthur.experience = -100  # 经验值也设置为一个不合逻辑的负数

这里,我们通过直接访问的方式将健康点数和经验值都设置成一个不合逻辑的负数。在游戏逻辑中,健康点数和经验值通常不应该是负数。直接访问使得这些属性可以被随意设置为无效值,从而可能会导致程序出错或行为异常。此外,直接访问还有可能降低程序的可维护性。当类的内部实现依赖于特定的属性状态时,直接修改这些属性会增加维护难度。假设 Character 类中有一个内部机制,它依赖于健康点数(health)来决定角色是否存活,并在健康点数达到零或以下时触发某些特定的行为,比如角色死亡:

class Character:
    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience

    def set_health(self, value):
        if value < 0:
            self.health = 0
            print(f"{self.name} has died.")
        elif value > 100:
            self.health = 100
        else:
            self.health = value

    def add_experience(self, value):
        self.experience += value
        while self.experience >= 100:
            self.experience -= 100
            print(f"{self.name} has leveled up!")

这里,set_health 方法用于安全地设置角色的健康点数。它接受一个值作为参数。该方法的功能是,首先进行负值检查,如果传入的 value 小于 0,这通常意味着角色受到了致命伤害。该方法则将 health 设置为 0,表示角色没有健康点数,同时输出一个消息提示角色已死亡。如果传入的 value 大于 100,这可能是由于游戏逻辑错误或特殊情况(如超级加血)。该方法则将 health 设置为满血状态(100),这是健康点数的假定上限,确保角色的健康点数不会超过这个值。当然,如果传入的 value 在 0 到 100 之间,直接将此值设置为角色的 health,表示角色的健康点数在正常范围内,不需要特殊处理。

add_experience 方法用于安全地增加角色的经验值,并处理升级逻辑。该方法首先将传入的 value 加到 experience 属性上,表示角色通过完成任务或击败敌人获得了经验。如果 experience 是达到或超过 100,则角色会升级,同时,减去 100 经验值(或更多,以处理多余的经验值),并输出一个消息提示角色已升级。这个循环会持续执行,直到 experience 小于 100,确保如果获得的经验足够多,角色可以连续升级多次。

通过这两个方法的解释,我们可以发现,Character 类的内部逻辑(如角色死亡或者升级)依赖于特定的属性状态(healthexperience 的值)。如果允许直接访问 health 属性,玩家或者游戏中的其他逻辑可能会将其设置为不合适的值,比如:

# 创建角色实例
arthur = Character("Arthur", 50, 0)

# 直接修改属性
arthur.health = -20   # 将健康点数设置为负数
arthur.health = 150  # 健康值超出最大限制

这里,我们将健康点数设置为 -20。根据游戏的规则,角色应当被视为死亡。然而,直接设置并不会触发 set_health 中定义的死亡逻辑(打印死亡消息并将健康点数设为 0)。这样,角色可能会在游戏中以一个不合逻辑的状态继续存在。而将 health 直接设置为 150 也违背了游戏的规则,因为这个类设计中的健康点数上限为 100。直接设置可能导致游戏的一些特定逻辑(如伤害计算和显示)出现不符合预期的结果。experience 的情况类似,根据类的设计,当角色的经验值超过 100 时,角色应该升级一次或者连续升级多次(每 100 经验升一级)。但是,直接修改 experience(比如 arthur.experience = 1000), 并不会触发 add_experience 方法中定义的升级逻辑(经验值重置和打印升级消息)。这会导致角色的经验值和等级之间的不一致,以及不触发与升级相关的其他游戏事件(如能力提升)。

可以看出,通过直接方式访问或者修改属性通常会导致数据不一致、逻辑错误和潜在的状态异常。该种方式并不可取。这也凸显了我们接下来要介绍的第二种属性访问方式的重要性。它不仅可以保护数据完整性,同时还可以维护程序的整体规则和逻辑的一致性。

accessormutator

第二种属性访问方式主要是通过定义“实例方法”来实现。这些方法不仅为我们提供了访问属性的接口,还可以在访问和修改属性时加入一些逻辑处理,如验证数据或修改相关的其他属性。由于其功能的特殊性,这类方法一般被称为访问器(accessor)和变更器(mutator),或者称为 “getter” 和 “setter” 方法。accessor,顾名思义,允许我们访问类中的属性,而 mutator 则允许我们设置或者修改类中属性的值。

要实现 accessor 和 mutator 模式,我们需要:

  1. 将属性设置为非公开的。
  2. 为每个属性编写 accessor 和 mutator 方法。

仍以 Character 类为例,下面运用 accessor 和 mutator 方法来管理角色的属性。为此,我们首先需要将 Character 类的属性 namehealthexperience 设置为受保护的属性,即在变量名称前加上单下划线(_),这样表示这些属性不应该被直接从类的外部访问和修改。

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience

在此基础上,我们为每个私有属性编写对应的 accessor 和 mutator 方法,以提供访问和修改属性的安全方式。这些方法通常命名为 get_<属性名>set_<属性名>,分别用于返回属性值和设置新的属性值,比如:

class Character:
    def __init__(self, name, health, experience):
        self._name = name           # 初始化时将属性设置为私有
        self._health = health
        self._experience = experience

    # accessor for name
    def get_name(self):
        return self._name

    # mutator for name
    def set_name(self, value):
        if isinstance(value, str):  # 简单的类型检查
            self._name = value

    # accessor for health
    def get_health(self):
        return self._health

    # mutator for health
    def set_health(self, value):
        if value < 0:
            value = 0  # 保护健康值不为负
        elif value > 100:
            value = 100  # 限制健康值的最大值
        self._health = value

    # accessor for experience
    def get_experience(self):
        return self._experience

    # mutator for experience
    def set_experience(self, value):
        if value < 0:
            value = 0  # 防止经验值为负
        self._experience = value

在该 Character 类的实现中,我们可以看到如何使用 accessor 和 mutator 方法来管理类的受保护属性。这些方法一般是成对定义,从而提供了一个明确的接口来控制对属性的访问和修改。在该类中,角色名称的访问器 get_name() 方法提供了对私有属性 _name 的访问途径,从而实现不暴露属性的直接修改方式,其他访问器类似。在 Python 中,accessor 一般不接受除 self 外的任何参数。这确保了该方法的调用仅依赖于对象本身的状态。同时,accessor 总是返回它们所获取的属性值,如 get_name() 方法返回角色的名称,get_health() 返回健康值,而 get_experience() 返回经验值。

Character 类中,健康值的变更器 set_health() 方法不仅设置健康值,还包含逻辑以确保健康值不会低于 0 或高于 100,从而维护游戏规则的一致性和角色的有效状态。通常情况下,mutator 除了隐含的 self 参数外,每个变更器方法通常接受一个参数,即想要设置的新值。为了避免设置操作的副作用,这些方法也通常不返回任何值(或者说返回 None)。在属性值被更新之前,在 mutator 中可以进行必要的检查,如 set_experience() 方法中的验证确保了经验值不为负值。

下面是 accessor 和 mutator 方法的使用示例:

arthur = Character("Arthur", 100, 50)

# 使用 getter 方法
print(arthur.get_name())  # 输出: Arthur

# 使用 setter 方法
arthur.set_health(150)
print(arthur.get_health())  # 输出: 100 (因为健康值被限制在100)

arthur.set_experience(-10)
print(arthur.get_experience())  # 输出: 0 (因为经验值不能为负)

@property 装饰器

虽然传统的 accessor(访问器)和 mutator(变更器)方法提供了一种明确的模式来控制类属性的访问和修改,并允许在这些操作中嵌入额外的处理逻辑,但这种方法通常需要为每个属性编写两个方法:一个用于获取属性值,另一个用于设置新值。这不仅使得代码变得较为冗长,也降低了代码的简洁性,与 Python 语言推崇的“简单明了”的原则也不太一致。

在上述模式中,属性的读取和写入需要通过 instance.get_<属性名>()instance.set_<属性名>(value) 的形式进行,这对于开发者和最终用户而言可能稍显繁琐。如果能有一种方法,既能够保持属性访问和修改的简洁性,又能够融入 accessor 和 mutator 的优势(如逻辑封装和数据验证),那将大大增强代码的可用性和可维护性。

为此,Python 提供了一种解决方案,即使用 @property 装饰器。@property 装饰器允许开发者将类中的方法转变为对应的属性,这些属性仍然执行与 accessor 和 mutator 相同的内部逻辑。这种方法的使用不仅使得属性的访问看起来就像是直接访问变量一样,而且还能在后台自动处理必要的逻辑,如检查数据有效性或执行必要的转换。因此,@property 装饰器是 Python 社区中处理类属性时尤为推崇的一种方法,特别是在需要对属性的读写进行控制或添加额外逻辑时。

以下是使用 @property 装饰器实现的 Character 类:

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise ValueError("Name must be a string")
        self._name = value

    @property
    def health(self):
        return self._health

    @health.setter
    def health(self, value):
        if value < 0:
            value = 0
        elif value > 100:
            value = 100
        self._health = value

    @property
    def experience(self):
        return self._experience

    @experience.setter
    def experience(self, value):
        if value < 0:
            value = 0
        self._experience = value

从该版本的 Character 类可以看出,使用 @property 装饰器将 accessor 和 mutator 方法转换为属性非常简单。只需在对应的 accessor 方法前添加 @property 装饰器,并在 mutator 方法前添加 @<property_name>.setter 装饰器。通常,这些方法的命名与它们控制的属性名相同,例如,对于 _name 属性,相应的 accessor 和 mutator 方法命名为 name;对于 _health 属性,方法命名为 health,依此类推。除此之外,内部的处理逻辑和不使用 @property 装饰器时的 accessor 和 mutator 方法内部逻辑一致。

使用 @property 装饰器的一个显著好处是,它使得访问属性值就像访问公开属性一样直接,同时允许我们在背后保留必要的逻辑处理(如该例子中的数据验证和条件检查)。比如说,我们创建一个 Character 类的实例,并初始化其属性::

hero = Character("Arthur", 100, 50)

由于使用了 @property 装饰器,我们可以直接访问这些属性,就像它们是公开的属性一样:

print(hero.name)            # 输出: Arthur
print(hero.health)          # 输出: 100
print(hero.experience)      # 输出: 50

当我们需要修改这些属性时,可以直接为它们赋值,而 @property 装饰器确保调用相应的 mutator 方法来处理赋值操作:

hero.name = "SunWukong"   # 尝试将 name 设置为 "SunWukong"。由于这是一个字符串,赋值将成功。
hero.health = 110         # 尝试将 health 设置为 110。由于 mutator 方法中限制了健康值的最大值为 100,它将自动调整为 100。
hero.experience = -10     # 尝试将 experience 设置为 -10。由于 mutator 方法不允许负值,它将自动调整为 0。

我们可以查看下相应的修改后的属性值:

print(hero.name)          # 输出: SunWukong
print(hero.health)          # 输出: 100
print(hero.experience)      # 输出: 0

除了以上方式使用 @property装饰器,Python 中的属性(properties)具有更为广泛的应用场景。例如,我们可以借助于 @property 装饰器创建只读(read-only)、只写(write-only)和可读写(read-write)的属性。

定义只读属性也很简单,在类中,针对某一个属性只定义 accessor 方法,而不定义对应的 mutator 方法即可。比如说:

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience

    @property
    def name(self):
        return self._name

这里,我们只定义了角色名称的 accessor 方法。显然有:

hero = Character("Arthur", 100, 50)

print(hero.name)       # 输出: Arthur

但是,在此种情况下,我们再想修改角色名称时:

hero.name = "SunWukong"

解释器会输出属性错误(AttributeError)的异常:

Traceback (most recent call last):
  File "c:\Users\thinkstation\Desktop\testing\property.py", line 44, in <module>
    hero.name = "SunWukong"
    ^^^^^^^^^
AttributeError: property 'name' of 'Character' object has no setter

如果想使某个属性为只写属性,那么我们可以使用 @property 装饰器结合 mutator 方法,而不提供 accessor 方法。这样,该属性只能被设置,而不能被外部直接读取,符合只写属性的定义。比如说游戏角色中有另外一个密码属性:

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience
        self._password = None  # 私有属性,初始时没有设置

我们可以按照如下方式定义对应的方法:

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience
        self._password = None  # 私有属性,初始时没有设置

    # 只写属性 password 的 setter
    @property
    def password(self):
        raise AttributeError("This attribute is write-only")

    @password.setter
    def password(self, value):
        # 可以在这里加入密码强度验证等逻辑
        self._password = value

在该例中,password 属性被定义为只写。尝试读取这个属性将会引发异常,而设置这个属性则是允许的:

hero = Character("Arthur", 100, 50)

# 设置密码
hero.password = "secure_password123"

# 尝试读取密码
try:
    print(hero.password)
except AttributeError as e:
    print(e)  # 输出: This attribute is write-only

除此之外,使用 @property 装饰器,还可以定义删除(deletion)操作和提供属性的文档说明等功能。比如说:

class Character:
    def __init__(self, name, health, experience):
        self._name = name
        self._health = health
        self._experience = experience

    @property
    def name(self):
        "The name of the character"
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @name.deleter
    def name(self):
        print("Deleting name...")
        del self._name

    @property
    def health(self):
        "The health level of the character. Must be between 0 and 100."
        return self._health

    @health.setter
    def health(self, value):
        if value < 0:
            value = 0
        elif value > 100:
            value = 100
        self._health = value

    @health.deleter
    def health(self):
        print("Deleting health...")
        del self._health

    @property
    def experience(self):
        "The experience points of the character."
        return self._experience

    @experience.setter
    def experience(self, value):
        if value < 0:
            value = 0
        self._experience = value

    @experience.deleter
    def experience(self):
        print("Deleting experience...")
        del self._experience

在这个 Character 类中,每个属性不仅可以被读取和修改,还可以被删除(通过在对应方法前加上 @<property_name>.deleter 实现)。同时,每个属性都有对应的文档字符串,可以使用 Python 的内置函数 help() 来查看:

hero = Character("Arthur", 100, 50)

# 删除属性
del hero.name
del hero.health
del hero.experience

# 尝试再次访问属性,会引发错误因为属性已被删除
try:
    print(hero.name)
except AttributeError as e:
    print(e)        # 输出:'Character' object has no attribute '_name'

使用 help() 来查看 Character 类的文档字符串:

help(Character)

则会显示如下结果:

Help on class Character in module __main__:

class Character(builtins.object)
 |  Character(name, health, experience)
 |
 |  Methods defined here:
 |
 |  __init__(self, name, health, experience)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  experience
 |      The experience points of the character.
 |
 |  health
 |      The health level of the character. Must be between 0 and 100.
 |
 |  name
 |      The name of the character

此外,我们也可以使用如下的方式,显示对应属性的文档字符串

# 显示属性的文档字符串
print(Character.name.__doc__)       # 输出:The name of the character
print(Character.health.__doc__)     # 输出:The health level of the character. Must be between 0 and 100.
print(Character.experience.__doc__) # 输出:The experience points of the character.

descriptors

除了前面三种方式的属性访问外,Python 还为我们提供了一种高级的控制属性行为的方法,即描述符(descriptors)。描述符是一种遵守特定协议(protocol)的类,该协议包括方法 __get__(), __set__(), 和 __delete__() 三种特殊方法。通过这些方法,描述符使得我们能够精细控制类属性的访问、修改和删除行为。

从背后的实现上看,Python 中使用 @property 装饰器创建的属性,实际上是在后台使用描述符来实现属性的控制的。这可以从使用 help(Character) 得到的结果中观察到。在这个帮助文档中,我们可以看到 namehealth 以及 experience 被列为数据描述符(Data descriptors)。数据描述符(data descriptors)是同时定义了 __get__()__set__() 方法的描述符。与之相对应的还有非数据描述符(non-data descriptors)。非数据描述符(non-data descriptors)是只定义了 __get__() 方法的描述符。相比而言,数据描述符具有更高的优先级。

现在,我们以 Character 类为基础,创建一个简单的描述符 NonNegative,来说明描述符的定义和使用。

class NonNegative:
    def __init__(self, default=0):
        self.default = default

    def __get__(self, instance, owner):
        # 返回实例中存储的值
        return getattr(instance, self.private_name, self.default)

    def __set__(self, instance, value):
        # 检查设置的值是否为负,如果是,抛出异常
        if value < 0:
            raise ValueError("This value must be non-negative")
        setattr(instance, self.private_name, value)

    def __set_name__(self, owner, name):
        # 存储属性的私有名称
        self.private_name = '_' + name

从定义上看,描述符本质上是类(Class),但是与普通类在功能定位、使用上下文、方法实现以及实例行为等方面还是有一定的区别:

  • 功能定位方面:普通类通常封装数据和行为,用来模拟真实世界的对象。而描述符类专门用于管理属性访问行为,它们实现了描述符协议,可以控制属性的获取、设置和删除行为。
  • 使用上下文方面:普通类可以直接实例化并使用。描述符类通常被用作另一个类的属性,用来控制那个类的某个属性的行为。
  • 方法实现方面:描述符类必须实现描述符协议中的一个或多个方法(__get__, __set__, __delete__),这是其作为描述符工作的基础。普通类则不需要这些方法,除非特定需求。
  • 实例行为方面:在描述符中,__get____set__ 方法的调用是自动的,基于属性访问和属性赋值触发,而普通类的方法调用通常是显式的。

从定义形式上,描述符可以按照如下结构定义:

class Descriptor:
    def __init__(self, initial_value=None, name='my_var'):
        self.value = initial_value # 存储属性的实际数据
        self.name = name

    def __get__(self, obj, objtype=None):
        print(f"Getting: {self.name}")
        return self.value

    def __set__(self, obj, value):
        print(f"Setting: {self.name} to {value}")
        self.value = value

    def __delete__(self, obj):
        print(f"Deleting: {self.name}")
        del self.value

其中的 __init__() 方法是用来初始化描述符实例的,可以接受一个初始值和一个名称(主要目的是用于调试或日志记录),这里的 value 存储了属性的实际数据。__get__ 方法负责返回属性的值,与 accessor 的功能类似。这里的 obj 是访问属性的对象实例,objtype 是拥有该属性的类。这个方法可以用来返回内部存储的值,或基于复杂逻辑计算后的值。__set__ 方法主要负责设置属性的新值,与 mutator 的功能类似。obj 是尝试修改属性的对象实例。value 是尝试设置的新值。这个方法可以用来验证新值是否符合要求,或在设置新值前进行必要的处理。__delete__ 方法主要负责处理属性的删除操作。obj 是尝试删除属性的对象实例。可以在删除属性前执行清理操作,或阻止删除操作。

回到 NonNegative 描述符。我们进一步简要介绍下其中各个方法的作用。

  • __get__(self, instance, owner):此方法在访问属性值时被调用。其中 instance 是属性所属的实例,owner 是拥有该实例的类。在 NonNegative 中,这个方法返回实例的私有属性值,如果未设置,则返回默认值。
  • __set__(self, instance, value):此方法在属性值被修改时调用。其中 value 是尝试设置的新值。在 NonNegative 中,这个方法确保只有非负数才能被设置到属性上,否则抛出 ValueError
  • __set_name__(self, owner, name):这是 Python 3.6 新增的一个描述符方法,它在创建类时自动调用,用于捕获属性名。其中 owner 是拥有描述符的类,name 是属性名。在 NonNegative 中,这个方法用来设置私有属性名称,通常是在原有属性名前加一个下划线(如 _health),以便内部使用且不与其他属性名冲突。

接下来,我们修改 Character 类,使用 NonNegative 描述符来管理 healthexperience 属性:

class Character:
    health = NonNegative()
    experience = NonNegative()

    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience

现在,任何尝试将 healthexperience 设置为负数的操作都会触发异常:

hero = Character("Arthur", 100, 50)

# 尝试设置负的健康值
try:
    hero.health = -10
except ValueError as e:
    print(e)  # 输出: This value must be non-negative

# 正常设置经验值
hero.experience = 30
print(hero.experience)  # 输出: 30

# 尝试删除 health 属性
try:
    del hero.health
except AttributeError as e:
    print(e)  # 如果有特定的删除逻辑,可以在描述符中处理

可以看出,使用描述符 NonNegative,确保了 Character 类的 healthexperience 属性不会被设置为负数。这在功能上与使用 @property 装饰器类似,但描述符提供了一些独特的优势,特别是在代码的复用性和一致性方面。

虽然 @property 装饰器在简单性和直观性方面上有优势,且易于理解和实施,但是描述符在处理更复杂的属性行为时优势明显。描述符允许开发者将属性的控制逻辑封装在一个独立的类中,这不仅增强了代码的模块化,还提高了维护效率。最重要的是,描述符可以在多个类之间共享,避免了重复编写相同的属性逻辑,从而保持了代码的干净和一致。例如,如果有多个类都需要确保某些数值属性不能为负,使用 NonNegative 描述符可以一次性解决这一需求,而无需在每个类中分别使用 @property 定义相同的逻辑。这不仅节省了开发时间,也使得未来的逻辑更改更加集中和高效。

__getattr__, __setattr__, __delattr__

关于属性访问,我们最后再介绍一种方式,使用 __getattr__, __setattr__, __delattr__ 特殊方法来实现属性的管理。这些方法允许开发者拦截对未知属性的访问或控制所有属性的赋值和删除行为,从而实现更复杂的属性管理策略。

在 Python 中,__getattr__ 被用于定义对象属性访问失败时的行为。当咱们尝试访问对象的一个属性时,如果该属性在对象的常规属性字典中不存在,Python 将自动调用这个对象的 __getattr__ 方法。

假设我们希望在 Character 类中,当某个属性不存在时,提供一种处理方式而不是抛出 AttributeError。这里,我们就可以添加一个 __getattr__ 方法来实现这个功能:

class Character:
    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience

    def __getattr__(self, attr):
        # 如果尝试访问的属性不存在,返回一个默认消息
        return f"{attr} not available"

hero = Character("Arthur", 100, 50)

# 正常访问存在的属性
print(hero.name)  # 输出:Arthur

# 尝试访问不存在的属性
print(hero.strength)  # 自动调用__getattr__(), 输出: strength not available

如果,不定义 __getattr__, 则在 print(hero.strength) 会输出如下异常:

Traceback (most recent call last):
  File "c:\Users\thinkstation\Desktop\testing\property.py", line 48, in <module>
    hero.strength
AttributeError: 'Character' object has no attribute 'strength'

此外,如果某个属性不存在,__getattr__ 还可以用来提供一个默认值。比如说:

class Character:
    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience

    def __getattr__(self, attr):
        # 提供默认值的字典
        default_values = {
            'strength': 10,
            'intelligence': 10,
            'agility': 10
        }
        # 如果属性在默认值字典中,返回默认值
        if attr in default_values:
            return default_values[attr]
        # 如果不在默认值字典中,返回通用不可用消息
        return f"{attr} not available"

这里,__getattr__ 方法首先检查一个名为 default_values 的字典,该字典定义了一些属性及其对应的默认值。当尝试访问的属性名在这个字典中时,方法返回相应的默认值。如果属性名不在这个字典中,则返回一个表示属性不可用的消息。

hero = Character("Arthur", 100, 50)

# 正常访问存在的属性
print(hero.name)  # 输出: Arthur

# 尝试访问不存在的属性,但提供默认值
print(hero.strength)  # 输出: 10
print(hero.intelligence)  # 输出: 10

# 尝试访问不存在且无默认值的属性
print(hero.charisma)  # 输出: charisma not available

__getattr__ 还常用于实现延迟加载(lazy loading),即属性的值在首次访问时才计算或加载。这对处理开销大的计算或从外部数据源加载数据尤其有用。假设我们有一个 profile 属性。该属性将从一个外部数据源(例如数据库或网络服务)加载角色的详细配置文件。我们可以通过如下方式实现 profile 属性的延迟加载:

class Character:
    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience
        self._profile_loaded = False
        self._profile = None

    def load_profile(self):
        # 这里模拟一个耗时的数据加载过程
        print("Loading profile from a slow database...")
        return {
            "biography": "Arthur was a legendary British leader.",
            "achievements": ["Established the Knights of the Round Table", "United Britain"]
        }

    def __getattr__(self, attr):
        if attr == "profile":
            if not self._profile_loaded:
                self._profile = self.load_profile()
                self._profile_loaded = True
            return self._profile
        return f"{attr} not available"

hero = Character("Arthur", 100, 50)

# 访问 profile 属性,将触发延迟加载
print(hero.profile)  # 输出加载过程和数据
print(hero.profile)  # 直接输出数据,无需再次加载

这里,我们初始化时程序并不加载 profile 数据,而是在该属性首次被访问时(如第一次print(hero.profile))调用 load_profile 方法来加载数据。这减少了对象创建时的开销,特别是对于加载数据特别耗时的属性。这个逻辑的具体实现是由 __getattr__() 特殊方法控制。__getattr__() 在属性 profile 被访问且尚未加载时,调用 load_profile 方法并缓存结果。一旦数据被加载,后续访问该属性不会再触发加载过程, 如第二次 print(hero.profile)

__setattr__ 方法允许我们定义当属性被赋值时应执行的自定义行为,如数据验证、自动转换、维护属性间的依赖关系,或其他任何需要在属性值改变时执行的操作。当咱们尝试给属性赋值时,例如 obj.attribute = value,Python 会自动调用定义好的 __setattr__ 方法。

__setattr__ 方法通常定义如下:

def __setattr__(self, name, value):
    # 自定义行为

其中 name 是要设置的属性名称,value 是要设置的值。

我们可以通过实现 __setattr__ 方法来达到确保 healthexperience 属性不会被设置为负值的目的:

class Character:
    def __init__(self, name, health, experience):
        self.name = name
        self.health = health
        self.experience = experience

    def __setattr__(self, key, value):
        # 对 health 和 experience 属性进行非负值检查
        if key in ["health", "experience"] and value < 0:
            raise ValueError(f"{key} cannot be negative.")

        # 调用基类的 __setattr__ 来实际设置属性
        super().__setattr__(key, value)

        # 记录属性设置
        print(f"Set {key} to {value}")

# 创建一个 Character 对象
hero = Character("Arthur", 100, 50)

# 修改 health 属性
hero.health = 80

# 尝试将 experience 设置为负值,将触发 ValueError
try:
    hero.experience = -10
except ValueError as e:
    print(e)    # 输出:experience cannot be negative

需要注意的是,在 __setattr__ 中直接使用 self.name = value 形式赋值会再次触发 __setattr__,导致无限递归。为避免这种情况,应使用 super().__setattr__(name, value)object.__setattr__(self, name, value) 来确保调用基类的实现,从而正确地设置属性。

__delattr__ 特殊方法的定义和使用与 __setattr__ 类似,具体可以参考Customizing attribute access。这里不再详述。

属性访问的四种主要方式各有其特点和适用场景。下面比较了直接访问、@property 装饰器、描述符(descriptors),以及通过使用特殊方法(如 __getattr____setattr__ 等)管理属性的方法。

属性访问方式 特点 优点 缺点 适用场景
直接访问 直接通过属性名访问或修改对象的属性 简单直观;代码易于理解和使用 不提供封装和验证;容易导致外部代码直接修改内部状态 不需要额外逻辑,对性能要求高的场合
@property 装饰器 使用装饰器定义的方法来管理属性的获取和设置 代码整洁;易于添加额外逻辑如验证和转换 每个属性需要额外的方法;可能影响性能 需要封装属性以隐藏实现细节,或当属性的设置和获取需要附加逻辑处理时
描述符 (Descriptors) 通过定义带有 __get____set____delete__ 方法的类来管理属性 高度可复用;可应用于多个属性和类;提供最大的控制力 较复杂,初学者可能难以理解;增加了代码的抽象层次 需要在多个属性或类中统一实现相同的存取逻辑,或需要跨属性同步或其他复杂的属性管理行为
特殊方法 (__getattr__, __setattr__, etc.) 使用魔法方法来拦截对属性的所有访问、设置或删除操作 提供对属性操作的全面控制;可以实现动态属性或代理 处理复杂;容易导致错误,如无限递归;可能影响性能 需要自定义属性访问机制,动态生成属性,或在访问属性时执行非标准行为
类的容器功能

在 Python 中,类可以被视为一种容器,它们用于封装数据和与数据相关的方法。同时,类也是实现复杂数据结构的主要工具。如链表、树、图、堆、哈希表等。通过定义节点、操作如插入、删除等,类能够提供数据结构所需的所有功能。下面借助于类,实现了一个简单的链表:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        new_node = Node(data)
        if self.head is None:
            self.head = new_node
        else:
            last = self.head
            while last.next:
                last = last.next
            last.next = new_node

    def display(self):
        current = self.head
        while current:
            print(current.data, end=' ')
            current = current.next
        print()

在这个例子中,Node 类用于创建链表节点,而 LinkedList 类则封装了链表的操作,如添加新节点和显示链表内容。这个设计体现了类作为容器的特性,其中 LinkedList 类封装了链表的所有功能和数据,使得链表的操作逻辑清晰且易于管理。

从这个例子上来说,容器就像一个桶或者盒子一样,是一个可以存放各种东西的地方。在编程里,这些“东西”可以是数字、文字或者其他任何东西。更为正式的说法是容器是一种用来存储和组织多个数据元素的数据结构。Python 内置的列表(list)、元组(tuple)、集合(set)、字典(dictionary)等均是容器。像列表和元组可以用来保存序列化的数据,而集合可以被用于存储无序且不重复的元素,字典则可以提供了基于键值对的存储机制。根据数据特征,使用不同的容器来存储,可以为我们提供便捷的方法来访问存储的数据。比如字典允许通过键来快速检索数据,列表则允许通过索引进行访问。此外,某些容器(如集合和字典)对数据的添加、删除和查找也能提供更为高效的处理方式,这些操作的时间复杂度可以达到 O(1)。

Python 的容器类型通常支持一系列的内置方法,比如列表通常支持 append(), remove(), pop() 等方法,字典通常支持 keys(), values(), items() 等方法。当然,我们也可以在自定义类中实现和扩展这些容器方法,以满足特定的业务需求或性能优化,如 LinkedList 类内容中实现的 append() 方法。与其他特殊方法类似,Python 也为我们提供了一些特殊方法来实现容器类的特殊功能,如 __len__, __getitem__,__setitem__, __delitem__ 等。这些方法的实现,可以提供类似与内置类型的行为。比如说,一旦一个类定义了 __len__ 方法,我们就可以使用内置的 len(instance) 函数来获取该容器的长度,这样的设计极大地提升了语言的一致性和直观性。下表列举了 Python 中用于定义容器类行为的常用特殊方法:

方法名 描述
__len__ 返回容器中元素的数量。
__add__ 定义加法操作,使对象能够使用 + 运算符。
__mul__ 定义乘法操作,使对象支持重复,如 obj * n
__iadd__ 定义就地(In-palce)加法操作,使对象支持 += 运算符。
__imul__ 定义就地乘法操作,使对象支持 *= 运算符。
__eq__ 定义等于测试,使对象支持 == 运算符。
__ne__ 定义不等于测试,使对象支持 != 运算符。
__lt__ 定义小于测试,使对象支持 < 运算符。
__le__ 定义小于等于测试,使对象支持 <= 运算符。
__gt__ 定义大于测试,使对象支持 > 运算符。
__ge__ 定义大于等于测试,使对象支持 >= 运算符。
__getitem__ 使对象支持索引操作,如 obj[key] 来访问元素。
__setitem__ 使对象支持通过索引设置元素,如 obj[key] = value
__delitem__ 使对象支持通过索引删除元素,如 del obj[key]
__iter__ 返回容器的迭代器,用于 for 循环遍历。
__next__ 使对象成为迭代器,返回迭代中的下一个元素。
__contains__ 检查容器是否包含某元素,响应 in 关键字。
__reversed__ 提供容器的反向迭代器。

下面,我们基于 LinkedList 链表类探讨在容器中如何实现这些特殊方法的定义及其使用。

LinkedList 类中,__len__() 方法用于返回链表中元素的数量。由于该链表本身不存储其长度信息,我们不能像数组那样直接访问长度属性。因此,我们需要从链表的头部开始遍历每一个节点,直到达到链表的末尾。每遍历一个节点,计数器就递增一次,这样可以确保准确计算出链表中的元素总数。

这个方法的实现反映了链表的基本特性——节点间的线性连接。计算长度的过程实际上是一个线性时间复杂度的操作(O(n)),这意味着操作的耗时与链表长度成正比。虽然这种方法在性能上不如直接访问数组长度,但它是链表结构所必需的,因为链表的非连续存储特性使得无法像数组那样直接通过一个属性获取长度。

以下是 __len__() 方法在 LinkedList 中的实现:

def __len__(self):
    count = 0
    current = self.head
    while current:
        count += 1
        current = current.next
    return count

这种方式确保了无论链表如何变化,都可以准确地获取其长度,是处理链表类型数据时常见的操作之一。这样,当想知道 LinkedList 对象中的数据个数时,我们就可以调用 len(my_list) 打印出链表的长度。在 Python 在内部,这个过程实际上调用了 my_list.__len__() 方法。如该方法所示,这个方法将遍历整个链表,计数节点的数量,并返回这个值。

# 创建一个 LinkedList 实例
heroes_list = LinkedList()

# 向链表中添加草丛三姐妹
heroes_list.append("妲己")
heroes_list.append("安琪拉")
heroes_list.append("王昭君")

# 使用 __len__ 方法获取链表的长度
length = len(heroes_list)  # 这里实际上调用的是 heroes_list.__len__()

# 打印链表的长度
print("Number of heroes in the linked list:", length) # 输出:Number of heroes in the linked list: 3

LinkedList 类中,__add__() 方法的实现是为了使 LinkedList 类的对象支持使用加号 (+) 运算符来合并两个链表,并返回一个新链表。

def __add__(self, other):
    if not isinstance(other, LinkedList):
        return NotImplemented
    new_list = LinkedList()
    current = self.head
    while current:
        new_list.append(current.data)
        current = current.next
    current = other.head
    while current:
        new_list.append(current.data)
        current = current.next
    return new_list

该方法首先检查 other 是否也是一个 LinkedList 实例(这是必要的,因为只有相同类型的链表才能合并)。如果 other 不是 LinkedList 类型,方法返回 NotImplemented,这是 Python 的一种标准做法,用于指示特定操作在给定的类型间不支持。

其次,创建一个名为 new_list 的新 LinkedList 实例。这个新链表将包含来自 self(当前对象)和 other(另一个链表)的所有节点。

第三步,设置一个指针 current 指向 self.head(当前对象的头节点)。遍历当前链表 (self),使用 append 方法将每个节点的数据复制到新链表 new_list。这确保 new_list 会包含当前链表的所有元素的副本

第四步,重新设置指针 current 指向 other.head(另一个链表的头节点)。继续遍历 other 链表,同样使用 append 方法将每个节点的数据添加到 new_list。这样,new_list 也将包含另一个链表的所有元素的副本。

最后,方法返回新创建的 new_list,这个链表现在就包含了两个原始链表的所有节点,而原始链表的节点和结构保持不变。

我们可以在前面的示例中添加第二个链表,并运用 + 的方式合并这两个链表:

# 创建第二个 LinkedList 实例,并添加其他英雄
more_heroes_list = LinkedList()
more_heroes_list.append("露娜")
more_heroes_list.append("娜可露露")
more_heroes_list.append("貂蝉")

# 显示第一个链表的英雄名字和长度
print("First LinkedList:")
heroes_list.display()
print("Number of heroes in the first linked list:", len(heroes_list))

# 显示第二个链表的英雄名字和长度
print("\nSecond LinkedList:")
more_heroes_list.display()
print("Number of heroes in the second linked list:", len(more_heroes_list))

# 使用 __add__ 方法合并两个链表
combined_list = heroes_list + more_heroes_list

# 显示合并后的链表的英雄名字和长度
print("\nCombined LinkedList:")
combined_list.display()
print("Number of heroes in the combined linked list:", len(combined_list))

运行后,会输出如下结果:

First LinkedList:
妲己 安琪拉 王昭君 
Number of heroes in the first linked list: 3

Second LinkedList:
露娜 娜可露露 貂蝉
Number of heroes in the second linked list: 3

Combined LinkedList:
妲己 安琪拉 王昭君 露娜 娜可露露 貂蝉
Number of heroes in the combined linked list: 6

类似的,我们可以在 LinkedList 中实现 __mul__() 方法,使链表可以通过乘法操作符(*)与一个整数相乘,从而创建一个新的链表,其中原链表的内容重复指定的次数。

def __mul__(self, n):
    if not isinstance(n, int):
        return NotImplemented
    if n <= 0:
        return LinkedList()  # 返回一个空的链表
    new_list = LinkedList()
    for _ in range(n):
        current = self.head
        while current:
            new_list.append(current.data)
            current = current.next
    return new_list

该方法的实现逻辑如下:

  • 首先,检查乘数 $n$ 是否为整数。如果不是,返回 NotImplemented
  • 其次,如果 $n$ 小于或等于 0,返回一个新的空链表,因为任何东西乘以 0 或负数在逻辑上应该为空或不存在。
  • 创建一个新的 LinkedList 实例 new_list
  • 使用两层循环来实现重复:外层循环控制重复次数,内层循环遍历当前链表的所有元素,并将它们添加到新链表中。
  • 最后,返回新的重复后的链表。

根据如上定义,我们可以使用乘法操作符 * 来重复链表,比如:

# 使用 __mul__ 方法重复链表内容
# 假设我们想将整个链表内容重复3次
repeated_list = heroes_list * 3

# 显示重复后的链表
print("Repeated LinkedList (3 times):")
repeated_list.display()

这将输出下入下信息:

Repeated LinkedList (3 times):
妲己 安琪拉 王昭君 妲己 安琪拉 王昭君 妲己 安琪拉 王昭君

定义 __iadd__ (In-place 加法)方法通常意味着要在原有对象的基础上修改而非创建一个新的对象。这与 __add__ 方法的典型实现有所不同,后者常常返回一个全新的对象。

def __iadd__(self, other):
    if not isinstance(other, LinkedList):
        return NotImplemented
    current = self.head
    if not current:  
        self.head = other.head
    else:
        last = current
        while last.next:  
            last = last.next
        last.next = other.head  
    return self

与前面的方法类似,该方法首先检查 other 是否也是一个 LinkedList 实例。如果 other 不是 LinkedList 类型,方法返回 NotImplemented

然后,将当前链表的头节点赋值给变量 current,并判断当前链表是否为空。如果当前链表为空,则直接将 self.head 指向 other 链表的头节点,将 other 链表直接接入到当前链表的头部。如果当前链表不为空。则初始化 last 变量以遍历链表找到最后一个节点。将找到的最后一个节点的 next 指向 other 链表的头节点,从而将 other 链表接在当前链表的末尾。

最后,方法返回 self,即经过修改后的原链表对象。

__imul__() 方法的实现用于链表的就地乘法操作,允许通过 *= 运算符将一个 LinkedList 实例的内容重复指定次数并更新原始链表。这也意味着,这种操作直接修改调用它的对象(即 self),而不是创建一个新的链表对象。

def __imul__(self, n):
    if not isinstance(n, int):
        return NotImplemented
    original_data = list(self)  # # Convert the linked list to a list, which relies on the __iter__ method for iteration

    if n <= 0:
        self.head = None
    else:
        self.head = None
        for _ in range(n):
            for item in original_data:
                self.append(item)
    return self

该方法的实现逻辑如下:

  • 首先,检查乘数 $n$ 是否为整数。如果不是,返回 NotImplemented
  • 其次,将链表转换为列表,以便存储原始链表中的所有元素。这一转换依赖于 __iter__() 方法的实现,该方法允许通过迭代器来遍历链表的每个节点,并获取它们的数据。我们将在下面介绍。
  • 在此基础上,检查整数 $n$ 是否小于或者等于0。如果 $n$ 小于或等于0,意味着链表应被清空(因为重复0次或负数次没有逻辑意义)。此时将 self.head 设置为 None,这样做将清除链表中的所有元素。 如果 $n$ 是正整数,那么首先清空原链表。这是重建链表之前的准备步骤,确保链表从空状态开始。然后使用两层循环,外层循环控制重复次数,即将整个链表内容重复 $n$ 次,内层循环遍历之前保存的原始数据列表。每个元素通过 self.append(item) 方法被重新添加到链表中。这样,原始链表的每个元素都会按原顺序被添加 $n$ 次。
  • 最后,方法返回经过就地修改后的链表实例(self)。

以下示例展示了该方法的基本使用方式:

# 使用 __imul__ 方法就地重复链表内容
heroes_list *= 3
print("\nHeroes List after using __imul__ (tripled in-place):")
heroes_list.display()
print("\nNumber of heroes after __imul__:", len(heroes_list))

运行后将显示:

Heroes List after using __imul__ (tripled in-place):
妲己 安琪拉 王昭君 妲己 安琪拉 王昭君 妲己 安琪拉 王昭君

Number of heroes after __imul__: 9

在实现 __imul__() 方法时,我们指出,该算法依赖于迭代器来将链表转换为列表,以便存储原始链表中的所有元素。那么什么是迭代器,甚至什么是迭代?

在编程中,迭代(Iteration,又大致可以称为遍历)指的是按照一定的顺序反复访问数据集中的每个元素的过程。迭代可以应用于各种数据结构,如列表、数组、树、图等。在 Python 中,迭代通常是通过使用循环结构(如 for 循环或 while 循环)来实现的,让程序可以执行重复的处理,直到满足某个条件为止。

比如说,我们可以借助于 for 循环输出一个列表中的所有元素:

for number in [1, 2, 3, 4, 5]:
    print(number)

这一过程就可以称为迭代。由此,也衍生出来了迭代器(Iterator)以及可迭代对象(Iterable)等概念。迭代器是实现了迭代器协议(Iterator Protocol)的对象,该协议主要包括 __iter__()__next__() 两个特殊方法。对于迭代器来说,这两个方法均为必须实现。__iter__() 返回迭代器自身,而 __next__() 返回集合中的下一个元素。当元素耗尽时,__next__() 必须抛出一个 StopIteration 异常,标志迭代的结束。在 Python 中,任何实现了 __iter__() 方法的对象或内置的序列对象(如列表和元组)都是可迭代的。当这些对象用于 forin 语句时,Python 会自动调用它们的 __iter__() 方法以获取迭代器。

值得注意的是,所有的迭代器都是可迭代对象,但并不是所有的可迭代对象均是迭代器。例如,列表是一个可迭代对象,因为它实现了 __iter__() 方法,但列表本身不包含 __next__() 方法,因此不是迭代器。从概念定义上讲,迭代主要是提供遍历数据的框架,而迭代器则提供在这个框架内操作的具体方法和机制。

为了在 LinkedList 中实现迭代器的功能,我们可以先定义一个单独的 LinkedListIterator 类。

class LinkedListIterator:
    def __init__(self, head):
        self.current = head

    def __iter__(self):
        return self

    def __next__(self):
        if not self.current:
            raise StopIteration
        data = self.current.data
        self.current = self.current.next
        return data

在这个新类中,__init__() 构造函数接收链表的头节点并初始化迭代器的当前节点 (self.current)。__iter__() 方法则返回迭代器对象本身,符合迭代器协议。__next__() 方法检查当前节点。如果当前节点为 None,则抛出 StopIteration 异常,标志迭代结束;否则,获取当前节点的数据,并将迭代器移向下一个节点,最后返回当前节点的数据。

由此,我们可以在 LinkedList 类中构建一个 __iter__() 方法,该方法创建并返回 LinkedListIterator 对象:

def __iter__(self):
    return LinkedListIterator(self.head)

这样我们就可以使用 for 循环迭代 LinkedList:

# 使用 for 循环迭代 LinkedList
for data in more_heroes_list:
    print(data, end=" ")     # 输出:露娜 娜可露露 貂蝉

__contains__() 方法的实现可以为 LinkedList 类提供了能力来检查某个元素是否存在于链表中(使用 in 关键字)。对于简单的数据结构,__contains__() 方法可以通过遍历来实现,对每个节点的数据进行比较。

def __contains__(self, item):
    current = self.head
    while current:
        if current.data == item:
            return True
        current = current.next
    return False
# 使用 __contains__ 方法实现检查某个元素是否在LinkedList对象中
print("安琪拉" in heroes_list)      # 输出:True
print("安琪拉" in more_heroes_list) # 输出:False

LinkedList 类中实现 __getitem__()__setitem__()__delitem__() 特殊方法可以使其支持支持通过索引进行获取、设置和删除元素的操作,类似于标准的序列类型如列表。以下是这三种方法实现示例:

# 实现 __getitem__
def __getitem__(self, index):
    if index < 0:
        raise IndexError("Negative indices are not supported")
    current = self.head
    for _ in range(index):
        if current is None:
            raise IndexError("Index out of bounds")
        current = current.next
    if current is None:
        raise IndexError("Index out of bounds")
    return current.data

这里,我们首先检查传入的索引是否为负数。考虑到 LinkedList 的特征,该链表并不支持负索引(与Python列表等不同),因此如果索引是负数,则抛出 IndexError 表示索引无效。然后,我们从链表的头节点开始,沿着链表向后遍历。使用一个循环来跳过直到达到指定的索引位置的元素。每次循环中,更新当前节点指向下一个节点。在遍历过程中,如果当前节点变为 None,说明已经超出了链表的尾部,这时也应抛出 IndexError 表示索引超出了链表的有效范围。一旦到达正确的位置,返回当前节点的数据。这里通过访问节点的 data 属性来实现的。

# 实现 __setitem__
def __setitem__(self, index, value):
    if index < 0:
        raise IndexError("Negative indices are not supported")
    current = self.head
    for _ in range(index):
        if current is None:
            raise IndexError("Index out of bounds")
        current = current.next
    if current is None:
        raise IndexError("Index out of bounds")
    current.data = value

这里,我们仍然先检查传入的索引是否为负数。由于该链表没有记录反向索引,因此,负数索引在这里是不被接受的,如果发现索引为负,则抛出 IndexError。接着,从链表的头节点开始,顺序遍历链表节点直到达到指定的索引位置。这一过程中,通过循环逐个访问链表的节点,每次循环将当前节点向前移动到下一个节点。如果在达到所需索引前当前节点已经为 None(即已经到达链表尾部而没有足够的元素),则表明索引超出了链表当前的长度范围,这时也应抛出 IndexError。一旦到达指定索引的节点,将该节点的数据设置为新的值。这通过直接赋值给节点的 data 属性来实现。注意该方法的实现与链表插入功能的区别。

# 实现 __delitem__
def __delitem__(self, index):
    if index < 0:
        raise IndexError("Negative indices are not supported")
    if self.head is None:
        raise IndexError("Index out of bounds")
    if index == 0:
        self.head = self.head.next
        return
    current = self.head
    previous = None
    for _ in range(index):
        previous = current
        if current is None or current.next is None:
            raise IndexError("Index out of bounds")
        current = current.next
    if current is None:
        raise IndexError("Index out of bounds")
    previous.next = current.next

__delitem__ 方法仍然首先检查提供的索引是否为负数,如果索引为负,则抛出 IndexError,表明索引无效。同时检查需要删除的是否为链表的第一个元素(即索引为0),此时,可以直接将头节点 (self.head) 指向下一个节点 (self.head.next),从而将当前头节点从链表中移除。如果不是删除第一个节点,则从头节点开始,遍历链表以找到指定索引的节点。同时保持对当前节点的前一个节点的引用(previous),这是为了在删除当前节点时能够重新连接链表。在遍历过程中,如果到达链表尾部(当前节点为 None)而还没有到达指定的索引,表示索引超出了链表的当前长度,这时应抛出 IndexError。一旦找到指定索引的节点,通过设置前一个节点的 next 指针绕过当前节点(即 previous.next = current.next),从而实现删除操作。由于删除操作不需要返回特定的值,方法完成后返回 None

使用示例:

# 使用 __getitem__ 访问元素
print(more_heroes_list[0])

# 使用 __setitem__ 修改元素
more_heroes_list[2] = '孙悟空'
print("\nHeros List after using __setitem__: ")
more_heroes_list.display()

# 使用 __delitem__ 删除元素
del more_heroes_list[2]
print("\nHeros List after using __delitem__: ")
more_heroes_list.display() 
露娜

Heros List after using __setitem__:
露娜 娜可露露 孙悟空

Heros List after using __delitem__:
露娜 娜可露露

至于 LinkedList 链表的比较操作,请参考 linkedlist.py

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!等资料。

参考资料