目录
一、面向对象概述
1. 面向过程与面向对象
2. 类与对象概念
3. 实例与实例化
二、定义类与创建对象
1. 基础语法
2. __init__构造方法
3. self 关键字
三、属性
1. 实例属性
2. 类属性
3. __dict__ 属性
4. 属性查找优先级
四、方法
1. 实例方法
2. 类方法
3. 静态方法
五、继承
1. 继承基本概念
2. 继承语法结构
3. 继承底层机制
4. 属性与方法继承案例
六、方法重写
1. 基本概念
super 函数
2. 方法重写案例
3. 方法重写注意事项
七、继承关系判断
1. isinstance() 函数
2. issubclass() 函数
3. 为什么不能使用 type() 进行类型判断
八、多重继承
1. 多重继承的语法实现
2. 菱形继承与命名冲突
3. MRO 与 C3 线性化算法
4. 多重继承下的 super 行为
5. 多重继承注意事项
总结
一、面向对象概述
当软件系统的业务复杂度达到一定规模时,传统的面向过程流控制会导致代码的维护成本呈指数级上升
为了构建高内聚、低耦合的现代软件系统,我们需要引入一种全新的编程范式——面向对象编程。本篇将系统拆解面向对象的核心概念,并探讨其底层的设计哲学
1. 面向过程与面向对象
理解面向对象编程,首先需要厘清它与面向过程编程在解决问题视角上的差异
面向过程(POP):以算法和执行步骤为核心。其核心思维是 "怎么做"。程序将复杂的业务逻辑拆解为一系列按顺序执行的函数,数据通过参数在各个函数之间跳转
局限性:当系统规模扩大时,由于数据与函数是分离的,对数据结构的任何微小改动都可能引发大面积的函数重构,系统维护极为脆弱
面向对象(OOP):以数据和实体对象为核心。其核心思维是 "谁来做"。程序将现实世界中的事物抽象为独立的 "对象",将数据(属性)与操纵这些数据的行为(方法)捆绑在一起,形成一个完整的结构
优势:通过封装、继承和多态,提供高复用性、可扩展性以及对复杂系统的控制力
2. 类与对象概念
为了准确理解,我们可以将其映射到现实中的关系
建筑图纸与实体房屋
类(Class):相当于一张精密的建筑图纸。图纸定义了房屋的结构(高矮、长宽、房间布局、承重墙位置),但图纸本身只是一个抽象的规范,并不占据实际的空间,也不能直接居住
对象(Object):相当于依据该图纸建造出来的真实房屋。它是实实在在占据物理空间、具备实际使用功能的实体
手机型号设计与手中持有的物理手机
类(Class):相当于某个品牌手机的出厂参数型号和生产规范(例如定义了它应该具备多大容量的电池、什么型号的处理器、以及支持拍摄功能)
对象(Object):相当于你此时此刻手中拿着的、正在使用的这台物理手机。它拥有属于它自己的剩余电量、专属的 IMEI 串号以及内部存储的具体数据
通过这两组映射可以看出:一个类可以作为模板,批量创造出成百上千个功能相同的对象。这些对象共享相同的行为逻辑(方法),但各自拥有独立的内部状态数据(属性)
3. 实例与实例化
在面向对象的语境中,还有两个高频出现的术语需要精准界定:
实例化:指由抽象的类产生具体的对象的过程。在代码层面,就是指调用某种特定机制,在系统内存中为该类分配空间并完成初始化赋值的行为
实例:通过 "实例化" 过程产生的具体对象,就被称为该类的一个实例
在绝大多数语境下,"对象(Object)"与 "实例(Instance)"是完全等价的概念。如果非要寻找细微差别,对象是一个更加宏观的实体代称,而实例则更加侧重强调该实体与其所属的母类之间("...的实例")的物理血缘关系
二、定义类与创建对象
在 Python 中,实现面向对象编程的第一步是声明 "类"(作为模板),进而通过该类在内存中分配空间并构建具体的对象实例
1. 基础语法
类的定义
Python 使用内置关键字 class 来定义一个类。根据 PEP 8 编码规范,类名应当组合多个单词并采用大驼峰命名法,即每个单词的首字母均大写,不使用下划线分隔
class SmartPhone: """定义一个智能手机类""" # 类的内部可以包含属性定义与方法定义 pass对象的创建
创建对象的语法形式类似于调用一个没有参数的函数:对象名 = 类名()
# 实例化两个独立的手机对象 phone_a = SmartPhone() phone_b = SmartPhone() print(type(phone_a)) # 输出:<class '__main__.SmartPhone'> print(id(phone_a)) # 输出 phone_a 在内存中的唯物理一地址 print(id(phone_b)) # 输出一个完全不同的物理地址,证明两者的独立性2. __init__构造方法
在创建对象时,我们通常需要为不同的对象赋予不同的初始状态(例如:赋予不同的手机品牌、颜色或价格)。为此,Python 引入了双下划线包围的特殊方法——__init__ 方法
__init__ 方法被称为构造方法或初始化方法。它的核心职能是:当对象被实例化出来时,Python 解释器会自动、隐式地调用该方法,用于执行对象属性的初始化赋值
class SmartPhone: def __init__(self, brand, price): # 在此处为新诞生顺产生的对象绑定初始属性 self.brand = brand self.price = price # 创建 SmartPhone 类的对象 my_phone = SmartPhone("Apple", 8999)3. self 关键字
为什么定义类的方法时,第一个参数必须是 self
从底层的物理结构来看,一个类被加载后,其内部的方法代码块在内存中只有一份副本。当有成百上千个不同的对象同时调用同一个方法时,Python 解释器必须知道当前正在操作哪一个具体的对象实体
self 关键字本质上是一个内存指针。它代表当前正在被创建或正在调用方法的那个对象实例本身。在 __init__ 方法中,self.brand = brand 的真实含义是:将传入的 brand 数据,写入到当前这个新诞生对象的专属内存空间中
为什么在实例化创建对象时,不需要手动传入 self
这是因为 Python 解释器在底层为我们做了隐式自动传递。
当我们执行代码 my_phone = SmartPhone("Apple", 8999) 时,底层的实际运行流程如下:
Python 在堆内存中开辟一块空间给新对象
Python 自动将这个新对象的内存首地址作为第一个参数,隐式传递给 __init__ 的 self 形参
传入的 "Apple" 和 8999 则依次对应传给 brand 和 price
也就是说,代码层面的 my_phone = SmartPhone("Apple", 8999) 在解释器内部等价于:
SmartPhone.__init__(my_phone, "Apple", 8999)}
三、属性
在面向对象编程中,"属性" 用于定义对象的状态数据。Python 的属性管理机制非常灵活,根据作用域和生命周期的不同,属性主要分为实例属性与类属性。理解这两者的区别是编写高效、无 Bug 面向对象代码的关键
1. 实例属性
实例属性是绑定在具体对象实例上的属性。每个对象实例都拥有一份独立的实例属性空间,互不干扰
定义:通常在类的 __init__ 构造方法中,通过 self.属性名 = 值 的形式进行声明和赋值
生命周期:随着对象的创建而诞生,随着对象的销毁而消失
访问方式:必须通过对象名进行访问,如对象名.属性名
class Account: def __init__(self, username, balance): self.username = username # 实例属性 self.balance = balance # 实例属性 acc1 = Account("Alice", 5000) acc2 = Account("Bob", 8000) # 每个实例的属性完全独立 print(acc1.username) # 输出:"Alice" print(acc2.username) # 输出:"Bob"2. 类属性
类属性是属于类对象本身的属性。它被该类所有的实例对象所共同拥有,在内存中只有一份副本
定义:直接定义在类体内部、所有方法之外
生命周期:随着类的加载而诞生,在程序运行期间一直存在,直到程序结束
访问方式:推荐直接通过类名访问(类名.属性名),也可以通过对象名间接访问
class Account: # 类属性:定义银行的名称,所有账户共享此信息 bank_name = "ICBC" def __init__(self, username): self.username = username # 推荐:直接通过类名访问类属性 print(Account.bank_name) # 输出:"ICBC" acc = Account("Charlie") # 允许:通过实例对象间接访问类属性 print(acc.bank_name) # 输出:"ICBC"3. __dict__ 属性
Python 的类和实例在底层基于字典进行动态管理
每个类对象和实例对象内部都包含一个内置的只读属性 __dict__。它是一个标准的 Python 字典,里面存储了该对象当前拥有一切属性和对应的值
通过观察 __dict__,我们可以清晰地看到实例属性与类属性的底层存储分离:
# 观察类对象的 __dict__ print("Account类的属性字典:", Account.__dict__) acc_demo = Account("David") # 观察实例对象的 __dict__ print("acc_demo实例的属性字典:", acc_demo.__dict__)从上述结果可以看出,实例对象的 __dict__中默认并不包含类属性。实例之所以能读取到类属性,全赖于 Python 底层的属性查找优先级机制
4. 属性查找优先级
当获取或修改属性值时,Python 解释器在底层的物理逻辑如下:
属性读取逻辑
当执行 print(acc_demo.bank_name) 时:
解释器首先进入 acc_demo.__dict__ 寻找是否存在键名为 "bank_name" 的实例属性
若没有找到,解释器会去该实例对应的类属性字典 Account.__dict__ 中寻找
若类属性字典中存在,则返回对应的值;若最终都找不到,则抛出异常
属性修改问题
如果尝试通过实例对象去修改类属性,会引发一个隐蔽的行为:
acc_x = Account("UserX") acc_y = Account("UserY") # 尝试通过实例 acc_x 修改类属性 acc_x.bank_name = "CCB" print(Account.bank_name) # 输出:"ICBC"(类属性并未改变) print(acc_y.bank_name) # 输出:"ICBC"(其他实例读取类属性,也未改变) print(acc_x.bank_name) # 输出:"CCB" (只有 acc_x 改变了?)底层逻辑:
当执行 acc_x.bank_name = "CCB" 时,根据 Python 的赋值规则,该操作并不会去修改 Account.__dict__ 中的内容,而是在 acc_x.__dict__ 动态创建了一个同名的实例属性
此时,两个对象的属性字典状态如下:
当再次读取 acc_x.bank_name 时,由于在实例属性字典中第一步就命中了 "bank_name",所以直接返回了新创建的实例属性 "CCB"。这种现象在开发中被称为属性遮蔽
结论:
如果需要修改类属性,必须显式地通过类名进行操作,即执行
Account.bank_name = "CCB"。这样才能确保所有实例共享的全局数据被同步更新
四、方法
在面向对象的设计中,方法用于定义对象的行为逻辑。Python 提供了灵活的方法声明机制,根据行为绑定的主体不同,方法主要分为实例方法、类方法和静态方法。正确选择方法类型,能够使类的逻辑划分更加严密
1. 实例方法
实例方法是类中最普通、使用频率最高的方法类型。它用来表达具体对象实例的行为
语法特征:不需要任何特殊的修饰符。第一个参数必须是 self,用以接收调用该方法的实例对象
访问权限:通过 self 指针,实例方法既可以读取或修改专属的实例属性,也可以读取或修改公共的类属性
调用方式:推荐通过对象实例直接调用
class Connection: # 类属性 timeout = 30 def __init__(self, ip): self.ip = ip # 实例属性 # 实例方法 def connect_server(self): # 既能访问实例属性,也能访问类属性 print(f"正在 {self.ip} 服务器建立连接,超时时间 {self.timeout} ")2. 类方法
类方法是属于类对象本身的方法。它通常用于操作类属性,或者作为一种 "工厂方法" 来创建特定配置的类实例
语法特征:在方法上方必须显式声明@classmethod 修饰符。第一个参数固定为 cls,代表当前类对象本身
访问权限:由于类方法在调用时不会隐式接收实例对象,因此它只能访问和修改类属性,无法访问任何实例属性
调用方式:推荐直接通过类名调用,也可以通过对象间接调用
class Connection: timeout = 30 def __init__(self, ip): self.ip = ip # 类方法 @classmethod def set_timeout(cls, new_timeout): # 通过 cls 指针安全地修改类属性 cls.timeout = new_timeout print(f"全局超时时间已修改为 {cls.timeout} 秒。") # 无需创建实例,直接通过类名调用 Connection.set_timeout(60)3. 静态方法
静态方法本质上是托管在类的命名空间内部的普通函数。它与类或具体的实例在数据层面没有任何耦合关系,通常用于存放某些辅助性的工具函数
语法特征:在方法上方必须显式声明@staticmethod 修饰符。没有强制性的第一个参数(不需要 self 或 cls)
访问权限:静态方法无法直接访问实例属性,也无法直接访问类属性。如果需要使用外部数据,必须通过参数显式传递进来
调用方式:通常直接通过类名调用
class Connection: # 静态方法:纯粹的辅助性逻辑,判断 IP 格式是否合法 @staticmethod def validate_ip(ip_str): # 与类和实例的属性无关,仅处理传入的参数 if ip_str.count(".") == 3: return True return False # 直接通过类名调用工具方法 is_valid = Connection.validate_ip("192.168.1.1") print(f"IP格式检测结果:{is_valid}") # 输出:True| 方法 | 第一个参数 | 能访问实例 | 能访问类 |
|---|---|---|---|
| 实例方法 | self | √ | √ |
| 类方法 | cls | × | √ |
| 静态方法 | 无 | × | × |
五、继承
在现实业务开发中,随着系统功能的扩张,我们会发现许多不同的类之间存在着大量重复的属性和定义。如果为每个类都独立编写一套相同的代码,不仅会导致软件结构臃肿,也会使后期的系统维护演变为灾难。为了解决这些问题,面向对象引入了继承(Inheritance)
1. 继承基本概念
假设我们需要开发一个校园教务管理系统,系统中需要定义两个实体类:Teacher(教师类)和 Student(学生类)
Teacher 拥有属性:姓名、年龄、工号;方法:吃饭、授课
Student 拥有属性:姓名、年龄、学号;方法:吃饭、学习
通过这两个类可以发现,它们在数据结构和行为特征上存在着明显的交集。重合的部分(姓名、年龄、吃饭行为)本质上是 "人类" 这一通用实体的共有特征
为了消除这种结构上的冗余,我们可以将这些共有特征向上抽离,定义一个通用的父类(亦称基类 或超类)。而具体的 Teacher 和 Student 只需要声明与该父类的关联,即可作为子类(亦称派生类 )自动获取这些通用特征。这种设计思想就是继承
2. 继承语法结构
Python 支持通过在类名后面的小括号中指定另一个类名来实现继承。其标准的语法声明如下:
# 声明通用的父类 class Person: def __init__(self, name, age): self.name = name self.age = age def eat(self): print(f"{self.name} 正在用餐。") # 声明子类 Teacher,继承自 Person class Teacher(Person): # 子类专有的行为 def teach(self): print(f"{self.name} 老师正在讲台上授课。") # 声明子类 Student,继承自 Person class Student(Person): # 子类专有的行为 def study(self): print(f"{self.name} 学生正在教室里听课。")3. 继承底层机制
在子类 Teacher 的内部,明明没有写过 eat 方法,也没有写过 __init__ 构造方法,为什么实例化的子类对象可以直接调用它们呢?
这背后的物理根源依然在于我们上一章提到的属性字典查找路径机制
当执行如下代码时:
t = Teacher("张华", 35) t.eat()Python 解释器在底层的执行流程如下:
寻找构造方法:当执行 Teacher("张华", 35) 时,解释器首先寻找 Teacher.__dict__ 中是否存在 __init__ 方法。由于 Teacher 类中没有定义,解释器会顺着继承链自动向上检索其父类 Person.__dict__。在父类中成功命中后,调用父类的构造方法,完成了 self.name 和 self.age 的属性绑定(即属性继承的实现)
寻找行为方法:当执行 t.eat() 时,解释器首先在实例 t.__dict__ 中寻找该方法,未命中;随后去子类 Teacher.__dict__ 中寻找,依然未命中;最后,解释器再次沿着继承路径向上追溯到父类 Person.__dict__,成功命中并执行该方法(即方法继承的实现)
也就是说,子类并不需要将父类的代码物理复制到自己的内存空间里,而是通过一套清晰的向上寻址,在运行时动态复用了父类的方法和属性空间
4. 属性与方法继承案例
下面通过完整的运行案例,验证子类在完全不重写父类基础的情况下,对父类资源的复用:
# 实例化派生类(子类)对象 # 尽管 Student 没有写 __init__,但它自动继承并复用了 Person 的构造方法 student = Student("李明", 19) # 1. 验证属性继承:成功读取父类构造方法绑定的属性 print(f"学生姓名:{student.name}") # 输出:"李明" print(f"学生年龄:{student.age}") # 输出:19 # 2. 验证方法继承:成功调用父类中声明的行为 student.eat() # 输出:"李明 正在用餐。" # 3. 验证子类专有行为:成功调用自身独有的方法 student.study() # 输出:"李明 学生正在教室里听课。"通过继承,子类能够在不改变父类原有物理结构的前提下,通过引入专有的方法实现对系统功能的自适应扩展。这完美契合了软件工程中经典的开闭原则(Open-Closed Principle)——对扩展开放,对修改关闭
六、方法重写
虽然继承为子类提供了复用父类代码的便利,但在实际业务中,父类定义的通用行为往往无法完全满足某个特定子类的个性化需求。此时,子类需要对父类的方法进行重新改造,这被称为方法重写(Method Overriding)
1. 基本概念
方法重写是指子类重新定义一个与父类在函数名称、参数列表上完全相同的方法
从底层属性字典的视角来看,一旦子类在其自身的 __dict__ 中注册了与父类同名的方法函数,当实例对象调用该方法时,Python 解释器在子类字典中就会直接命中,从而拦截了向上追溯到父类字典的过程。这种机制使得子类能够表现出区别于父类的特定行为
super 函数
在进行方法重写时,子类往往并不是要完全抛弃父类的原有逻辑,而是在保留父类基础功能的同时,扩展出属于子类的特有业务逻辑
为了避免在子类中手工重复编写父类的逻辑,Python 提供了内建的super() 函数
功能定义:super() 返回一个代表父类的代理对象,允许子类在重写的方法内部,显式地调用父类中被遮蔽的同名方法
物理意义:它确保了父类的初始化代码或核心逻辑能够被安全地执行,是维护继承链条完整性的关键纽带
2. 方法重写案例
在软件工程中,重写最经典的应用场景是构造方法的扩展(__init__ 重写)以及核心业务行为的特异化
下面通过 "员工" 与 "经理" 的场景来演示这一机制:
class Employee: """父类:普通员工类""" def __init__(self, name, base_salary): self.name = name self.base_salary = base_salary def get_salary(self): """计算薪资的通用方法""" return self.base_salary def work(self): print(f"员工 {self.name} 正在处理日常业务。") class Manager(Employee): """子类:经理类""" def __init__(self, name, base_salary, bonus): # 1. 核心应用一:利用 super() 调用父类的构造方法,完成基础属性的绑定 super().__init__(name, base_salary) # 2. 绑定经理类特有的专属属性 self.bonus = bonus def get_salary(self): # 3. 核心应用二:重写薪资计算逻辑 # 经理的薪资 = 父类算出的基本薪资 + 专有的奖金 actual_salary = super().get_salary() + self.bonus return actual_salary def work(self): # 4. 核心应用三:重写行为方法 # 既保留父类的“日常业务”输出,又追加经理特有的“管理”行为 super().work() print(f"经理 {self.name} 正在主持部门周会。")运行与观测验证:
# 实例化子类对象 mgr = Manager("高诚", 15000, 5000) # 验证 get_salary() 的重写 print(f"经理实际薪资:{mgr.get_salary()} 元") # 验证 work() 的重写 mgr.work()3. 方法重写注意事项
在复杂的工程项目开发中,使用方法重写必须遵守以下规范:
命名一致性:重写方法的名称必须与父类方法完全一致。如果由于拼写错误(如将 get_salary 错写为 get_salery),Python 解释器会将其识别为一个全新的子类专有方法,导致重写机制失效
super() 的参数传递:在子类 __init__ 中调用 super().__init__(...) 时,传递的参数必须严格匹配父类构造方法所要求的形参列表
调用顺序:通常情况下,应当在子类重写方法的第一行代码中优先调用 super() 方法,确保父类的基础状态或依赖环境初始化完毕后,再执行子类的特有逻辑
通过方法重写与 super() 的配合,我们实现了子类对父类行为的精准重塑
七、继承关系判断
Python 提供了两个内置函数:isinstance() 与 issubclass()。它们用于在程序运行期动态检测对象与类、以及类与类之间的血缘关系
1. isinstance() 函数
isinstance(object, classinfo) 用于检测一个对象实例是否属于指定的类,或者属于该类的任何派生子类
参数说明:
object:需要检测的对象实例。
classinfo:目标类对象。也可以是一个包含多个类的元组(Tuple),只要对象属于元组中任意一个类,就会返回 True
返回值:布尔值
class Vehicle: pass class ElectricCar(Vehicle): pass class Airplane: pass # 实例化子类对象 tesla = ElectricCar() # 1. 验证对象是否属于其直接类 print(isinstance(tesla, ElectricCar)) # 输出:True # 2. 验证对象是否属于其父类(关键特性) print(isinstance(tesla, Vehicle)) # 输出:True # 3. 验证对象是否属于无关类 print(isinstance(tesla, Airplane)) # 输出:False # 4. 使用类元组进行多向联合判定 print(isinstance(tesla, (Airplane, Vehicle))) # 输出:True(命中 Vehicle)2. issubclass() 函数
与 isinstance() 关注对象与类的关系不同,issubclass(sub, classinfo) 专门用于检测类与类之间是否存在继承派生关系
参数说明:
sub:待检测的子类对象
classinfo:待检测的父类对象(同样支持类元组)
返回值:布尔值
# 检测子类与父类的继承关系 print(issubclass(ElectricCar, Vehicle)) # 输出:True print(issubclass(ElectricCar, Airplane)) # 输出:False # 严格判定:Python 中一个类默认被视为其自身的子类 print(issubclass(Vehicle, Vehicle)) # 输出:True3. 为什么不能使用 type() 进行类型判断
在基础语法学习中,type(object) 常用于获取对象的类型。然而在面向对象的工程实践中,强烈禁止使用 type(obj) == Class 的形式进行动态类型检查
以下代码清晰地展示了这种做法的设计缺陷:
# 错误示范:使用 type() 判定继承链 if type(tesla) == Vehicle: print("这是一辆交通工具") else: print("类型不匹配") # 实际运行输出:"类型不匹配"底层原因分析:
type() 函数的行为是绝对精确的,它只会返回对象在内存中被实例化时的那个最直接的类(即 ElectricCar)。它无法识别面向对象中的继承层次结构
由于 tesla 确实是一辆交通工具(Vehicle),使用 type() 会直接割裂子类与父类之间的关联。因此,遵从面向对象的设计规范,应当统一使用 isinstance() 来实现鲁棒的类型检查。
通过运行时类型检查,我们能够确保继承链条中的动态对象在传递时的类型安全
八、多重继承
在前面的章节中,我们探讨的都是一个子类只继承一个父类的单重继承结构。然而,现实世界中的某些实体可能同时具备多种不同特征。为了模拟这种复杂的映射关系,Python 支持多重继承,即允许一个子类同时继承多个父类
多重继承在带来强大扩展能力的同时,也引入了方法命名冲突的隐患。为了彻底解决这一冲突,Python 底层设计了一套严密的方法搜索算法
1. 多重继承的语法实现
在 Python 中实现多重继承,只需在子类声明的小括号内,将多个父类用英文逗号,隔开即可
class Flyable: def fly(self): print("具备飞行能力。") class Swimmable: def swim(self): print("具备游泳能力。") # AmphibiousVehicle(水陆两栖车)同时继承两个父类 class AmphibiousVehicle(Flyable, Swimmable): pass obj = AmphibiousVehicle() obj.fly() # 来自 Flyable obj.swim() # 来自 Swimmable2. 菱形继承与命名冲突
当多个父类中存在同名方法时,多重继承的复杂性会剧增。最经典的架构冲突被称为菱形继承:
类 A 是基类,类 B 和类 C 分别继承自类 A,而类 D 同时继承自类 B 和类 C
如果类 B 和类 C 都重写了类 A 的某个同名方法,那么当类 D 的实例调用该方法时,Python 究竟应该执行类 B 的逻辑,还是类 C 的逻辑?
为了消除这种歧义性,Python 引入了MRO机制
3. MRO 与 C3 线性化算法
MRO 决定了一个类在继承拓扑结构中,方法被搜索的先后物理顺序。Python 3 采用C3 线性化算法来计算这个顺序。该算法保证了多重继承环境下的三个原则:
子类优先于父类被检索
在小括号中靠前的父类优先于靠后的父类被检索(从左至右)
如果存在共同的终极基类,该基类将最后被检索(单调性)
如何查看 MRO 顺序?
Python 提供了两种内置途径来直接观测某个类的 MRO 线性列表:
读取类的魔术属性类名.__mro__(返回元组)
调用类方法类名.mro()(返回列表)
下面通过一个标准的菱形继承案例进行验证:
class A: def show(self): print("A.show") class B(A): def show(self): print("B.show") class C(A): def show(self): print("C.show") class D(B, C): pass # 查看类 D 的方法分辨率顺序 (MRO) print(D.mro()) # 输出:[<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]搜索路径解析:
当执行d = D() d.show()时,解释器会严格按照 D.mro() 输出的线性链条依次寻找 show 方法:
首先检查 D 自身,未找到
接着检查左侧第一个父类 B,成功命中,立刻执行 B.show 并停止向下搜索
因此,屏幕最终输出 "B.show"。
4. 多重继承下的 super 行为
在单重继承中常将 super() 简单理解为调用父类。但在多重继承架构下,这种理解会导致严重的逻辑错误
super() 的本质并不是调用直系父类,而是严格按照当前实例所属类的 MRO 列表,调用该列表中的下一个(Next)类
class Base: def __init__(self): print("Base 入口") print("Base 出口") class SubLeft(Base): def __init__(self): print("SubLeft 入口") super().__init__() # 注意:此时它调用的可能不是 Base! print("SubLeft 出口") class SubRight(Base): def __init__(self): print("SubRight 入口") super().__init__() print("SubRight 出口") class Final(SubLeft, SubRight): def __init__(self): print("Final 入口") super().__init__() print("Final 出口")执行 f = Final() 的输出结果:
运行轨迹拆解:
首先,Final.mro() 的物理顺序为:[Final, SubLeft, SubRight, Base, object]
进入 Final 的构造函数,打印 "Final 入口"
执行 super().__init__(),在 MRO 链条中 Final 的下一个是 SubLeft,于是进入 SubLeft,打印 "SubLeft 入口"
关键点:在 SubLeft 中执行 super().__init__() 时,Python 会参考原始实例的 MRO 链条(即 Final 的 MRO)。在链条中,SubLeft 的下一个类是 SubRight,而不是它的直系父类 Base
于是控制权交给了 SubRight,打印 "SubRight 入口"
SubRight 执行 super().__init__(),MRO 下一个是 Base,打印 "Base 入口"、"Base 出口"
各层函数开始执行未完成的尾部代码,依次逆向弹栈,最终打印出完整的拓扑嵌套闭合
这种设计确保了在复杂的网状继承结构中,每一个基类的构造方法有且仅被精确执行一次,避免了资源的重复初始化
5. 多重继承注意事项
多重继承虽然强大,但极易破坏软件代码的直观性。在软件工程中应遵循以下设计规范:
优先采用 Mixin 模式设计:不要建立过于深厚的网状多重继承。通常推荐使一个子类主继承一个核心父类,其余伴随的父类仅作为功能补充的 "混入类(Mixin)"。Mixin 类应当职能单一,且其内部不定义 __init__ 状态属性
凡重写必用 super():如果在多重继承的体系中,某个子类重写了方法却直接通过父类名.方法名(self)进行调用,将会彻底割裂 C3 算法的链条,导致某些公共基类被重复执行
保持参数一致性:整个继承网路中,相关联的同名方法的参数签名应当尽量保持兼容,否则在 MRO 链条动态流转时,极易触发参数投递不匹配的运行时崩溃
总结
本章围绕 Python 面向对象编程的基础知识展开,系统学习了类、对象、实例化、属性、方法以及继承等核心内容。通过本章的学习,我们不仅掌握了如何定义类、创建对象,还了解了实例属性与类属性、实例方法、类方法、静态方法之间的区别,以及继承、多重继承和方法重写等重要特性
与面向过程相比,面向对象更加注重对现实世界的抽象,它将数据与操作数据的方法封装在一起,使程序具有更好的可维护性、可扩展性和复用性。而继承机制则进一步减少了重复代码,让不同类之间能够共享已有的功能,从而提高开发效率
不过,本章更多介绍的是如何使用面向对象,而真正理解面向对象的设计思想,还需要进一步学习对象之间是如何协作的,以及 Python 在面向对象方面提供的更多高级特性
下一篇文章,我们将继续深入学习 Python 面向对象编程,重点介绍封装(权限控制)、多态、抽象类、鸭子类型以及对象的内存模型等内容,从 "会使用类" 逐步迈向 "理解面向对象设计",真正掌握 Python 面向对象编程的核心思想