想象你走进一家星巴克咖啡店。这里有许多不同种类的咖啡杯,每个咖啡杯都具有不同的特性。例如:
- 容量:有小杯(Tall)、中杯(Grande)、大杯(Venti)等。
- 颜色:一些是经典的绿色和白色星巴克标志,其他可能是特别版或季节性设计。
- 材质:有些是一次性的纸杯,有些则是可以多次使用的陶瓷杯或不锈钢杯。
- 形状:有些杯子是标准的圆柱形,而有些可能有独特的设计,如节日特别款或限量版。
用另外一个词来描述这些咖啡杯的特性就是“属性”。这些咖啡杯有其主要用途,即盛装咖啡供顾客饮用,这就是它们的“方法”。在不同的时间,顾客可能会根据个人喜好或需要选择不同的咖啡杯。比如,一个需要长时间工作的顾客可能选择一个大杯的拿铁(Latte)以维持更长时间的精神状态;另一个顾客可能只是想要一小杯意式浓缩咖啡(Espresso)快速提神。
在这个例子中,我们可以看到每个咖啡杯都是一个具体的“对象(Object)”,具备独特的属性和功能。这些咖啡杯虽然多样,但都属于更广泛的“咖啡杯”类别,每个都为顾客的不同需求服务。
我们再来看一个例子。大家都很喜欢游戏,可能都玩过角色扮演类的游戏(RPG)。假设有一个名为“英雄与怪兽”的角色扮演游戏。在这个游戏中,玩家可以选择扮演不同的角色,例如骑士、法师或弓箭手,这些角色的目的是去打游戏世界中充满的各种怪兽,获取掉落的装备、提升自己的等级。我们知道,不管是玩家角色还是怪兽,都应该具有一些共有的特性,比如生命值(HP)、魔法值(MP)、攻击力(ATK)和防御力(DEF)等等,同时也会有一些共有的技能,比如攻击(attack)、防御(defend)和使用技能(use_skill)。当然,如果游戏中所有角色都具有相同的属性和方法,那么对于玩家来说就没有吸引力了。所以,一般情况下,玩家角色和怪兽都会有自身的一些固有属性以及技能。比如说,对于玩家角色,骑士可能具有更高的防御力,法师具有更强的魔法攻击,而弓箭手可能攻击速度更快;骑士的“盾墙”提高防御,法师的“火球术”提供强大的范围攻击,而弓箭手的“瞄准射击”将造成重大单体伤害。对于怪兽,龙可能会喷火,而哥布林可能会进行群攻;每种怪兽都有自己的攻击方式和生存策略。那么我们可以用如下的图来表示各个角色本身的特性和技能,以及它们之间的存在的关系,
从以上描述可以看出,游戏中的每个角色和物体都可以是一个对象,具有自己的属性和行为。同时,它们之间也可以存在一种“继承”的关系,如具体的玩家角色(骑士、法师、弓箭手)和怪兽都可以拥有角色的属性和技能。游戏环境,如地图,与角色有着明显的区别,因此,可以单独作为一个对象。类似的,为了增加游戏的趣味性,我们还可以设计多张地图,每个地图上都可以生成不同类型的怪兽。而这些子地图除了本身的特性和功能外,也可以继承于地图里的共有的属性和功能。
有了这样的概念,我们就可以回到编程上来了。现代编程中有一种面向对象编程(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)也被称为构造函数(构造方法)、初始化方法等。
和析构器(Destructors)也被称为析构函数(析构方法)、终结函数、清理方法等。
构造器(Constructors)通常在创建对象时自动调用,以便设置对象的初始状态。构造器的主要任务是初始化对象拥有的属性,为它们赋予合适的初始值。在 Python 中,构造器方法通常被称为 __init__
。例如,在一个游戏角色类 Character
中,构造器可能会接受参数如健康值、力量和防御力,并将它们赋值给对象的相应属性:
1class Character:
2 def __init__(self, health, strength, defense):
3 self.health = health
4 self.strength = strength
5 self.defense = defense
此代码段定义了 Character
类的构造器,它初始化了三个属性:health
、strength
和 defense
。当创建 Character
类的新实例时,必须提供这些参数。
析构器(Destructors)是在对象即将被销毁时自动调用的方法,用于执行必要的清理操作。这可能包括释放资源、关闭文件句柄或断开网络连接等。在 Python 中,析构器方法通常被称为 __del__
。虽然 Python 有自动垃圾收集机制来处理内存管理,但在某些情况下,显式定义析构器来清理资源仍然是必要的:
1class Character:
2 def __del__(self):
3 print("Character has been destroyed.")
这个析构器实现很简单,仅仅是在对象销毁时打印一条消息。在实际应用中,析构器的使用应该非常谨慎,因为很多时候,我们并不能预测 Python 的垃圾收集机制的执行时间。
从构建形式上,Python 中的构造器和析构器其实与特殊方法(Magic Methods)相似。但不同的是,它们与对象的生命周期直接相关,具有一些独特的特性和重要的职责。比如:
- 生命周期管理
- 构造器:构造器是创建对象时自动调用的方法,它的主要任务是初始化新对象的状态。构造器使得创建和初始化对象变得简单,确保对象在使用前已经设置了所有必要的初始条件。由于构造器对对象的初始化至关重要,没有正确的初始化,对象可能无法正确地执行其余的功能。
- 析构器:析构器在对象生命周期结束时调用,用于执行清理工作,如资源释放、关闭文件等。在某些语言中(如 C++),析构器的重要性更加突出,因为需要手动管理内存和其他资源。在 Python 中,虽然有垃圾回收机制自动管理内存,但在处理非内存资源时析构器依然很重要。
- 自动调用的特性:构造器和析构器是自动调用的。构造器在对象实例化时自动调用,析构器在对象即将被销毁时自动调用。这与其他方法不同,其他方法通常需要显式调用才会执行。
- 设计模式和良好实践:正确使用构造器和析构器是面向对象设计中的一个重要方面。它们是实现封装和管理对象状态的关键工具。不当的使用可能会导致资源泄露、无效状态或者软件缺陷。
- 编程语言的语法和约定:在许多面向对象的编程语言中,构造器和析构器有特定的语法和命名约定(如 Python 中的
__init__
和__del__
;C++ 中的ClassName(parameters)
和~ClassName()
),这使得它们在语言层面就与其他方法区分开来。
因此,在这单独介绍构造器和析构器
继承、封装与多态
继承(Inheritance)、封装(Encapsulation)和多态(Polymorphism)是面向对象编程的核心特性,因为有了这些特性及其结合,基于面向对象思想的编程方式可以很大程度上帮助我们降低代码的复杂性,从而增强代码的可重用性和可维护性。
继承是一种使一个类(称为子类或派生类)能够继承另一个类(称为父类或基类)的属性和方法的机制。子类继承了父类的所有特征,并可以添加新的特征或修改继承来的行为以满足新的需求。例如,如果有一个基类 Vehicle
,它有通用属性如车轮子数和方法如启动和停止,一个子类 Car
可以继承 Vehicle
,并添加特有属性如车门数和方法如倒车等。通过使用继承,我们可以创建一个层次结构的类,使得代码更易于管理和扩展,同时避免了代码的重复。
封装是一种设计策略,用来将数据(属性)和功能(方法)捆绑在一起作为一个独立的单元或对象,并对对象的信息进行隐藏和保护。这意味着类的内部工作方式可以独立于外部界面,外部代码不能直接访问对象的内部结构。封装的一个主要好处是它减少了系统各部分之间的相互依赖性,增加了代码的安全性。在某些编程语言中(如 C++),可以通过使用访问控制关键字,如私有(private
)、受保护(protected
)和公开(public
),控制类成员的可访问性。Python 采用了更为灵活(也有人认为是更不正式)的方式来表示私有或受保护的属性和方法。默认情况下,Python 中的类的所有成员都是公开的,可以从类的外部直接访问。如果想要表示某个变量或方法只有类及其子类可以访问,可以通过在其变量名或方法名前加一个下划线(_
)。若要创建一个私有成员,可以在名称前添加两个下划线(__
)。
在 Python 中,如果定义了一个以两个下划线(__
)开始的属性或方法,就会触发 Python 的名称改编(name mangling)过程,解释器将修改属性名以包含类名作为前缀,从而在实际上实现属性或者方法的私有功能。如 __attribute
,Python 解释器会自动将这个名称转换为 _ClassName__attribute
,其中 ClassName
是定义该属性的类的名称。这个改编的名称包含了一个前缀,它是基于类名的,这就使得从类的外部直接访问这个属性变得相对困难。例如:
1class MyClass:
2 def __init__(self):
3 self.__attribute = 220804
4
5# 外部尝试访问
6instance = MyClass()
7print(instance.__attribute)
这个程序会抛出如下异常:
1Traceback (most recent call last):
2 File "c:\Users\rolfz\Desktop\error_handling\err.py", line 8, in <module>
3 print(instance.__attribute)
4 ^^^^^^^^^^^^^^^^^^^^
5AttributeError: 'MyClass' object has no attribute '__attribute'. Did you mean: '__getattribute__'?
这里,因为在执行该段代码时,解释器将 __attribute
属性改名为 _MyClass__attribute
,因此,会抛出如上的属性错误(AttributeError
),提示 MyClass
类的实例 instance
没有一个名为 __attribute
的属性。这里就体现出了直接访问这个 __attribute
的难度。那么为什么说是相对难度呢?因为,只要我们知道了改编后的名称,即 _MyClass__attribute
,那么在外部调用时,使用该新名字就能访问到相应的属性值。比如:
1print(instance._MyClass__attribute) # result: 220804
可以看出,Python 中的私有变量更多的是一种约定,并不能像 C++ 中的 private
关键字那样提供严格的访问控制。但它也通过使属性和方法不那么容易从类外部直接访问来减少了误用的可能性。
多态是面向对象编程中的一个重要特性,它允许使用统一的接口表示不同的基础形态(数据类型)。这意味着可以通过同一接口调用不同类的方法,而实际执行的是与调用对象的实际类型相对应的方法。换句话说,多态让我们可以用同一个接口来调用不同类的同名方法,而程序在运行时会自动选择合适的方法来执行,这个选择基于对象的实际类型。这就像是不同的人在听到“开始”这个指令时,根据各自的职责开始不同的任务:警察可能开始巡逻,教师可能开始上课,学生们开始学习。
在编程中,多态通常通过“重载(Overloading)”(多个同名函数但参数不同)和“重写(Overriding)”(子类重新定义继承自父类的行为)来实现。
重载和重写这两种技术虽然名称相似,但在功能上有明显的区别:
-
重载:在传统的面向对象编程中,特别是在像 Java 或 C++ 这样的语言中,发生在同一个类中,涉及到创建多个同名的方法,但这些方法的参数类型、个数或者顺序不同。这允许同一个方法名称在不同的上下文中根据输入参数的不同执行不同的功能。不过,值得说明的是,Python 似乎并不支持传统的方法重载,但可以通过默认参数、关键字参数或可变参数来模仿重载行为。
-
重写:发生在父类和子类之间,子类重新定义从父类继承的方法。通过重写,子类可以提供特定的实现细节,这使得在调用子类的该方法时,可以展现和父类不同的行为。
关于多态,我们来看一个具体的例子。假设我们有一个基类叫做 Animal
,它有一个方法叫做 speak()
。然后,我们有两个继承自 Animal
的子类,分别是 Dog
和 Cat
。
1# 定义一个基类 Animal
2class Animal:
3 def speak(self):
4 raise NotImplementedError("Subclasses must implement this method")
5
6# 定义 Dog 类继承自 Animal
7class Dog(Animal):
8 def speak(self):
9 return "Bark!"
10
11# 定义 Cat 类继承自 Animal
12class Cat(Animal):
13 def speak(self):
14 return "Meow!"
15
16# 函数,用于调用任何 Animal 类型的 speak 方法
17def make_animal_speak(animal):
18 return animal.speak()
19
20# 创建 Dog 和 Cat 的实例
21my_dog = Dog()
22my_cat = Cat()
23
24# 测试这些实例
25dog_sound = make_animal_speak(my_dog) # 输出 "Bark!"
26cat_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),即每个单词的首字母大写,不使用下划线。类定义的基本语法如下:
1class ClassName:
2 # 类体定义
class
这个关键字标志着接下来的代码块将定义一个新的类。类定义以类名开始,类名后面紧跟一个冒号 :
,宣告了类体的开始。类体是一个缩进的代码块,包含了类的方法和属性定义。
Python 中类本身也是一个对象。每当我们使用 class
关键字定义新类时,Python 实际上是在创建一个对象。这个对象(类)自身具有创建新对象(实例)的能力,因此,类本质上是对象的“工厂”。这是 Python 中一切皆对象理念的一个重要体现。
运用 class
关键字定义新类时,Python 实际上是在创建一个对象。这也意味着类本身也是对象(实例)。那么这个新类是谁的对象呢?
该问题涉及到 Python 对象模型中比较深层次的概念。在 Python 中,类本身也是对象,这是通过元类(metaclass)实现的。元类是创建类的“类”,就像类是创建实例的模板一样。默认情况下,Python 中所有新定义的类都是 type
这个元类的实例,包括 Python 的内建类型如 int
、str
等。因此,当咱们定义一个新的类时,实质上是 type
类在创建一个新的对象。
1class MyClass:
2 pass
这个定义等价于:
1MyClass = type('MyClass', (), {})
这里,type
有三个参数,分别为:
- 类名(
MyClass
):新类的名称。 - 基类的元组(
()
):新类的基类集合,空元组意味着基类是object
。 - 字典(
{}
):包含类体定义的命名空间,比如方法和类变量。
这意味着 MyClass
是 type
的一个实例,就像 MyClass
的实例是 MyClass
类的对象一样。这构成了 Python 中一致的对象模型:所有东西都是对象,包括类本身。下面呈现了一个使用 type
动态创建类的示例:
1# 动态创建一个类
2MyDynamicClass = type('MyDynamicClass', (object,), {'x': 10, 'y': 20})
3
4# 实例化这个类
5instance = MyDynamicClass()
6print(instance.x, instance.y) # 输出 10 20
这个等价于如下传统的类定义方式创建的 MyDynamicClass
类:
1class MyDynamicClass:
2 x = 10 # 类属性
3 y = 20 # 类属性
4
5instance = MyDynamicClass()
6print(instance.x, instance.y) # 输出 10 20
虽然 type
是大多数类的默认元类,但 Python 也允许咱们定义自己的元类。这通常是通过继承 type
并重写 __new__
或 __init__
方法来完成的,使咱们可以在创建类时拦截和修改类的创建过程。比如:
1class Meta(type):
2 def __new__(cls, name, bases, dct):
3 # 可以在这里修改类的定义
4 return super().__new__(cls, name, bases, dct)
5
6class MyClass(metaclass=Meta):
7 pass
在这个示例中,MyClass
通过指定 metaclass=Meta
使用了自定义的元类 Meta
而不是默认的 type
。这允许 Meta
在 MyClass
被创建时修改其行为。
值得进一步说明的是,type
本身也是一个对象,在 Python 中,它是自己的一个实例。这意味着 type
不仅是创建其他类的工具,同时也描述了自己的结构。这种情况是一种特殊的自引用情形,可以理解为 type
是自己的元类:
1type(type) is type # 返回 True
至于这种特殊的自引用特性到底是如何实现的,请查看和阅读 Cpython 的源代码。我也不懂😇。
类对象允许我们进行如下操作:
- 属性访问:访问类变量或通过类方法定义的计算属性。
- 函数调用:调用类方法。
- 实例化:通过调用类(如调用函数那样)来创建新的实例。
自定义的类在 Python 中的地位与内置类型如 int
和 str
相同。正如可以创建 int
或 str
的实例一样,我们也可以创建由自定义类定义的类型的实例。这种设计将自定义类型提升到与 Python 的内置类型同等的层次,使得使用自定义类型就像使用内置类型一样。
类体的定义
除了类的声明(class MyClass:
),类体是定义类的行为和状态的核心部分,它包含了数据属性(变量)和函数(方法)。
- 属性(Attributes)主要包括两类:
- 类变量:在类中定义,为类的所有实例所共享。用于存储与类相关的数据,而非单个实例相关的数据。
- 实例变量:通常在构造器(
__init__
方法)中通过self
参数定义。实例变量是每个类的实例独立拥有的数据。
- 方法(Methods)大致可以分为以下四类:
- 实例方法:定义操作实例数据的函数。第一个参数总是
self
,它代表类的一个实例本身。 - 类方法:使用
@classmethod
装饰器定义,其第一个参数是cls
,代表类本身。类方法可以访问类变量,但不能访问任何单独实例的数据。 - 静态方法:使用
@staticmethod
装饰器定义,不接受self
或cls
参数。静态方法不能访问类或实例的任何属性,主要用于放置不需要访问任何实例或类数据的工具性函数。 - 特殊方法(Magic Methods):如
__init__
(构造器)、__str__
(转换为字符串)、__repr__
(官方字符串表示)等。这些方法有特殊的名称和用途,是由 Python 的语法和行为规则预定义的。
- 实例方法:定义操作实例数据的函数。第一个参数总是
我们仍然以游戏角色为例,来进一步解释和理解类体的内容:
1class Character:
2 level_up_experience = 100 # 类变量,所有实例共享,设定升级所需的经验值
3
4 def __init__(self, name, health, experience):
5 self.name = name # 实例变量,每个角色的名字
6 self.health = health # 实例变量,每个角色的健康点数
7 self.experience = experience # 实例变量,每个角色的经验值
8
9 def attack(self, other):
10 """实例方法,角色可以攻击其他角色"""
11 if other.health > 0:
12 damage = 10
13 other.health -= damage
14 print(f"{self.name} attacks {other.name} causing {damage} damage points.")
15 self.gain_experience(10)
16
17 @classmethod
18 def change_level_up_experience(cls, new_experience):
19 """类方法,允许修改升级所需的经验值"""
20 cls.level_up_experience = new_experience
21 print(f"Level up experience is now set to {new_experience}.")
22
23 @staticmethod
24 def game_info():
25 """静态方法,提供游戏的一些基本信息,不依赖于类或实例的状态"""
26 return "This is a role-playing game."
27
28 def gain_experience(self, points):
29 """实例方法,角色通过活动获得经验值"""
30 self.experience += points
31 print(f"{self.name} gains {points} experience points.")
32 if self.experience >= Character.level_up_experience:
33 self.level_up()
34
35 def level_up(self):
36 """实例方法,角色升级"""
37 self.health += 50
38 print(f"{self.name} levels up! Health increased to {self.health}.")
39
40 def __str__(self):
41 """特殊方法,定义角色的字符串表示形式"""
42 return f"Character({self.name}, Health: {self.health}, Experience: {self.experience})"
下图描述了 Charactor
类的组成:
Character
类的类体定义基本上包含了 Python 类体定义的常见组成部分,如类变量、实例变量、实例方法、类方法、静态方法和特殊方法。每个组成部分都扮演着特定的角色,共同定义了一个游戏中角色的行为和状态。
类变量的定义
level_up_experience
在 Character
类中是类变量,用于定义所有角色升级所需的经验值。在 Python 中,类变量是定义在类的所有实例之间共享的变量。这些变量通常用于存储与类相关的数据,而非单个实例独有的数据。因为它们的行为与实例变量有显著不同,尤其是在涉及继承和方法访问这些变量时,因此,有必要进一步了解类变量的定义规则。
类变量应在类定义的顶层定义,即在任何方法之外。其赋值与在 Python 中定义全局变量或局部变量的方式相似,采用标准赋值语法,即使用赋值符号(=
)进行赋值。这样,类变量属于类的命名空间,而不是某个具体实例的命名空间。这与实例变量不同,实例变量在类的构造器(如 __init__
方法)中定义,使用 self
关键字并且每个实例维护自己的一份拷贝。修改一个实例的实例变量不会影响到其他实例。类变量的共享特性决定了类变量的值在一个实例中被修改时,这个修改对所有其他实例都将可见。这种行为使类变量适用于跨多个实例需要保持一致的数据。访问方式上,类变量可以通过类本身直接访问,也可以通过类的任何实例访问。然而,推荐使用直接访问的方式,即以类名称来访问类变量(如,Character.level_up_experience
),以区分类变量和实例变量,并避免在继承中可能出现的混淆。由于类变量在类的命名空间中,因此类变量可以在类中的任何方法(实例方法、类方法或静态方法)中使用。其实,在类方法中访问类变量尤为常见,因为类方法通常处理与类状态相关的操作。例如,在该示例中,类方法 change_level_up_experience
修改了类变量 level_up_experience
的值,达到更新升级所需要的经验值的目的。
实例变量的定义
name
,health
以及 experience
在 Character
类中均是实例变量。name
变量用于定义角色的名字,为每个角色实例所独有。health
变量用于定义角色的健康点数,代表角色的当前健康状态。而 experience
用于定义角色的经验值,当经验值达到一定数值时,角色将升级。
如前所述,这些实例变量在类的构造器( __init__
方法)中初始化,它们为每个角色实例提供了独立的状态数据。注意,在构造器参数中有一个比较特殊的参数,即 self
关键字。self
在 Python 中代表当前对象的实例,是实例方法(包括构造器)的第一个参数。通过使用 self
,我们可以在类的其他方法中引用实例变量。比如说:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name # 实例变量,存储角色名
4 self.health = health # 实例变量,存储角色健康点数
5 self.experience = experience # 实例变量,存储角色经验值
6
7 def display_info(self):
8 # 方法中访问实例变量
9 print(f"Name: {self.name}, Health: {self.health}, Experience: {self.experience}")
这里的 display_info
方法中,我们通过 self.name
,self.health
,self.experience
分别访问了对应的实例变量。假设我们有一个 Character
类的实例 hero
:
1hero = Character("Hero", 100, 50)
那么我们在使用 display_info
方法时(hero.display_info()
),将输出: Name: Hero, Health: 100, Experience: 50
。
在 Python 中,self
是一个约定俗成的名称,用于引用类的当前实例。从技术上讲,虽然咱们可以用其他任何名称替代 self
,但使用 self
是 Python 社区的标准做法,有助于维护代码的可读性和一致性,我们就不要修改了。另外,对于类的实例方法(包括构造器和析构器),self
在参数的中的位置也很重要,必须是方法的第一个参数。这不仅是一个强制性的约定,而且是 Python 解释器设计的要求。Python 解释器将方法调用转换为内部操作,自动将调用该方法的对象作为第一个参数传递。这一机制确保了方法能够访问其所属实例的属性和其他方法,从而操作和修改实例的状态。具体来说,当咱们写 obj.method(arg1, arg2)
时,Python 在内部实际上将其转换为 Class.method(obj, arg1, arg2)
,其中 obj
是 self
。这个转换是依赖于 Python 解释器自动进行,要求方法定义中的第一个参数必须接受这个对象引用。这种转换完全由 Python 解释器自动处理,并且强制要求方法定义中的首个参数接受这个对象引用。因此,当咱们在类中定义方法时,应确保将接收实例引用的参数(self
)置于参数列表的最前面。
除了传统的位置参数(name
, health
, experience
)和 self
这个比较特殊的参数外,由于类中的方法实质上就是函数,因此,我们可以在其中设置和使用不同类型的参数,包括关键字参数和默认参数及可变参数。其效果与普通函数中的参数效果一致。具体可以参考理解 Python 函数不同类型的参数及其使用。下面分别列举了使用默认参数、关键字参数以及可变参数的三个例子:
1# 默认参数的使用意味着当创建类的实例时,可以不用提供所有参数,只提供那些需要与默认值不同的参数。
2class Character:
3 def __init__(self, name, health=100, experience=0):
4 self.name = name
5 self.health = health
6 self.experience = experience
7
8# 可以只提供名字,健康值和经验值将使用默认值
9hero = Character("Hero")
1# 关键字参数在构造器中的使用允许在不修改构造器定义的情况下传递额外的数据。这些数据可以用于动态设置实例属性或进行其他初始化任务。
2class Character:
3 def __init__(self, name, **kwargs):
4 self.name = name
5 self.health = kwargs.get('health', 100)
6 self.experience = kwargs.get('experience', 0)
7 self.inventory = kwargs.get('inventory', [])
8
9# 创建角色时可以传递任何额外的参数
10hero = Character("Hero", health=150, experience=10, inventory=["sword", "shield"])
1# 在不确定会传递多少参数,或者想要接受任意数量参数时可以使用可变参数。
2class Character:
3 def __init__(self, name, *args):
4 self.name = name
5 self.abilities = list(args) # 将传入的能力转换为列表
6
7# 创建一个角色,可以列举出任意多的能力
8hero = Character("Hero", "strength", "agility", "wisdom")
在继承、封装与多态中,我们介绍过 Python 的类成员默认是公开的,但也提供了一些约定的方式实现实例变量和方法的访问控制。比如运用一个下划线(_
)前缀来表示变量或方法是受保护的;双下划线(__
)前缀用来声明私有成员。这里,我们再举两个例子,以便加深印象。
1class Character:
2 def __init__(self, name):
3 self._name = name # 受保护的实例变量
4
5 def _display_name(self):
6 # 受保护的方法
7 return f"This character is named {self._name}."
8
9# 外部仍然能访问
10hero = Character("Hero")
11print(hero._name) # 打印出:Hero
12print(hero._display_name()) # 打印出:This character is named Hero.
1class Character:
2 def __init__(self, name):
3 self.__name = name # 私有实例变量
4
5 def __display_name(self):
6 # 私有方法
7 return f"This character is named {self.__name}."
8
9# 外部尝试访问或修改会失败
10hero = Character("Hero")
11# print(hero.__name) # 报错,无法访问
12# print(hero.__display_name()) # 报错,无法访问
13print(hero._Character__name) # 打印出: Hero
14print(hero._Character__display_name()) # 打印出:This character is named Hero.
可以看出,运用 _
来表示受保护的变量真的是一种约定,没有任何的约束,外部仍然可以轻松地直接访问。相对而言,使用 __
前缀来声明私有成员时,约束性稍微强了一些。我们不能直接访问该类的私有成员。如果,实在是有必要,我们只能通过在变量或者方法名前加上以 _
开头的类名的方式访问,如 hero._Character__name
和 hero._Character__display_name()
。
在 Python 中,名称前加单下划线(_
)或双下划线(__
)的机制主要用于类的属性和方法,以指示其访问级别。但是,这种机制也适用于模块级别的函数和变量,用以控制它们的可见性和访问性,尤其是在模块被导入时。然而,它们的含义和效果有所不同。
-
单下划线(
_
)开头: 在函数中,当函数名以单下划线(_
)开头时,它主要是作为一个内部使用的信号。虽然这不会影响函数的访问性(即它们仍然可以从模块外部被调用),但它是一个常见的约定,表明这个函数不应该被模块外的代码直接调用。在模块中,用于模块内部使用的函数或变量通常以单下划线开头。这是一个约定,向外部用户表明这些函数或变量是内部的,可能在未来版本中更改,不应在模块外部直接使用。 -
双下划线(
__
)开头: 在函数中,Python 中的普通函数如果以双下划线(__
)开头,它不会像在类属性和方法中那样触发名称改编(name mangling)。双下划线在普通函数中很少使用,因为它并不提供真正的访问保护。
以下示例展示了函数名和变量名前的下划线的使用:
1# module.py
2
3def _internal_function():
4 print("这是一个内部函数。")
5
6def __name_mangling_not_applied():
7 print("这个函数的名称没有被改变。")
8
9def public_function():
10 print("这是一个供外部使用的函数。")
11 _internal_function()
12
13class MyClass:
14 def __init__(self):
15 self.__private_var = "这是私有的"
16
17 def __private_method(self):
18 print("这是一个私有方法")
19
20 def display(self):
21 print(self.__private_var)
22 self.__private_method()
23
24
25# 使用实例
26if __name__ == "__main__":
27 _internal_function() # 从模块内部调用,不推荐外部调用
28 __name_mangling_not_applied() # 没有应用名称改编
29 obj = MyClass()
30 obj.display()
如果想从外部其他文件导入相应的函数,都能正常工作,但不推荐,比如说:
1# 其他文件尝试导入和使用:
2from module import public_function, _internal_function, __name_mangling_not_applied
3
4public_function() # 正常使用
5_internal_function() # 正常使用,但不建议这么做,因为它违反了约定
6__name_mangling_not_applied() # 正常使用,但真的没啥用,也不常见,所以也不推荐
实例方法的定义
接着我们介绍类体中的实例方法的定义。下面这段代码呈现的是 Character
类的实例方法部分,以便查看。
1class Character:
2 def attack(self, other):
3 """实例方法,角色可以攻击其他角色"""
4 if other.health > 0:
5 damage = 10
6 other.health -= damage
7 print(f"{self.name} attacks {other.name} causing {damage} damage points.")
8 self.gain_experience(10)
9
10 def gain_experience(self, points):
11 """实例方法,角色通过活动获得经验值"""
12 self.experience += points
13 print(f"{self.name} gains {points} experience points.")
14 if self.experience >= Character.level_up_experience:
15 self.level_up()
16
17 def level_up(self):
18 """实例方法,角色升级"""
19 self.health += 50
20 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()
方法:
1class Character:
2 level_up_experience = 100 # 类变量,所有实例共享,设定升级所需的经验值
3
4 @classmethod
5 def change_level_up_experience(cls, new_experience):
6 """类方法,允许修改升级所需的经验值"""
7 cls.level_up_experience = new_experience
8 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()
的访问和修改类变量或者调用其他的类方法。我们来看一下下面这个示例:
1class Character:
2 total_characters = 0 # 类变量,记录游戏中角色的总数
3 level_up_experience = 100 # 类变量,设定升级所需的经验值
4
5 def __init__(self, name, experience=0):
6 self.name = name
7 self.experience = experience
8 self.level = 1
9 Character.total_characters += 1 # 每创建一个角色,总数加一
10
11 @classmethod
12 def change_level_up_experience(cls, new_experience):
13 """类方法,修改所有角色的升级经验阈值"""
14 cls.level_up_experience = new_experience
15 print(f"Updated level up experience to {new_experience} for all characters.")
16
17 @classmethod
18 def reset_game(cls):
19 """类方法,重置游戏状态,包括角色总数和默认经验值"""
20 cls.total_characters = 0
21 cls.level_up_experience = 100
22 print("Game has been reset. All character counts and experiences are set to default.")
23
24 @classmethod
25 def check_and_reset_game(cls):
26 """检查是否需要重置游戏,并调用重置"""
27 if cls.total_characters > 10: # 假设超过10个角色就重置游戏
28 cls.reset_game()
29
30 def gain_experience(self, points):
31 """实例方法,增加经验并判断是否升级"""
32 self.experience += points
33 if self.experience >= Character.level_up_experience:
34 self.level_up()
35
36 def level_up(self):
37 """实例方法,处理角色升级逻辑"""
38 self.level += 1
39 print(f"{self.name} has leveled up to level {self.level}!")
40
41# 使用示例
42for i in range(12): # 创建多个角色
43 char = Character(f"Hero{i}")
44Character.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()
方法:
1class Character:
2 @staticmethod
3 def game_info():
4 """静态方法,提供游戏的一些基本信息,不依赖于类或实例的状态"""
5 return "This is a role-playing game."
在使用场景上,静态方法通常用于以下情景:
- 用于大型类中,为类提供相关功能,但又不需要类或实例的状态(即,属性或方法)时。
- 作为工具函数,用于执行某些任务,比如格式化数据,处理输入等,这些不直接与类的任何实例的内部状态相关的操作。
相较于其他方法,静态方法还有一个明显的特征。在使用静态方法时,我们并不需要实例化对象。比如说在 Character
类这个例子中,我们可以通过 print(Character.game_info())
直接打印出该游戏的信息(This is a role-playing game.
),而无需创建 Character
类实例。这种机制直接的好处是,可以节省内存的使用。
为了加深影响,我们可以再看一个例子:
1class MathUtility:
2 @staticmethod
3 def add_numbers(a, b):
4 return a + b
5
6 @staticmethod
7 def multiply_numbers(a, b):
8 return a * b
MathUtility
类中定义了两个静态方法:add_numbers()
和 multiply_numbers()
。我们可以按照如下方式使用这两个静态方法:
1# 使用静态方法
2result_add = MathUtility.add_numbers(10, 5) # 无需创建类实例
3result_multiply = MathUtility.multiply_numbers(10, 5)
4
5print("Addition:", result_add) # 输出 15
6print("Multiplication:", result_multiply) # 输出 50
如果 add_numbers()
和 multiply_numbers()
为普通的实例方法:
1class MathUtility:
2 def __init__(self, a, b):
3 self.a = a
4 self.b = b
5
6 def add_numbers(self):
7 return self.a + self.b
8
9 def multiply_numbers(self):
10 return self.a * self.b
那么,我们要使用这两个方法时,只能先对 MathUtility
类进行实例化,然后调用这两方法:
1# 创建 MathUtility 类的实例
2math_util = MathUtility(10, 5)
3
4# 使用实例方法
5result_add = math_util.add_numbers() # 调用实例方法
6result_multiply = math_util.multiply_numbers()
7
8print("Addition:", result_add) # 输出 15
9print("Multiplication:", result_multiply) # 输出 50
在此情景下,如果仍然按照静态方法的使用方式,比如:
1result_add = MathUtility.add_numbers(10, 5) # 没有创建类实例
通常会抛出以下错误:
1TypeError: 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 可以正确识别它们,而且也使得它们在视觉上与普通方法区分开来。
如何理解这里的自动调用机制?我们来看一下如下这个示例:
1class Character:
2 def __init__(self, name, health=100):
3 self.name = name
4 self.health = health
5
6 def __str__(self):
7 return f"{self.name}, Level {self.health}"
当我们创建一个 Character
类的实例时,例如 Character("Arthur", 200)
,Python 解释器执行以下步骤:
- 内存分配:首先,解释器为新对象分配内存。这个新对象是类的一个实例,拥有类定义中声明的所有属性和方法。
- 调用
__init__
:一旦内存分配完成,Python 自动调用类的__init__
方法。此时,self
参数指向新创建的内存地址(即新对象),而其他参数则是我们传递给类构造器的值,如name
和health
值。在这分别是"Arthur"
和200
。 - 初始化对象:
__init__
方法内的代码负责设置对象的初始状态。这通常包括给实例变量赋值和执行必要的初始化操作,这里是通过在__init__
内部设置self.name
和self.level
实现。在__init__
方法执行完毕后,对象被认为是准备好的,可以被用于后续操作。
这样的机制设计,可以确保每个对象在使用前都被正确初始化。同时也确保了所有对象的创建和初始化过程都是一致的,有助于维护类的行为一致性。
所以说,在 Python 中,当我们实现或修改特殊方法时,实际上是在利用面向对象编程中的多态性概念,通过重写这些方法为咱们的类定制一些行为。因为特殊方法,如 __add__
、__str__
、__getitem__
等,已经在更高级别的基类(如 object
类)中预定义了,通过在新的类中重新定义这些方法,实际上是在重写继承自基类的方法。至于要重写那些特殊方法,需要根据新类将要实现哪些行为来确定。比如说,根据类的预期功能决定实现相应的特殊方法。如果新类代表一个序列类型,我们可能会实现 __len__
和 __getitem__
方法来支持序列协议。或者,如果想让新类的实例能够通过调用方式使用,那么我们应该实现 __call__
方法。
下面我们将对一些常用的特殊方法进行介绍。
构造和初始化
在 Python 中,__init__
是一个极其关键的特殊方法,主要负责在对象的创建后立即进行初始化。这个方法的核心功能是设置对象的初始状态,具体包括属性的赋值和执行必要的初始化操作。作为对象生命周期中第一个自动调用的方法,__init__
扮演了构造器的角色。通过 __init__
方法,创建对象时所需的数据可以作为参数传递进来。这些参数随后被用于初始化对象的各个属性或进行启动时必须的任何操作。这样的设计确保了对象在使用前已经被正确地配置和准备好,为对象的后续使用提供了一致的基础。注意,由于__init__
方法的主要目的是初始化对象,因此,不应该返回任何值。其基本的语法结构如下:
1class ClassName:
2 def __init__(self, param1, param2):
3 self.param1 = param1
4 self.param2 = param2
我们仍以 Character
类为例。
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name # 实例变量,每个角色的名字
4 self.health = health # 实例变量,每个角色的健康点数
5 self.experience = experience # 实例变量,每个角色的经验值
6
7 def __str__(self):
8 """特殊方法,定义角色的字符串表示形式"""
9 return f"Character({self.name}, Health: {self.health}, Experience: {self.experience})"
在此例子中,Character
类有三个属性,即,name
, health
, experience
。当创建一个新的 Character
实例时,需要提供 name
, health
, 以及 experience
三个参数。这些参数在 __init__
方法中被接收并赋值给相应的实例属性。以下示例展示了 __init__
方法的使用:
1arthur = Character("Character", 100, 0)
2
3print(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__
这两个特殊方法的区别以及它们的具体使用场景:
1class Character:
2 def __init__(self, name, level, items=None):
3 self.name = name
4 self.level = level
5 self.items = items if items is not None else []
6
7 def __str__(self):
8 return f"{self.name}, Level {self.level}"
9
10 def __repr__(self):
11 return f"Character('{self.name}', {self.level}, {self.items})"
这里,__str__
方法提供了一个用户友好的字符串表示,适用于最终用户需要的输出,如在游戏界面或日志文件中显示角色信息:
1character = Character("Aragorn", 5, ["Sword", "Shield"])
2print(str(character)) # 输出: Aragorn, Level 5
而 __repr__
方法则提供一个更正式的字符串表示,其目标是明确无误,甚至可以用来重新创建该对象:
1character = Character("Aragorn", 5, ["Sword", "Shield"])
2print(repr(character)) # 输出: Character('Aragorn', 5, ['Sword', 'Shield'])
在这个例子中,__repr__
输出了足够的信息,包括角色的名字、等级和具体的装备列表。这样的信息量使得开发者可以使用这个字符串来理解或重构对象的精确状态。
如何理解 __repr__
方法的输出应该使得开发者能够“理解或重构对象的精确状态”?
在 Python 中,__repr__
方法设计的核心思想是让它返回一个对象的精确表示,理想情况下,这个表示应该足以通过执行相同的字符串来重建该对象。这种能力使得开发者可以从打印或日志中直接获取对象的详细状态,并用它来复现或调试程序的状态。这也是为什么 __repr__
的返回值通常被写成是一个有效的 Python 表达式,如 Character('Aragorn', 5, ['Sword', 'Shield'])
,这个表达式包含了类名 Character
以及相应的所有参数 ('Aragorn', 5, ['Sword', 'Shield'])
。所以说,开发者可以借助于这个表达式,重建 character
这个对象的状态。但是,开发者仅凭此示例中的 __str__
方法返回的信息则很难实现同样的目的。
我们可以再举一个例子:假设咱们有一个复杂的数据结构,如一个表示几何图形的类 Rectangle
:
1class Rectangle:
2 def __init__(self, width, height):
3 self.width = width
4 self.height = height
5
6 def __repr__(self):
7 return f"Rectangle({self.width}, {self.height})"
如果一个 Rectangle
对象的 __repr__
方法被调用并返回 Rectangle(10, 20)
,这条信息不仅告诉了我们这是一个宽度为 10、高度为 20 的矩形,而且这条字符串本身就是一个可以用来创建相同矩形的 Python 命令。这对于调试非常有用,尤其是在处理多个对象或复杂系统时,能够快速地通过日志或错误输出确定程序状态。此外,这也符合“代码即文档”的理念,让代码本身就足够表达意图和状态,减少了额外文档的需要。
总结下,关于 __repr__
方法的设计,应该遵循如下标准:
- 精确性:
__repr__
应提供所有关键信息,以充分描述对象的当前状态。这包括所有重要的属性值和可能影响对象行为的内部状态。 - 明确性:输出应清晰到足够让其他开发者(或未来的你)理解对象的构造和行为,无需查看其他文档或源代码。
- 可执行性:在许多情况下,
__repr__
的输出格式是一个有效的 Python 表达式,即复制粘贴这个表达式到 Python 解释器或代码中,应能重新创建出相同的对象。例如,eval(repr(obj))
应该能够创建一个与obj
状态相同的新对象。
在设计类时,通常建议至少实现 __repr__
,因为它的目的是无歧义的重现对象;如果可能的话,也实现 __str__
提供更友好的用户界面。在实际开发中,这样的实践可以大大增强代码的可维护性和调试效率,同时也提升了用户交互的友好性。
属性访问
在面向对象编程中,当咱们定义一个类时,通常会包含实例属性和类属性,如 Character
类中的 name
,experience
,level
和 level_up_experience
。这些属性实际上是可以通过实例或类访问的变量,它们存储了对象的内部状态。因为它们直接影响到对象的行为和数据的一致性,因此,管理这些属性的访问和修改是面向对象编程中一个重要的环节。
直接访问
在 Python 中,属性访问主要有两种基本方式。一种是直接访问和修改属性。我们可以直接通过对象来获取或设置属性的值。例如,想访问 Character
类的实例对象中的 name
属性,我们可以通过 instance.name
的方式来访问和修改该属性:
1arthur = Character("Arthur", 100, 0)
2print(arthur.name)
这种方法虽然简单快捷,但它不能控制外部对属性的修改。所以说直接访问属性可能会破坏封装性。
封装性作为面向对象编程中的一个核心概念,它要求一个对象的状态(属性)应该被隐藏或保护起来,不应该被外部直接访问。封装性有助于保护对象内部的实现细节,确保对象数据的完整性,并使对象易于使用和维护。
让我们通过前面提到的 Character
类来探讨为什么直接访问属性可能会破坏封装性。
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name # 角色的名字
4 self.health = health # 角色的健康点数
5 self.experience = experience # 角色的经验值
在这个类中,属性 name
, health
, 和 experience
都是公开的,意味着它们可以在类的外部被直接访问和修改:
1# 创建角色实例
2arthur = Character("Arthur", 100, 0)
3
4# 直接修改属性
5arthur.health = -50 # 将健康点数设置为负数
6arthur.experience = -100 # 经验值也设置为一个不合逻辑的负数
这里,我们通过直接访问的方式将健康点数和经验值都设置成一个不合逻辑的负数。在游戏逻辑中,健康点数和经验值通常不应该是负数。直接访问使得这些属性可以被随意设置为无效值,从而可能会导致程序出错或行为异常。此外,直接访问还有可能降低程序的可维护性。当类的内部实现依赖于特定的属性状态时,直接修改这些属性会增加维护难度。假设 Character
类中有一个内部机制,它依赖于健康点数(health
)来决定角色是否存活,并在健康点数达到零或以下时触发某些特定的行为,比如角色死亡:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name
4 self.health = health
5 self.experience = experience
6
7 def set_health(self, value):
8 if value < 0:
9 self.health = 0
10 print(f"{self.name} has died.")
11 elif value > 100:
12 self.health = 100
13 else:
14 self.health = value
15
16 def add_experience(self, value):
17 self.experience += value
18 while self.experience >= 100:
19 self.experience -= 100
20 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
类的内部逻辑(如角色死亡或者升级)依赖于特定的属性状态(health
和 experience
的值)。如果允许直接访问 health
属性,玩家或者游戏中的其他逻辑可能会将其设置为不合适的值,比如:
1# 创建角色实例
2arthur = Character("Arthur", 50, 0)
3
4# 直接修改属性
5arthur.health = -20 # 将健康点数设置为负数
6arthur.health = 150 # 健康值超出最大限制
这里,我们将健康点数设置为 -20
。根据游戏的规则,角色应当被视为死亡。然而,直接设置并不会触发 set_health
中定义的死亡逻辑(打印死亡消息并将健康点数设为 0)。这样,角色可能会在游戏中以一个不合逻辑的状态继续存在。而将 health
直接设置为 150 也违背了游戏的规则,因为这个类设计中的健康点数上限为 100。直接设置可能导致游戏的一些特定逻辑(如伤害计算和显示)出现不符合预期的结果。experience
的情况类似,根据类的设计,当角色的经验值超过 100 时,角色应该升级一次或者连续升级多次(每 100 经验升一级)。但是,直接修改 experience
(比如 arthur.experience = 1000
), 并不会触发 add_experience
方法中定义的升级逻辑(经验值重置和打印升级消息)。这会导致角色的经验值和等级之间的不一致,以及不触发与升级相关的其他游戏事件(如能力提升)。
可以看出,通过直接方式访问或者修改属性通常会导致数据不一致、逻辑错误和潜在的状态异常。该种方式并不可取。这也凸显了我们接下来要介绍的第二种属性访问方式的重要性。它不仅可以保护数据完整性,同时还可以维护程序的整体规则和逻辑的一致性。
accessor 和 mutator
第二种属性访问方式主要是通过定义“实例方法”来实现。这些方法不仅为我们提供了访问属性的接口,还可以在访问和修改属性时加入一些逻辑处理,如验证数据或修改相关的其他属性。由于其功能的特殊性,这类方法一般被称为访问器(accessor)和变更器(mutator),或者称为 “getter” 和 “setter” 方法。accessor,顾名思义,允许我们访问类中的属性,而 mutator 则允许我们设置或者修改类中属性的值。
要实现 accessor 和 mutator 模式,我们需要:
- 将属性设置为非公开的。
- 为每个属性编写 accessor 和 mutator 方法。
仍以 Character
类为例,下面运用 accessor 和 mutator 方法来管理角色的属性。为此,我们首先需要将 Character
类的属性 name
、health
和 experience
设置为受保护的属性,即在变量名称前加上单下划线(_
),这样表示这些属性不应该被直接从类的外部访问和修改。
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
在此基础上,我们为每个私有属性编写对应的 accessor 和 mutator 方法,以提供访问和修改属性的安全方式。这些方法通常命名为 get_<属性名>
和 set_<属性名>
,分别用于返回属性值和设置新的属性值,比如:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name # 初始化时将属性设置为私有
4 self._health = health
5 self._experience = experience
6
7 # accessor for name
8 def get_name(self):
9 return self._name
10
11 # mutator for name
12 def set_name(self, value):
13 if isinstance(value, str): # 简单的类型检查
14 self._name = value
15
16 # accessor for health
17 def get_health(self):
18 return self._health
19
20 # mutator for health
21 def set_health(self, value):
22 if value < 0:
23 value = 0 # 保护健康值不为负
24 elif value > 100:
25 value = 100 # 限制健康值的最大值
26 self._health = value
27
28 # accessor for experience
29 def get_experience(self):
30 return self._experience
31
32 # mutator for experience
33 def set_experience(self, value):
34 if value < 0:
35 value = 0 # 防止经验值为负
36 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 方法的使用示例:
1arthur = Character("Arthur", 100, 50)
2
3# 使用 getter 方法
4print(arthur.get_name()) # 输出: Arthur
5
6# 使用 setter 方法
7arthur.set_health(150)
8print(arthur.get_health()) # 输出: 100 (因为健康值被限制在100)
9
10arthur.set_experience(-10)
11print(arthur.get_experience()) # 输出: 0 (因为经验值不能为负)
@property
装饰器
虽然传统的 accessor(访问器)和 mutator(变更器)方法提供了一种明确的模式来控制类属性的访问和修改,并允许在这些操作中嵌入额外的处理逻辑,但这种方法通常需要为每个属性编写两个方法:一个用于获取属性值,另一个用于设置新值。这不仅使得代码变得较为冗长,也降低了代码的简洁性,与 Python 语言推崇的“简单明了”的原则也不太一致。
在上述模式中,属性的读取和写入需要通过 instance.get_<属性名>()
和 instance.set_<属性名>(value)
的形式进行,这对于开发者和最终用户而言可能稍显繁琐。如果能有一种方法,既能够保持属性访问和修改的简洁性,又能够融入 accessor 和 mutator 的优势(如逻辑封装和数据验证),那将大大增强代码的可用性和可维护性。
为此,Python 提供了一种解决方案,即使用 @property
装饰器。@property
装饰器允许开发者将类中的方法转变为对应的属性,这些属性仍然执行与 accessor 和 mutator 相同的内部逻辑。这种方法的使用不仅使得属性的访问看起来就像是直接访问变量一样,而且还能在后台自动处理必要的逻辑,如检查数据有效性或执行必要的转换。因此,@property
装饰器是 Python 社区中处理类属性时尤为推崇的一种方法,特别是在需要对属性的读写进行控制或添加额外逻辑时。
以下是使用 @property
装饰器实现的 Character
类:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
6
7 @property
8 def name(self):
9 return self._name
10
11 @name.setter
12 def name(self, value):
13 if not isinstance(value, str):
14 raise ValueError("Name must be a string")
15 self._name = value
16
17 @property
18 def health(self):
19 return self._health
20
21 @health.setter
22 def health(self, value):
23 if value < 0:
24 value = 0
25 elif value > 100:
26 value = 100
27 self._health = value
28
29 @property
30 def experience(self):
31 return self._experience
32
33 @experience.setter
34 def experience(self, value):
35 if value < 0:
36 value = 0
37 self._experience = value
从该版本的 Character
类可以看出,使用 @property
装饰器将 accessor 和 mutator 方法转换为属性非常简单。只需在对应的 accessor 方法前添加 @property
装饰器,并在 mutator 方法前添加 @<property_name>.setter
装饰器。通常,这些方法的命名与它们控制的属性名相同,例如,对于 _name
属性,相应的 accessor 和 mutator 方法命名为 name
;对于 _health
属性,方法命名为 health
,依此类推。除此之外,内部的处理逻辑和不使用 @property
装饰器时的 accessor 和 mutator 方法内部逻辑一致。
使用 @property
装饰器的一个显著好处是,它使得访问属性值就像访问公开属性一样直接,同时允许我们在背后保留必要的逻辑处理(如该例子中的数据验证和条件检查)。比如说,我们创建一个 Character
类的实例,并初始化其属性::
1hero = Character("Arthur", 100, 50)
由于使用了 @property
装饰器,我们可以直接访问这些属性,就像它们是公开的属性一样:
1print(hero.name) # 输出: Arthur
2print(hero.health) # 输出: 100
3print(hero.experience) # 输出: 50
当我们需要修改这些属性时,可以直接为它们赋值,而 @property
装饰器确保调用相应的 mutator 方法来处理赋值操作:
1hero.name = "SunWukong" # 尝试将 name 设置为 "SunWukong"。由于这是一个字符串,赋值将成功。
2hero.health = 110 # 尝试将 health 设置为 110。由于 mutator 方法中限制了健康值的最大值为 100,它将自动调整为 100。
3hero.experience = -10 # 尝试将 experience 设置为 -10。由于 mutator 方法不允许负值,它将自动调整为 0。
我们可以查看下相应的修改后的属性值:
1print(hero.name) # 输出: SunWukong
2print(hero.health) # 输出: 100
3print(hero.experience) # 输出: 0
除了以上方式使用 @property
装饰器,Python 中的属性(properties)具有更为广泛的应用场景。例如,我们可以借助于 @property
装饰器创建只读(read-only)、只写(write-only)和可读写(read-write)的属性。
定义只读属性也很简单,在类中,针对某一个属性只定义 accessor 方法,而不定义对应的 mutator 方法即可。比如说:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
6
7 @property
8 def name(self):
9 return self._name
这里,我们只定义了角色名称的 accessor 方法。显然有:
1hero = Character("Arthur", 100, 50)
2
3print(hero.name) # 输出: Arthur
但是,在此种情况下,我们再想修改角色名称时:
1hero.name = "SunWukong"
解释器会输出属性错误(AttributeError
)的异常:
1Traceback (most recent call last):
2 File "c:\Users\thinkstation\Desktop\testing\property.py", line 44, in <module>
3 hero.name = "SunWukong"
4 ^^^^^^^^^
5AttributeError: property 'name' of 'Character' object has no setter
如果想使某个属性为只写属性,那么我们可以使用 @property
装饰器结合 mutator 方法,而不提供 accessor 方法。这样,该属性只能被设置,而不能被外部直接读取,符合只写属性的定义。比如说游戏角色中有另外一个密码属性:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
6 self._password = None # 私有属性,初始时没有设置
我们可以按照如下方式定义对应的方法:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
6 self._password = None # 私有属性,初始时没有设置
7
8 # 只写属性 password 的 setter
9 @property
10 def password(self):
11 raise AttributeError("This attribute is write-only")
12
13 @password.setter
14 def password(self, value):
15 # 可以在这里加入密码强度验证等逻辑
16 self._password = value
在该例中,password
属性被定义为只写。尝试读取这个属性将会引发异常,而设置这个属性则是允许的:
1hero = Character("Arthur", 100, 50)
2
3# 设置密码
4hero.password = "secure_password123"
5
6# 尝试读取密码
7try:
8 print(hero.password)
9except AttributeError as e:
10 print(e) # 输出: This attribute is write-only
除此之外,使用 @property
装饰器,还可以定义删除(deletion)操作和提供属性的文档说明等功能。比如说:
1class Character:
2 def __init__(self, name, health, experience):
3 self._name = name
4 self._health = health
5 self._experience = experience
6
7 @property
8 def name(self):
9 "The name of the character"
10 return self._name
11
12 @name.setter
13 def name(self, value):
14 self._name = value
15
16 @name.deleter
17 def name(self):
18 print("Deleting name...")
19 del self._name
20
21 @property
22 def health(self):
23 "The health level of the character. Must be between 0 and 100."
24 return self._health
25
26 @health.setter
27 def health(self, value):
28 if value < 0:
29 value = 0
30 elif value > 100:
31 value = 100
32 self._health = value
33
34 @health.deleter
35 def health(self):
36 print("Deleting health...")
37 del self._health
38
39 @property
40 def experience(self):
41 "The experience points of the character."
42 return self._experience
43
44 @experience.setter
45 def experience(self, value):
46 if value < 0:
47 value = 0
48 self._experience = value
49
50 @experience.deleter
51 def experience(self):
52 print("Deleting experience...")
53 del self._experience
在这个 Character
类中,每个属性不仅可以被读取和修改,还可以被删除(通过在对应方法前加上 @<property_name>.deleter
实现)。同时,每个属性都有对应的文档字符串,可以使用 Python 的内置函数 help()
来查看:
1hero = Character("Arthur", 100, 50)
2
3# 删除属性
4del hero.name
5del hero.health
6del hero.experience
7
8# 尝试再次访问属性,会引发错误因为属性已被删除
9try:
10 print(hero.name)
11except AttributeError as e:
12 print(e) # 输出:'Character' object has no attribute '_name'
使用 help()
来查看 Character
类的文档字符串:
1help(Character)
则会显示如下结果:
1Help on class Character in module __main__:
2
3class Character(builtins.object)
4 | Character(name, health, experience)
5 |
6 | Methods defined here:
7 |
8 | __init__(self, name, health, experience)
9 | Initialize self. See help(type(self)) for accurate signature.
10 |
11 | ----------------------------------------------------------------------
12 | Data descriptors defined here:
13 |
14 | __dict__
15 | dictionary for instance variables
16 |
17 | __weakref__
18 | list of weak references to the object
19 |
20 | experience
21 | The experience points of the character.
22 |
23 | health
24 | The health level of the character. Must be between 0 and 100.
25 |
26 | name
27 | The name of the character
此外,我们也可以使用如下的方式,显示对应属性的文档字符串
1# 显示属性的文档字符串
2print(Character.name.__doc__) # 输出:The name of the character
3print(Character.health.__doc__) # 输出:The health level of the character. Must be between 0 and 100.
4print(Character.experience.__doc__) # 输出:The experience points of the character.
descriptors
除了前面三种方式的属性访问外,Python 还为我们提供了一种高级的控制属性行为的方法,即描述符(descriptors)。描述符是一种遵守特定协议(protocol)的类,该协议包括方法 __get__()
, __set__()
, 和 __delete__()
三种特殊方法。通过这些方法,描述符使得我们能够精细控制类属性的访问、修改和删除行为。
从背后的实现上看,Python 中使用 @property
装饰器创建的属性,实际上是在后台使用描述符来实现属性的控制的。这可以从使用 help(Character)
得到的结果中观察到。在这个帮助文档中,我们可以看到 name
,health
以及 experience
被列为数据描述符(Data descriptors)。数据描述符(data descriptors)是同时定义了 __get__()
和 __set__()
方法的描述符。与之相对应的还有非数据描述符(non-data descriptors)。非数据描述符(non-data descriptors)是只定义了 __get__()
方法的描述符。相比而言,数据描述符具有更高的优先级。
现在,我们以 Character
类为基础,创建一个简单的描述符 NonNegative
,来说明描述符的定义和使用。
1class NonNegative:
2 def __init__(self, default=0):
3 self.default = default
4
5 def __get__(self, instance, owner):
6 # 返回实例中存储的值
7 return getattr(instance, self.private_name, self.default)
8
9 def __set__(self, instance, value):
10 # 检查设置的值是否为负,如果是,抛出异常
11 if value < 0:
12 raise ValueError("This value must be non-negative")
13 setattr(instance, self.private_name, value)
14
15 def __set_name__(self, owner, name):
16 # 存储属性的私有名称
17 self.private_name = '_' + name
从定义上看,描述符本质上是类(Class),但是与普通类在功能定位、使用上下文、方法实现以及实例行为等方面还是有一定的区别:
- 功能定位方面:普通类通常封装数据和行为,用来模拟真实世界的对象。而描述符类专门用于管理属性访问行为,它们实现了描述符协议,可以控制属性的获取、设置和删除行为。
- 使用上下文方面:普通类可以直接实例化并使用。描述符类通常被用作另一个类的属性,用来控制那个类的某个属性的行为。
- 方法实现方面:描述符类必须实现描述符协议中的一个或多个方法(
__get__
,__set__
,__delete__
),这是其作为描述符工作的基础。普通类则不需要这些方法,除非特定需求。 - 实例行为方面:在描述符中,
__get__
和__set__
方法的调用是自动的,基于属性访问和属性赋值触发,而普通类的方法调用通常是显式的。
从定义形式上,描述符可以按照如下结构定义:
1class Descriptor:
2 def __init__(self, initial_value=None, name='my_var'):
3 self.value = initial_value # 存储属性的实际数据
4 self.name = name
5
6 def __get__(self, obj, objtype=None):
7 print(f"Getting: {self.name}")
8 return self.value
9
10 def __set__(self, obj, value):
11 print(f"Setting: {self.name} to {value}")
12 self.value = value
13
14 def __delete__(self, obj):
15 print(f"Deleting: {self.name}")
16 del self.value
其中的 __init__()
方法是用来初始化描述符实例的,可以接受一个初始值和一个名称(主要目的是用于调试或日志记录),这里的 value
存储了属性的实际数据。__get__
方法负责返回属性的值,与 accessor 的功能类似。这里的 obj
是访问属性的对象实例,objtype
是拥有该属性的类。这个方法可以用来返回内部存储的值,或基于复杂逻辑计算后的值。__set__
方法主要负责设置属性的新值,与 mutator 的功能类似。obj
是尝试修改属性的对象实例。value
是尝试设置的新值。这个方法可以用来验证新值是否符合要求,或在设置新值前进行必要的处理。__delete__
方法主要负责处理属性的删除操作。obj
是尝试删除属性的对象实例。可以在删除属性前执行清理操作,或阻止删除操作。
在描述符中,__init__
方法并不是必须的。是否在描述符中使用 __init__
方法主要还是取决于描述符是否需要存储或初始化数据,以及是否需要接受外部配置参数。在很多简单应用或固定行为的描述符中,完全可以省略 __init__
方法以简化代码,如下例,
1class ToStringDescriptor:
2 def __get__(self, obj, objtype=None):
3 return getattr(obj, '_hidden', '')
4
5 def __set__(self, obj, value):
6 setattr(obj, '_hidden', str(value))
该描述符用于将所有赋值转换为字符串类型。在这个示例中,描述符 ToStringDescriptor
自身不需要初始化任何内部状态,因为它仅仅是将值转换为字符串并存储在实例的隐藏属性 _hidden
中。因此,这里没有必要实现 __init__
方法。
不过,在需要灵活配置或有状态的描述符中,__init__
方法是初始化内部状态的理想选择。在以下两种情况下,建议构建 __init__
方法:
- 初始化数据:如果描述符需要内部存储信息,如计数器、缓存或其他状态信息,
__init__
方法就非常有用。它可以初始化这些内部数据结构。 - 配置属性:在某些情况下,描述符可能需要从使用它的类接收配置信息或初始值。通过在描述符的构造函数中设置这些值,可以让描述符的行为更加灵活和动态。
比如说,在描述符 NonNegative
中,我们构建了 __init__
方法。该方法被用于初始化一个默认值 default
。这个默认值在 __get__
方法中用作一个后备值,如果实例中还没有设置属性,则返回这个默认值。假设我们不创建 __init__
方法。则 __get__
方法假定实例始终已经有一个有效的值存储。这意味着在使用描述符之前,实例需要以某种方式初始化这些属性,否则在第一次访问属性时可能引发错误。
回到 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
描述符来管理 health
和 experience
属性:
1class Character:
2 health = NonNegative()
3 experience = NonNegative()
4
5 def __init__(self, name, health, experience):
6 self.name = name
7 self.health = health
8 self.experience = experience
现在,任何尝试将 health
或 experience
设置为负数的操作都会触发异常:
1hero = Character("Arthur", 100, 50)
2
3# 尝试设置负的健康值
4try:
5 hero.health = -10
6except ValueError as e:
7 print(e) # 输出: This value must be non-negative
8
9# 正常设置经验值
10hero.experience = 30
11print(hero.experience) # 输出: 30
12
13# 尝试删除 health 属性
14try:
15 del hero.health
16except AttributeError as e:
17 print(e) # 如果有特定的删除逻辑,可以在描述符中处理
可以看出,使用描述符 NonNegative
,确保了 Character
类的 health
和 experience
属性不会被设置为负数。这在功能上与使用 @property
装饰器类似,但描述符提供了一些独特的优势,特别是在代码的复用性和一致性方面。
虽然 @property
装饰器在简单性和直观性方面上有优势,且易于理解和实施,但是描述符在处理更复杂的属性行为时优势明显。描述符允许开发者将属性的控制逻辑封装在一个独立的类中,这不仅增强了代码的模块化,还提高了维护效率。最重要的是,描述符可以在多个类之间共享,避免了重复编写相同的属性逻辑,从而保持了代码的干净和一致。例如,如果有多个类都需要确保某些数值属性不能为负,使用 NonNegative
描述符可以一次性解决这一需求,而无需在每个类中分别使用 @property
定义相同的逻辑。这不仅节省了开发时间,也使得未来的逻辑更改更加集中和高效。
__getattr__
, __setattr__
, __delattr__
关于属性访问,我们最后再介绍一种方式,使用 __getattr__
, __setattr__
, __delattr__
特殊方法来实现属性的管理。这些方法允许开发者拦截对未知属性的访问或控制所有属性的赋值和删除行为,从而实现更复杂的属性管理策略。
在 Python 中,__getattr__
被用于定义对象属性访问失败时的行为。当咱们尝试访问对象的一个属性时,如果该属性在对象的常规属性字典中不存在,Python 将自动调用这个对象的 __getattr__
方法。
在 Python 中,每个类的实例都有一个名为 __dict__
的属性,它是一个字典对象,用于存储实例的属性及其值。这个字典被称为 “常规属性字典”,它持有对象在运行时动态设置的所有属性。这些属性可以是任何添加到对象的数据成员,不仅包括在类定义中直接声明的属性,也包括在对象生命周期中动态添加的属性。下例展示了如何使用 __dict__
:
1class Person:
2 def __init__(self, name, age):
3 self.name = name
4 self.age = age
5
6person = Person("John", 30)
7
8# 访问 __dict__
9print(person.__dict__) # 输出:{'name': 'John', 'age': 30}
10
11# 动态添加新属性
12person.height = 175
13print(person.__dict__) # 输出:{'name': 'John', 'age': 30, 'height': 175}
14
15# 删除属性
16del person.age
17print(person.__dict__) # 输出:{'name': 'John', 'height': 175}
当咱们尝试获取一个属性时,Python 首先查看对象的 __dict__
来找到这个属性。如果找不到,Python 将检查类的 __dict__
,然后是继承链上的类的 __dict__
。如果所有这些常规查找都失败了,才会调用特殊方法 __getattr__
(如果定义了的话),这就是为什么说“如果该属性在对象的常规属性字典中不存在,Python 才将自动调用这个对象的 __getattr__
方法”。
假设我们希望在 Character
类中,当某个属性不存在时,提供一种处理方式而不是抛出 AttributeError
。这里,我们就可以添加一个 __getattr__
方法来实现这个功能:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name
4 self.health = health
5 self.experience = experience
6
7 def __getattr__(self, attr):
8 # 如果尝试访问的属性不存在,返回一个默认消息
9 return f"{attr} not available"
10
11hero = Character("Arthur", 100, 50)
12
13# 正常访问存在的属性
14print(hero.name) # 输出:Arthur
15
16# 尝试访问不存在的属性
17print(hero.strength) # 自动调用__getattr__(), 输出: strength not available
如果,不定义 __getattr__
, 则在 print(hero.strength)
会输出如下异常:
1Traceback (most recent call last):
2 File "c:\Users\thinkstation\Desktop\testing\property.py", line 48, in <module>
3 hero.strength
4AttributeError: 'Character' object has no attribute 'strength'
此外,如果某个属性不存在,__getattr__
还可以用来提供一个默认值。比如说:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name
4 self.health = health
5 self.experience = experience
6
7 def __getattr__(self, attr):
8 # 提供默认值的字典
9 default_values = {
10 'strength': 10,
11 'intelligence': 10,
12 'agility': 10
13 }
14 # 如果属性在默认值字典中,返回默认值
15 if attr in default_values:
16 return default_values[attr]
17 # 如果不在默认值字典中,返回通用不可用消息
18 return f"{attr} not available"
这里,__getattr__
方法首先检查一个名为 default_values
的字典,该字典定义了一些属性及其对应的默认值。当尝试访问的属性名在这个字典中时,方法返回相应的默认值。如果属性名不在这个字典中,则返回一个表示属性不可用的消息。
1hero = Character("Arthur", 100, 50)
2
3# 正常访问存在的属性
4print(hero.name) # 输出: Arthur
5
6# 尝试访问不存在的属性,但提供默认值
7print(hero.strength) # 输出: 10
8print(hero.intelligence) # 输出: 10
9
10# 尝试访问不存在且无默认值的属性
11print(hero.charisma) # 输出: charisma not available
__getattr__
还常用于实现延迟加载(lazy loading),即属性的值在首次访问时才计算或加载。这对处理开销大的计算或从外部数据源加载数据尤其有用。假设我们有一个 profile
属性。该属性将从一个外部数据源(例如数据库或网络服务)加载角色的详细配置文件。我们可以通过如下方式实现 profile
属性的延迟加载:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name
4 self.health = health
5 self.experience = experience
6 self._profile_loaded = False
7 self._profile = None
8
9 def load_profile(self):
10 # 这里模拟一个耗时的数据加载过程
11 print("Loading profile from a slow database...")
12 return {
13 "biography": "Arthur was a legendary British leader.",
14 "achievements": ["Established the Knights of the Round Table", "United Britain"]
15 }
16
17 def __getattr__(self, attr):
18 if attr == "profile":
19 if not self._profile_loaded:
20 self._profile = self.load_profile()
21 self._profile_loaded = True
22 return self._profile
23 return f"{attr} not available"
24
25hero = Character("Arthur", 100, 50)
26
27# 访问 profile 属性,将触发延迟加载
28print(hero.profile) # 输出加载过程和数据
29print(hero.profile) # 直接输出数据,无需再次加载
这里,我们初始化时程序并不加载 profile
数据,而是在该属性首次被访问时(如第一次print(hero.profile)
)调用 load_profile
方法来加载数据。这减少了对象创建时的开销,特别是对于加载数据特别耗时的属性。这个逻辑的具体实现是由 __getattr__()
特殊方法控制。__getattr__()
在属性 profile
被访问且尚未加载时,调用 load_profile
方法并缓存结果。一旦数据被加载,后续访问该属性不会再触发加载过程, 如第二次 print(hero.profile)
。
__setattr__
方法允许我们定义当属性被赋值时应执行的自定义行为,如数据验证、自动转换、维护属性间的依赖关系,或其他任何需要在属性值改变时执行的操作。当咱们尝试给属性赋值时,例如 obj.attribute = value
,Python 会自动调用定义好的 __setattr__
方法。
__setattr__
方法通常定义如下:
1def __setattr__(self, name, value):
2 # 自定义行为
其中 name
是要设置的属性名称,value
是要设置的值。
我们可以通过实现 __setattr__
方法来达到确保 health
和 experience
属性不会被设置为负值的目的:
1class Character:
2 def __init__(self, name, health, experience):
3 self.name = name
4 self.health = health
5 self.experience = experience
6
7 def __setattr__(self, key, value):
8 # 对 health 和 experience 属性进行非负值检查
9 if key in ["health", "experience"] and value < 0:
10 raise ValueError(f"{key} cannot be negative.")
11
12 # 调用基类的 __setattr__ 来实际设置属性
13 super().__setattr__(key, value)
14
15 # 记录属性设置
16 print(f"Set {key} to {value}")
17
18# 创建一个 Character 对象
19hero = Character("Arthur", 100, 50)
20
21# 修改 health 属性
22hero.health = 80
23
24# 尝试将 experience 设置为负值,将触发 ValueError
25try:
26 hero.experience = -10
27except ValueError as e:
28 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 中,类可以被视为一种容器,它们用于封装数据和与数据相关的方法。同时,类也是实现复杂数据结构的主要工具。如链表、树、图、堆、哈希表等。通过定义节点、操作如插入、删除等,类能够提供数据结构所需的所有功能。下面借助于类,实现了一个简单的链表:
1class Node:
2 def __init__(self, data):
3 self.data = data
4 self.next = None
5
6class LinkedList:
7 def __init__(self):
8 self.head = None
9
10 def append(self, data):
11 new_node = Node(data)
12 if self.head is None:
13 self.head = new_node
14 else:
15 last = self.head
16 while last.next:
17 last = last.next
18 last.next = new_node
19
20 def display(self):
21 current = self.head
22 while current:
23 print(current.data, end=' ')
24 current = current.next
25 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
中的实现:
1def __len__(self):
2 count = 0
3 current = self.head
4 while current:
5 count += 1
6 current = current.next
7 return count
这种方式确保了无论链表如何变化,都可以准确地获取其长度,是处理链表类型数据时常见的操作之一。这样,当想知道 LinkedList
对象中的数据个数时,我们就可以调用 len(my_list)
打印出链表的长度。在 Python 在内部,这个过程实际上调用了 my_list.__len__()
方法。如该方法所示,这个方法将遍历整个链表,计数节点的数量,并返回这个值。
1# 创建一个 LinkedList 实例
2heroes_list = LinkedList()
3
4# 向链表中添加草丛三姐妹
5heroes_list.append("妲己")
6heroes_list.append("安琪拉")
7heroes_list.append("王昭君")
8
9# 使用 __len__ 方法获取链表的长度
10length = len(heroes_list) # 这里实际上调用的是 heroes_list.__len__()
11
12# 打印链表的长度
13print("Number of heroes in the linked list:", length) # 输出:Number of heroes in the linked list: 3
为了提升 LinkedList
类的效率,我们可以在类内部直接跟踪链表的长度,从而避免每次调用 __len__
方法时需要遍历整个链表。下面是修改后的 LinkedList
类,其中添加了一个 length
属性来跟踪链表的长度,并相应地更新了 append
和 __len__
方法:
1class Node:
2 def __init__(self, data):
3 self.data = data
4 self.next = None
5
6class LinkedList:
7 def __init__(self):
8 self.head = None
9 self.length = 0 # 初始化链表长度为0
10
11 def append(self, data):
12 new_node = Node(data)
13 if self.head is None:
14 self.head = new_node
15 else:
16 last = self.head
17 while last.next:
18 last = last.next
19 last.next = new_node
20 self.length += 1 # 每添加一个节点,长度增加1
21
22 def __len__(self):
23 return self.length # 直接返回链表的长度
这样,我们就不用每次计算链表长度时,均需要遍历链表中的所有元素,只需要访问访问对象的 length
属性。这使得获取长度的时间复杂度降低到了 O(1),即常数时间内完成。
在 LinkedList
类中,__add__()
方法的实现是为了使 LinkedList
类的对象支持使用加号 (+
) 运算符来合并两个链表,并返回一个新链表。
1def __add__(self, other):
2 if not isinstance(other, LinkedList):
3 return NotImplemented
4 new_list = LinkedList()
5 current = self.head
6 while current:
7 new_list.append(current.data)
8 current = current.next
9 current = other.head
10 while current:
11 new_list.append(current.data)
12 current = current.next
13 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
,这个链表现在就包含了两个原始链表的所有节点,而原始链表的节点和结构保持不变。
我们可以在前面的示例中添加第二个链表,并运用 +
的方式合并这两个链表:
1# 创建第二个 LinkedList 实例,并添加其他英雄
2more_heroes_list = LinkedList()
3more_heroes_list.append("露娜")
4more_heroes_list.append("娜可露露")
5more_heroes_list.append("貂蝉")
6
7# 显示第一个链表的英雄名字和长度
8print("First LinkedList:")
9heroes_list.display()
10print("Number of heroes in the first linked list:", len(heroes_list))
11
12# 显示第二个链表的英雄名字和长度
13print("\nSecond LinkedList:")
14more_heroes_list.display()
15print("Number of heroes in the second linked list:", len(more_heroes_list))
16
17# 使用 __add__ 方法合并两个链表
18combined_list = heroes_list + more_heroes_list
19
20# 显示合并后的链表的英雄名字和长度
21print("\nCombined LinkedList:")
22combined_list.display()
23print("Number of heroes in the combined linked list:", len(combined_list))
运行后,会输出如下结果:
1First LinkedList:
2妲己 安琪拉 王昭君
3Number of heroes in the first linked list: 3
4
5Second LinkedList:
6露娜 娜可露露 貂蝉
7Number of heroes in the second linked list: 3
8
9Combined LinkedList:
10妲己 安琪拉 王昭君 露娜 娜可露露 貂蝉
11Number of heroes in the combined linked list: 6
类似的,我们可以在 LinkedList
中实现 __mul__()
方法,使链表可以通过乘法操作符(*
)与一个整数相乘,从而创建一个新的链表,其中原链表的内容重复指定的次数。
1def __mul__(self, n):
2 if not isinstance(n, int):
3 return NotImplemented
4 if n <= 0:
5 return LinkedList() # 返回一个空的链表
6 new_list = LinkedList()
7 for _ in range(n):
8 current = self.head
9 while current:
10 new_list.append(current.data)
11 current = current.next
12 return new_list
该方法的实现逻辑如下:
- 首先,检查乘数 $n$ 是否为整数。如果不是,返回
NotImplemented
。 - 其次,如果 $n$ 小于或等于 0,返回一个新的空链表,因为任何东西乘以 0 或负数在逻辑上应该为空或不存在。
- 创建一个新的
LinkedList
实例new_list
。 - 使用两层循环来实现重复:外层循环控制重复次数,内层循环遍历当前链表的所有元素,并将它们添加到新链表中。
- 最后,返回新的重复后的链表。
根据如上定义,我们可以使用乘法操作符 *
来重复链表,比如:
1# 使用 __mul__ 方法重复链表内容
2# 假设我们想将整个链表内容重复3次
3repeated_list = heroes_list * 3
4
5# 显示重复后的链表
6print("Repeated LinkedList (3 times):")
7repeated_list.display()
这将输出下入下信息:
1Repeated LinkedList (3 times):
2妲己 安琪拉 王昭君 妲己 安琪拉 王昭君 妲己 安琪拉 王昭君
其实,按照如上定义,整数的在表达式的位置比较重要,只能在旧的链表的右边(如 heroes_list * 3
)。当我们将整数放在左边时(3 * heroes_list
),会出现如下错误:
1Traceback (most recent call last):
2 File "d:\git\web\hakuna\content\posts\2024-04-16-oop-data-structure-algorithms\code\linkedlist.py.py", line 117, in <module>
3 main()
4 File "d:\git\web\hakuna\content\posts\2024-04-16-oop-data-structure-algorithms\code\linkedlist.py.py", line 112, in main
5 tripled_list_reversed = 3 * heroes_list
6 ~~^~~~~~~~~~~~~
7TypeError: unsupported operand type(s) for *: 'int' and 'LinkedList'
要使 LinkedList
类的实例支持将整数放在乘法表达式的左侧(例如 3 * heroes_list
),我们需要实现 Python 的反向特殊方法 __rmul__()
。这个方法在正常的方法(如 __mul__
)无法找到合适的实现或者操作数的顺序反向时被调用。示例如下:
1def __rmul__(self, n):
2 return self.__mul__(n) # 可以直接调用 __mul__ 来实现相同的功能
当乘法操作的顺序颠倒时,如 3 * heroes_list
,该方法将被自动调用。在这个实现中,__rmul__
方法简单地调用 __mul__
方法,因为乘法是交换的,而我们的 __mul__
方法已经足够通用,可以处理乘法的逻辑。
定义 __iadd__
(In-place 加法)方法通常意味着要在原有对象的基础上修改而非创建一个新的对象。这与 __add__
方法的典型实现有所不同,后者常常返回一个全新的对象。
1def __iadd__(self, other):
2 if not isinstance(other, LinkedList):
3 return NotImplemented
4 current = self.head
5 if not current:
6 self.head = other.head
7 else:
8 last = current
9 while last.next:
10 last = last.next
11 last.next = other.head
12 return self
与前面的方法类似,该方法首先检查 other
是否也是一个 LinkedList
实例。如果 other
不是 LinkedList
类型,方法返回 NotImplemented
。
然后,将当前链表的头节点赋值给变量 current
,并判断当前链表是否为空。如果当前链表为空,则直接将 self.head
指向 other
链表的头节点,将 other
链表直接接入到当前链表的头部。如果当前链表不为空。则初始化 last
变量以遍历链表找到最后一个节点。将找到的最后一个节点的 next
指向 other
链表的头节点,从而将 other
链表接在当前链表的末尾。
最后,方法返回 self
,即经过修改后的原链表对象。
__imul__()
方法的实现用于链表的就地乘法操作,允许通过 *=
运算符将一个 LinkedList
实例的内容重复指定次数并更新原始链表。这也意味着,这种操作直接修改调用它的对象(即 self
),而不是创建一个新的链表对象。
1def __imul__(self, n):
2 if not isinstance(n, int):
3 return NotImplemented
4 original_data = list(self) # # Convert the linked list to a list, which relies on the __iter__ method for iteration
5
6 if n <= 0:
7 self.head = None
8 else:
9 self.head = None
10 for _ in range(n):
11 for item in original_data:
12 self.append(item)
13 return self
该方法的实现逻辑如下:
- 首先,检查乘数 $n$ 是否为整数。如果不是,返回
NotImplemented
。 - 其次,将链表转换为列表,以便存储原始链表中的所有元素。这一转换依赖于
__iter__()
方法的实现,该方法允许通过迭代器来遍历链表的每个节点,并获取它们的数据。我们将在下面介绍。 - 在此基础上,检查整数 $n$ 是否小于或者等于0。如果 $n$ 小于或等于0,意味着链表应被清空(因为重复0次或负数次没有逻辑意义)。此时将
self.head
设置为None
,这样做将清除链表中的所有元素。 如果 $n$ 是正整数,那么首先清空原链表。这是重建链表之前的准备步骤,确保链表从空状态开始。然后使用两层循环,外层循环控制重复次数,即将整个链表内容重复 $n$ 次,内层循环遍历之前保存的原始数据列表。每个元素通过self.append(item)
方法被重新添加到链表中。这样,原始链表的每个元素都会按原顺序被添加 $n$ 次。 - 最后,方法返回经过就地修改后的链表实例(
self
)。
以下示例展示了该方法的基本使用方式:
1# 使用 __imul__ 方法就地重复链表内容
2heroes_list *= 3
3print("\nHeroes List after using __imul__ (tripled in-place):")
4heroes_list.display()
5print("\nNumber of heroes after __imul__:", len(heroes_list))
运行后将显示:
1Heroes List after using __imul__ (tripled in-place):
2妲己 安琪拉 王昭君 妲己 安琪拉 王昭君 妲己 安琪拉 王昭君
3
4Number of heroes after __imul__: 9
在实现 __imul__()
方法时,我们指出,该算法依赖于迭代器来将链表转换为列表,以便存储原始链表中的所有元素。那么什么是迭代器,甚至什么是迭代?
在编程中,迭代(Iteration,又大致可以称为遍历)指的是按照一定的顺序反复访问数据集中的每个元素的过程。迭代可以应用于各种数据结构,如列表、数组、树、图等。在 Python 中,迭代通常是通过使用循环结构(如 for
循环或 while
循环)来实现的,让程序可以执行重复的处理,直到满足某个条件为止。
比如说,我们可以借助于 for
循环输出一个列表中的所有元素:
1for number in [1, 2, 3, 4, 5]:
2 print(number)
这一过程就可以称为迭代。由此,也衍生出来了迭代器(Iterator)以及可迭代对象(Iterable)等概念。迭代器是实现了迭代器协议(Iterator Protocol)的对象,该协议主要包括 __iter__()
和 __next__()
两个特殊方法。对于迭代器来说,这两个方法均为必须实现。__iter__()
返回迭代器自身,而 __next__()
返回集合中的下一个元素。当元素耗尽时,__next__()
必须抛出一个 StopIteration
异常,标志迭代的结束。在 Python 中,任何实现了 __iter__()
方法的对象或内置的序列对象(如列表和元组)都是可迭代的。当这些对象用于 for
和 in
语句时,Python 会自动调用它们的 __iter__()
方法以获取迭代器。
值得注意的是,所有的迭代器都是可迭代对象,但并不是所有的可迭代对象均是迭代器。例如,列表是一个可迭代对象,因为它实现了 __iter__()
方法,但列表本身不包含 __next__()
方法,因此不是迭代器。从概念定义上讲,迭代主要是提供遍历数据的框架,而迭代器则提供在这个框架内操作的具体方法和机制。
为了在 LinkedList
中实现迭代器的功能,我们可以先定义一个单独的 LinkedListIterator
类。
1class LinkedListIterator:
2 def __init__(self, head):
3 self.current = head
4
5 def __iter__(self):
6 return self
7
8 def __next__(self):
9 if not self.current:
10 raise StopIteration
11 data = self.current.data
12 self.current = self.current.next
13 return data
在这个新类中,__init__()
构造函数接收链表的头节点并初始化迭代器的当前节点 (self.current
)。__iter__()
方法则返回迭代器对象本身,符合迭代器协议。__next__()
方法检查当前节点。如果当前节点为 None
,则抛出 StopIteration
异常,标志迭代结束;否则,获取当前节点的数据,并将迭代器移向下一个节点,最后返回当前节点的数据。
在 Python 中,其实还可以使用生成器来实现这一功能。生成器自动处理 __iter__()
和 __next__()
方法的实现:
1def __iter__(self):
2 current = self.head
3 while current:
4 yield current.data
5 current = current.next
在 LinkedList
类中,方法 __iter__()
起着生成器的作用,它将遍历链表中的每个元素。其中,current
初始化为链表的头节点 (self.head
)。只要 current
不为 None
,就继续迭代。yield current.data
表达式用于一次返回链表中的一个元素,并在下次迭代时从停下的地方继续。current = current.next
将迭代器移动到链表的下一个节点。
由此,我们可以在 LinkedList
类中构建一个 __iter__()
方法,该方法创建并返回 LinkedListIterator
对象:
1def __iter__(self):
2 return LinkedListIterator(self.head)
这样我们就可以使用 for
循环迭代 LinkedList
:
1# 使用 for 循环迭代 LinkedList
2for data in more_heroes_list:
3 print(data, end=" ") # 输出:露娜 娜可露露 貂蝉
__contains__()
方法的实现可以为 LinkedList
类提供了能力来检查某个元素是否存在于链表中(使用 in
关键字)。对于简单的数据结构,__contains__()
方法可以通过遍历来实现,对每个节点的数据进行比较。
1def __contains__(self, item):
2 current = self.head
3 while current:
4 if current.data == item:
5 return True
6 current = current.next
7 return False
1# 使用 __contains__ 方法实现检查某个元素是否在LinkedList对象中
2print("安琪拉" in heroes_list) # 输出:True
3print("安琪拉" in more_heroes_list) # 输出:False
在 LinkedList
类中实现 __getitem__()
,__setitem__()
,__delitem__()
特殊方法可以使其支持支持通过索引进行获取、设置和删除元素的操作,类似于标准的序列类型如列表。以下是这三种方法实现示例:
1# 实现 __getitem__
2def __getitem__(self, index):
3 if index < 0:
4 raise IndexError("Negative indices are not supported")
5 current = self.head
6 for _ in range(index):
7 if current is None:
8 raise IndexError("Index out of bounds")
9 current = current.next
10 if current is None:
11 raise IndexError("Index out of bounds")
12 return current.data
这里,我们首先检查传入的索引是否为负数。考虑到 LinkedList
的特征,该链表并不支持负索引(与Python列表等不同),因此如果索引是负数,则抛出 IndexError
表示索引无效。然后,我们从链表的头节点开始,沿着链表向后遍历。使用一个循环来跳过直到达到指定的索引位置的元素。每次循环中,更新当前节点指向下一个节点。在遍历过程中,如果当前节点变为 None
,说明已经超出了链表的尾部,这时也应抛出 IndexError
表示索引超出了链表的有效范围。一旦到达正确的位置,返回当前节点的数据。这里通过访问节点的 data
属性来实现的。
1# 实现 __setitem__
2def __setitem__(self, index, value):
3 if index < 0:
4 raise IndexError("Negative indices are not supported")
5 current = self.head
6 for _ in range(index):
7 if current is None:
8 raise IndexError("Index out of bounds")
9 current = current.next
10 if current is None:
11 raise IndexError("Index out of bounds")
12 current.data = value
这里,我们仍然先检查传入的索引是否为负数。由于该链表没有记录反向索引,因此,负数索引在这里是不被接受的,如果发现索引为负,则抛出 IndexError
。接着,从链表的头节点开始,顺序遍历链表节点直到达到指定的索引位置。这一过程中,通过循环逐个访问链表的节点,每次循环将当前节点向前移动到下一个节点。如果在达到所需索引前当前节点已经为 None
(即已经到达链表尾部而没有足够的元素),则表明索引超出了链表当前的长度范围,这时也应抛出 IndexError
。一旦到达指定索引的节点,将该节点的数据设置为新的值。这通过直接赋值给节点的 data
属性来实现。注意该方法的实现与链表插入功能的区别。
1# 实现 __delitem__
2def __delitem__(self, index):
3 if index < 0:
4 raise IndexError("Negative indices are not supported")
5 if self.head is None:
6 raise IndexError("Index out of bounds")
7 if index == 0:
8 self.head = self.head.next
9 return
10 current = self.head
11 previous = None
12 for _ in range(index):
13 previous = current
14 if current is None or current.next is None:
15 raise IndexError("Index out of bounds")
16 current = current.next
17 if current is None:
18 raise IndexError("Index out of bounds")
19 previous.next = current.next
__delitem__
方法仍然首先检查提供的索引是否为负数,如果索引为负,则抛出 IndexError
,表明索引无效。同时检查需要删除的是否为链表的第一个元素(即索引为0),此时,可以直接将头节点 (self.head
) 指向下一个节点 (self.head.next
),从而将当前头节点从链表中移除。如果不是删除第一个节点,则从头节点开始,遍历链表以找到指定索引的节点。同时保持对当前节点的前一个节点的引用(previous
),这是为了在删除当前节点时能够重新连接链表。在遍历过程中,如果到达链表尾部(当前节点为 None
)而还没有到达指定的索引,表示索引超出了链表的当前长度,这时应抛出 IndexError
。一旦找到指定索引的节点,通过设置前一个节点的 next
指针绕过当前节点(即 previous.next = current.next
),从而实现删除操作。由于删除操作不需要返回特定的值,方法完成后返回 None
。
使用示例:
1# 使用 __getitem__ 访问元素
2print(more_heroes_list[0])
3
4# 使用 __setitem__ 修改元素
5more_heroes_list[2] = '孙悟空'
6print("\nHeros List after using __setitem__: ")
7more_heroes_list.display()
8
9# 使用 __delitem__ 删除元素
10del more_heroes_list[2]
11print("\nHeros List after using __delitem__: ")
12more_heroes_list.display()
1露娜
2
3Heros List after using __setitem__:
4露娜 娜可露露 孙悟空
5
6Heros List after using __delitem__:
7露娜 娜可露露
至于 LinkedList
链表的比较操作,请参考 linkedlist.py。
该图展示了 Python 中的基本集合类型(Collections)接口,包括 Iterable(可迭代)、Sized(有大小)、Container(容器)、Reversible(可反转)、Sequence(序列)、Mapping(映射)和 Set(集合)。这些接口定义了集合类型的行为。
图中的方法名用斜体表示时,它们代表的是抽象方法。抽象方法指的是那些必须由具体的子类(例如 Python 中的 list
或 dict
)来实现的功能。例如,__getitem__
和 __contains__
是抽象方法,具体的 list
或 dict
类必须提供这些方法的具体实现。
图中未使用斜体表示的方法已经有了默认实现。子类继承这些接口时,可以直接使用这些已实现的方法,而不需要重新实现它们。换句话说,子类只需要实现那些抽象方法,其他已有实现的方法会自动继承并可以直接使用。
比如:当你定义一个新的类继承自 Sequence
或 Mapping
接口时,必须实现图中斜体表示的方法,例如 __getitem__
或 __contains__
。而那些没有使用斜体表示的方法(例如 count()
或 items()
),则已经有了具体实现,你可以直接使用它们。
Python 中类的使用
在面向对象编程中,我们有几个比较重要的工作:开发者首先根据需求定义相关的类,这些类充当现实世界实体或概念的蓝图或模板,包括相关的一组属性(变量)和方法(函数)。其次,使用者通过实例化(调用类的构造函数,即 __init__
方法),从这些类创建对象。此时,每个对象都会继承已定义类的结构和行为,从而,使用者可以通过公共接口(即类所提供的公开方法)来完成相关的工作。
从开发者视角来看,在面向对象编程中,一般应该遵循四大基本原则:封装、继承、多态和抽象。封装使得对象可以将其状态(属性)和实现细节隐藏在内部,只通过公共接口与外界交互。继承允许类间共享和扩展功能,使得代码更加模块化并减少重复。多态性提供了接口的多样化实现,使得同一操作可适用于不同的对象,具体行为取决于对象的实际类型。抽象则帮助开发者集中于高层设计,忽略底层的具体细节,通过抽象类或接口形成一套共有的操作框架。在整个开发过程中,单元测试和调试确保了代码的可靠性和功能的正确实现,而设计模式则提供了解决常见问题的有效方法。
前面我们已经介绍了类定义的基本情况,后面将重点聚焦于类的使用,包括创建对象、继承和多态。
创建对象
在面向对象编程中,实例化是将类模板转换为具体对象的过程,这是面向对象程序运行的基础。通过实例化,我们可以从抽象的类定义创建具体的对象实例,使得相同的类模板可以被用来创建多个独立的对象,每个对象都有其独立的属性和状态,但共享相同的方法。这些实例在内存中占有具体的空间,并拥有类定义的结构和行为。
下图展示了同一个类模板(Character
)可以被用来创建多个独立的对象(Object1
,Object2
,Object3
),且每个对象都有其独立的属性和状态,但共享相同的方法。
实例化的核心在于创建一个类的具体实例,这个过程涉及到类构造函数的调用。如前所述,在 Python 中,构造函数通过 __init__
方法实现。当咱们创建一个类的新实例时,Python 会自动调用这个类的 __init__
方法。实例化一个类的具体机制可以参考特殊方法的定义中关于自动调用机制的解释。
创建一个类的实例非常简单,只需要调用类名并传入构造函数所需要的参数,比如使用表达式 new_object = MyClass()
。下面展示了一个具体的示例:
1# 创建 Character 类的一个实例
2my_character = Character("Arthur", 100, 0)
在这个例子中,我们创建了一个名为 my_character
的新对象,并传递了三个参数给 __init__
方法:"Arthur"
作为角色的名字,100
作为初始健康值,0
作为初始经验值。
一旦实例化完成,就可以使用这个对象的属性和方法了:
1# 访问对象属性
2print(my_character.name) # 输出: Arthur
3print(my_character.health) # 输出: 100
4print(my_character.experience) # 输出: 0
5
6# 调用 gain_experience 方法来更新经验值
7my_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)
的方式查看该基类的所有方法:
1>>> dir(object)
2['__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__
以提供更具可读性或调试友好性的字符串表示。我们来看一个简单的例子:
1class MyClass:
2 pass
3
4# MyClass 自动继承自 object
5print(isinstance(MyClass(), object)) # 输出: True
6print(isinstance(MyClass(), MyClass)) # 输出: True
7print(issubclass(MyClass, object)) # 输出:True
这里,我们定义了一个什么也不干(没有自定义新属性及其相应的方法)的 MyClass
类。从上面的输出结果可以看出:
MyClass()
实例是object
类的一个实例,这表明所有在 Python 中定义的类,无论是否显式继承其他类,都默认继承自object
类。MyClass()
实例也是它自身类MyClass
的实例,这是面向对象编程中的一个基本概念:对象总是属于它的直接类或其父类。issubclass(MyClass, object)
返回True
,确认了MyClass
是object
的子类。这强调了所有自定义类都继承自 Python 的object
基类。
以下示例展示了 MyClass
实例继承自 object
类的一些默认方法,并演示了它们的行为:
1obj1 = MyClass()
2print(str(obj1)) # 输出类似于: <__main__.MyClass object at 0x000001641905A240>
3print(repr(obj1)) # 输出类似于: <__main__.MyClass object at 0x000001641905A240>
4
5obj2 = MyClass()
6print(obj1 == obj2) # 默认情况下,这将返回 False,因为它们是不同的实例(内存地址不同)。
7print(hash(obj1), hash(obj2)) # 返回不同的哈希值,通常是基于它们的 id。输出类似于: 133108140574 133108140565
这些示例表明,即使是简单的类定义也继承了许多基本行为,它们为类的实例提供了标准的接口。尽管 object
类为我们提供了这些基础方法的默认实现,但往往我们需要根据具体需求重写这些方法,以实现更复杂的功能和行为。这不仅体现了继承机制的力量,也强调了面向对象设计中定制行为以满足特定需求的重要性。
单继承
在 Python 中,类的继承允许我们定义一个类来继承另一个已存在的类的属性和方法。继承语法很简单,主要包括在类定义时指定要继承的父类。这是 Python 中实现继承的基本语法结构:
1class BaseClass:
2 # 父类中的方法和属性
3 pass
4
5class DerivedClass(BaseClass):
6 # 派生类中的方法和属性
7 pass
这里的 BaseClass
是父类(也称作基类),而 DerivedClass
是从 BaseClass
派生的子类。子类继承了父类的所有方法和属性,可以使用这些方法和属性就像它们是在子类中定义的一样。由于 DerivedClass(BaseClass)
中,只有 BaseClass
一个父类,因此,这样的继承方式也被称为单继承(Single inheritance),即一个子类只继承自一个父类。
假设我们想要在 Character
类的基础上创建几个具体的角色类,如 Knight
, Mage
和 Archer
。我们可以通过如下方式实现:
1class Character:
2 def __init__(self, name, health):
3 self.name = name
4 self.health = health
5
6 def display_info(self):
7 print(f"{self.name}, Health: {self.health}")
8
9class Knight(Character):
10 def __init__(self, name, health, armor):
11 super().__init__(name, health)
12 self.armor = armor
13
14 def display_info(self):
15 super().display_info()
16 print(f"Armor: {self.armor}")
17
18class Mage(Character):
19 def __init__(self, name, health, mana):
20 super().__init__(name, health)
21 self.mana = mana
22
23 def display_info(self):
24 super().display_info()
25 print(f"Mana: {self.mana}")
在这个例子中,Knight
和 Mage
是从 Character
类派生的。它们通过调用 super().__init__(name, health)
继承了 Character
类的构造方法,这样可以保证基类的初始化逻辑被正确执行。此外,派生类还扩展了自己特有的属性如 armor
和 mana
,并且重写了 display_info()
方法来展示额外的信息。
使用 super()
函数在继承中是一个非常重要的实践。当一个类继承自另一个类时,子类通常需要初始化它从父类继承的部分。super()
函数用于调用父类(基类)的方法,确保了基类的初始化代码可以执行。比如该示例中的 Knight
类。当创建一个 Knight
类的实例时,首先会调用它的构造器 __init__(self, name, health, armor)
。此构造器接收三个参数:name
,health
,和 armor
。由于在 Knight
类的构造器中,super().__init__(name, health)
的存在,super()
函数会返回对父类 Character
的临时对象引用,然后调用其 __init__()
方法。父类 Character
的构造器接收两个参数:name
和 health
。这两个属性在父类中被初始化。这意味着每个 Knight
实例都会在其父类部分具有 name
和 health
属性,并且这些属性在父类的构造器中被设置。父类的构造器执行完成后,控制权返回到 Knight
类的构造器。接下来的行 self.armor = armor
初始化 Knight
类特有的属性 armor
。这是在 Knight
实例中添加的新属性,而不影响父类 Character
。Knight
类的构造器完成执行,此时,一个具有 name
,health
,和 armor
属性的 Knight
对象被成功创建。这个初始化过程保证了 Knight
类的对象不仅拥有其独特的属性(如 armor
),而且还继承了 Character
类的所有功能和属性(如 name
和 health
)。
此外,super()
函数的使用还保证了多重继承中能够使用 C3 线性化算法(一个确定方法解析顺序的算法)来确保所有基类都被适当地初始化。super()
调用不仅会查找直接父类,还会按照方法解析顺序(Method Resolution Order, MRO)处理多继承中可能的复杂关系。这里出现了几个比较重要的概念:多重继承、C3 线性化算法、方法解析顺序。这些概念的理解对深入了解 Python 类的继承有着重要的作用,下面我们将逐一探讨这些概念,以确保对 Python 类继承机制有一个全面的理解。
多重继承
多重继承允许一个类同时继承多个父类。在 Python 中,实现多重继承的语法非常直观。咱们只需要在定义类时,在类名后的括号中列出所有要继承的父类,各个父类之间用逗号分隔。下面是一个简单的示例:
1class Base1:
2 def method1(self):
3 print("Method from Base1")
4
5class Base2:
6 def method2(self):
7 print("Method from Base2")
8
9# 多重继承
10class Derived(Base1, Base2):
11 def method_derived(self):
12 print("Method from Derived")
13
14# 创建 Derived 类的实例
15instance = Derived()
16instance.method1() # 调用来自 Base1 的方法
17instance.method2() # 调用来自 Base2 的方法
18instance.method_derived() # 调用来自 Derived 的方法
在这个例子中,Derived
类同时继承了 Base1
和 Base2
。因此,Derived
类的实例可以访问所有父类中定义的方法。我们可以通过如下方式查看 Derived
类的实例访问父类的顺序:
1print(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 Order、C3 linearization、Python’s super() considered super!等资料。