Python 面向对象(二):继承与封装的深度探索
在上一篇文章中,我们初步认识了面向对象编程的基本概念,包括类、实例、属性和方法等核心元素。今天我们将聚焦于面向对象中实现代码重用的核心机制 ------ 继承,同时深入探讨封装的高级用法和多态的实践意义。这些知识将帮助我们写出更灵活、更易维护的代码。
一、继承:站在父类的肩膀上
继承是面向对象编程实现代码重用的核心手段。通过继承,我们可以从已有的类(父类)中 "继承" 属性和方法,同时在此基础上扩展新的功能。这种机制就像现实世界中 "子承父业"------ 子女继承父母的财产,同时可以开创自己的事业。
继承的基本使用
在 Python 中,定义子类时只需在类名后的括号中指定父类即可。我们用一个 "厨房电器" 的例子来理解继承的核心逻辑:
python
# 父类:厨房电器
class KitchenAppliance:
def __init__(self, brand, power):
self.brand = brand # 品牌(非私有属性)
self.power = power # 功率(非私有属性)
self.__working_hours = 0 # 工作时长(私有属性,双下划线开头)
def start(self):
print(f"{self.brand}电器开始工作,功率{self.power}W")
def __record_working(self): # 私有方法
self.__working_hours += 1
print(f"累计工作{self.__working_hours}小时")
# 子类:咖啡机(继承自厨房电器)
class CoffeeMachine(KitchenAppliance):
pass # 暂时不添加任何内容
# 创建子类实例
my_coffee = CoffeeMachine("飞利浦", 1200)
my_coffee.start() # 输出:飞利浦电器开始工作,功率1200W(继承父类方法)
print(my_coffee.brand) # 输出:飞利浦(访问继承的非私有属性)
# my_coffee.__record_working() # 报错:无法访问父类私有方法
# print(my_coffee.__working_hours) # 报错:无法访问父类私有属性
这个例子完美诠释了继承的第一句核心规则:子类拥有父类非私有化的属性和方法,但无法直接访问私有成员。父类中的__working_hours(私有属性)和__record_working(私有方法)被封装在父类内部,子类无法直接使用。
子类的扩展:添加专属属性和方法
继承的第二句规则是 "子类可以拥有自己的属性和方法"。我们给CoffeeMachine添加专属功能:
python
class CoffeeMachine(KitchenAppliance):
def __init__(self, brand, power, cup_capacity):
# 调用父类构造方法初始化继承的属性
super().__init__(brand, power)
self.cup_capacity = cup_capacity # 新增属性:一次可制作的杯数
# 新增方法:制作咖啡
def make_coffee(self, type_):
print(f"制作{type_}咖啡,一次可做{self.cup_capacity}杯")
# 使用扩展后的子类
my_coffee = CoffeeMachine("德龙", 1500, 4)
my_coffee.start() # 继承的父类方法
my_coffee.make_coffee("拿铁") # 子类专属方法:制作拿铁咖啡,一次可做4杯
print(my_coffee.cup_capacity) # 访问子类专属属性:4
子类通过super().init()调用父类的构造方法,确保父类的属性被正确初始化,同时添加了cup_capacity属性和make_coffee方法,实现了对父类的扩展。
重写父类方法:用自己的方式实现功能
当子类需要对父类的方法进行个性化实现时,可以重写(Override)父类方法,这就是继承的第三句规则:"子类可以用自己的方式实现父类的方法"。
python
class CoffeeMachine(KitchenAppliance):
# (省略__init__和make_coffee方法,同上)
# 重写父类的start方法
def start(self):
print(f"{self.brand}咖啡机启动,预热中...功率{self.power}W")
# 对比父类和子类的方法
appliance = KitchenAppliance("海尔", 800)
appliance.start() # 输出:海尔电器开始工作,功率800W(父类方法)
coffee = CoffeeMachine("德龙", 1500, 4)
coffee.start() # 输出:德龙咖啡机启动,预热中...功率1500W(重写后的方法)
重写后,子类实例调用start()时会执行自己的实现,而不是父类的方法。这种机制让子类在保留方法名的同时,实现了个性化功能。
调用父类方法:在重写中复用父类逻辑
有时我们需要在重写的方法中保留父类的功能,这时可以通过super()函数或父类名调用父类方法:
ruby
class CoffeeMachine(KitchenAppliance):
def start(self):
# 方式1:用super()调用父类方法
super().start()
# 补充子类自己的逻辑
print("咖啡机开始研磨咖啡豆...")
def clean(self):
# 方式2:用父类名调用父类方法(需传入self)
KitchenAppliance.start(self)
print("咖啡机进行自动清洗...")
coffee = CoffeeMachine("德龙", 1500, 4)
coffee.start()
# 输出:
# 德龙电器开始工作,功率1500W(父类方法)
# 咖啡机开始研磨咖啡豆...(子类补充逻辑)
coffee.clean()
# 输出:
# 德龙电器开始工作,功率1500W(父类方法)
# 咖啡机进行自动清洗...(子类逻辑)
两种方式的区别在于:super()会自动处理继承关系,在多重继承中更安全;而父类名调用方式更直接,但在多重继承中可能引发问题。
二、多重继承:同时继承多个父类
Python 支持多重继承,即一个子类可以同时继承多个父类。语法上只需在类名后的括号中用逗号分隔多个父类:
python
# 父类1:带定时功能的设备
class TimedDevice:
def set_timer(self, minutes):
print(f"设置定时器:{minutes}分钟后自动关闭")
# 父类2:带显示功能的设备
class DisplayDevice:
def show_status(self):
print(f"{self.brand}设备当前状态:运行中")
# 子类:智能烤箱(同时继承两个父类)
class SmartOven(KitchenAppliance, TimedDevice, DisplayDevice):
def bake(self, food):
print(f"烤制{food}中...")
# 使用多重继承的子类
oven = SmartOven("美的", 2000)
oven.start() # 继承自KitchenAppliance
oven.set_timer(30) # 继承自TimedDevice
oven.show_status() # 继承自DisplayDevice
oven.bake("鸡翅") # 子类自己的方法
多重继承虽然强大,但可能引发 "菱形问题"(多个父类最终继承自同一个基类时的方法调用冲突)。Python 通过MRO(方法解析顺序) 解决这个问题,即按照类名.__mro__显示的顺序查找方法:
arduino
print(SmartOven.__mro__)
# 输出:
# (<class '__main__.SmartOven'>, <class '__main__.KitchenAppliance'>,
# <class '__main__.TimedDevice'>, <class '__main__.DisplayDevice'>,
# <class 'object'>)
调用方法时,Python 会按 MRO 顺序查找,找到第一个匹配的方法就执行。
三、实用内置函数与属性:对象信息的查询工具
除了基础的type()和isinstance(),Python 还提供了多个实用工具用于查询对象信息:
1. type()与isinstance():判断对象类型
- type(obj):返回对象的类型
- isinstance(obj, cls):判断对象是否是指定类(或其子类)的实例
python
oven = SmartOven("美的", 2000)
print(type(oven)) # 输出:<class '__main__.SmartOven'>
print(isinstance(oven, SmartOven)) # 输出:True
print(isinstance(oven, KitchenAppliance)) # 输出:True(子类实例也是父类的实例)
2. issubclass():判断类的继承关系
issubclass(sub, parent)用于检查sub是否是parent的子类:
python
print(issubclass(SmartOven, KitchenAppliance)) # 输出:True
print(issubclass(CoffeeMachine, TimedDevice)) # 输出:False
3. dir():查看对象的所有属性和方法
dir(obj)返回对象所有可用的属性和方法列表:
ini
coffee = CoffeeMachine("德龙", 1500, 4)
print(dir(coffee)) # 输出包含brand、power、cup_capacity等属性,以及start、make_coffee等方法
4. __dict__属性:对象的属性字典
obj.__dict__返回一个字典,包含对象的所有实例属性及其值:
go
print(coffee.__dict__)
# 输出:{'brand': '德龙', 'power': 1500, 'cup_capacity': 4}
5. hasattr():检查对象是否有指定属性
hasattr(obj, name)判断对象是否包含名为name的属性:
python
print(hasattr(coffee, "brand")) # 输出:True
print(hasattr(coffee, "weight")) # 输出:False
这些工具在调试和动态操作对象时非常实用,比如通过hasattr()和getattr()可以实现灵活的属性访问。
四、封装的高级用法:私有成员与 property 属性
封装的核心是隐藏对象的内部状态,只通过公开接口与外部交互。除了基础的私有属性和方法,Python 还提供了property机制用于属性的精细化控制。
私有成员的访问控制
父类的私有成员(以双下划线__开头)只能在父类内部访问,子类和外部都无法直接访问。但可以通过父类提供的公开方法间接访问:
python
class KitchenAppliance:
def __init__(self, brand):
self.__brand = brand # 私有属性
# 公开方法:获取私有属性
def get_brand(self):
return self.__brand
# 公开方法:设置私有属性(带验证)
def set_brand(self, new_brand):
if isinstance(new_brand, str) and new_brand.strip():
self.__brand = new_brand
else:
print("品牌名称必须是非空字符串")
appliance = KitchenAppliance("苏泊尔")
print(appliance.get_brand()) # 输出:苏泊尔(通过公开方法访问)
appliance.set_brand("") # 输出:品牌名称必须是非空字符串(验证生效)
私有方法的定义与使用
私有方法同样以__开头,只能在类内部调用,用于封装内部逻辑:
python
class BankCard:
def __init__(self, balance=0):
self.__balance = balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self.__log_transaction(f"存款{amount}元") # 调用私有方法
else:
print("存款金额必须为正数")
# 私有方法:记录交易日志
def __log_transaction(self, msg):
print(f"交易记录:{msg}")
card = BankCard()
card.deposit(500) # 输出:交易记录:存款500元(内部调用私有方法)
# card.__log_transaction("测试") # 报错:无法外部调用
property 属性:让属性访问更优雅
property可以将方法伪装成属性,既保留了方法的逻辑控制,又能像属性一样直接访问。有两种实现方式:
方式 1:装饰器语法
python
class Book:
def __init__(self, price):
self.__price = price # 私有属性
@property
def price(self): # 相当于getter
return f"¥{self.__price:.2f}"
@price.setter
def price(self, new_price): # 相当于setter
if new_price > 0:
self.__price = new_price
else:
print("价格必须大于0")
book = Book(39.9)
print(book.price) # 输出:¥39.90(像属性一样访问)
book.price = 49.9 # 像属性一样赋值
print(book.price) # 输出:¥49.90
book.price = -10 # 输出:价格必须大于0(验证生效)
方式 2:类属性语法
python
class Book:
def __init__(self, price):
self.__price = price
def get_price(self):
return f"¥{self.__price:.2f}"
def set_price(self, new_price):
if new_price > 0:
self.__price = new_price
else:
print("价格必须大于0")
# 定义property属性
price = property(get_price, set_price)
# 使用方式和装饰器语法完全一致
book = Book(29.9)
print(book.price) # 输出:¥29.90
property让我们可以在不改变外部接口的情况下,对属性的访问和赋值添加逻辑控制(如验证、格式化),是封装思想的高级应用。
五、多态:同一接口,不同实现
多态是指不同的对象对同一方法调用做出不同响应的能力。它依赖于继承和方法重写,让我们可以用统一的接口处理不同类型的对象。
ini
# 定义统一接口函数
def use_appliance(appliance):
appliance.start() # 调用统一的start方法
# 创建不同的子类实例
coffee = CoffeeMachine("德龙", 1500, 4)
oven = SmartOven("美的", 2000)
# 同一接口处理不同对象
use_appliance(coffee) # 输出:德龙电器开始工作,功率1500W(咖啡机的start)
use_appliance(oven) # 输出:美的电器开始工作,功率2000W(烤箱的start)
即使未来添加新的厨房电器子类(如Juicer),只要它继承KitchenAppliance并实现start()方法,use_appliance函数无需任何修改就能正常工作。这种 "开闭原则" 正是多态的价值所在 ------ 扩展新功能时无需修改原有代码。
六、总结与实践建议
本文深入探讨了面向对象的核心机制:
- 继承通过 "父类定义共性,子类实现个性" 实现代码重用
- 多重继承需注意 MRO 顺序,避免方法调用冲突
- 私有成员和property属性强化了封装的安全性和灵活性
- 多态通过统一接口实现了代码的扩展性
实践中建议:
- 继承不要超过 3 层,避免代码逻辑过于复杂
- 优先使用组合(将对象作为属性)而非多重继承解决复杂问题
- 用property替代直接暴露属性,为未来扩展留有余地
- 设计类时考虑多态性,让接口更通用
下一篇我们将探讨魔法方法,敬请期待!如果您在实践中遇到问题,欢迎在评论区交流讨论。