方法重写
上一节学习 MRO 时,如果子类中没有需要的方法,Python 会继续按照搜索顺序到父类中查找。
但父类提供的通常是通用行为,具体子类可能需要不同的实现。动物都有 eat(),老虎和狮子吃东西时却可以有自己的表现。子类应该完全替换父类行为,还是保留父类逻辑后再扩展?
方法重写概念
子类重新定义父类中的同名方法,叫作方法重写,也叫 override。
class 父类:
def 方法名(self):
pass
class 子类(父类):
def 方法名(self):
pass子类对象调用这个方法时,会先找到子类自己的版本。父类原来的方法仍然保留,父类对象调用时不会受到影响。
方法重写通常分成两种情况:
- 完全使用子类自己的实现
- 保留父类逻辑,再增加子类自己的操作
父类方法定义
先定义 Animal,提供所有动物都能使用的 eat():
class Animal:
def __init__(self, name, high, weight):
self.name = name
self.high = high
self.weight = weight
def eat(self):
print(f'{self.name} 正在吃东西')
animal = Animal('普通动物', 1, 20)
animal.eat()运行结果:
普通动物 正在吃东西接下来分别用老虎和狮子演示两种重写方式。
完全覆盖父类方法
如果父类的方法不适合当前子类,应该如何处理?可以在子类中编写完整的新实现:
class Tiger(Animal):
# 子类重新定义同名方法,完全使用自己的实现
def eat(self):
print(f'{self.name} 正在撕咬食物')
print(f'{self.name} 发出咔咔的声音')
tiger = Tiger('泰哥', 1.88, 360)
animal.eat()
tiger.eat()运行结果:
普通动物 正在吃东西
泰哥 正在撕咬食物
泰哥 发出咔咔的声音Tiger.eat() 没有调用父类方法,执行的全部是老虎自己的逻辑。这种写法可以称为完全覆盖。
同时调用 animal.eat() 可以看到,Animal.eat() 没有被修改。方法重写只改变当前子类对象优先使用哪个版本。
基于父类逻辑扩展
如果父类方法仍然有用,如何避免重复编写通用逻辑?可以先通过 super() 调用父类版本,再补充子类逻辑:
class Lion(Animal):
def eat(self):
# 先执行父类的通用逻辑
super().eat()
# 再补充子类自己的逻辑
print(f'{self.name} 吃完后发出吼声')
lion = Lion('辛巴', 2, 380)
lion.eat()运行结果:
辛巴 正在吃东西
辛巴 吃完后发出吼声执行 lion.eat() 时,程序会按下面的顺序运行:
- 进入
Lion.eat() - 执行
super().eat(),调用父类的通用逻辑 - 回到
Lion.eat(),继续执行子类新增的逻辑
这里的 super() 与初始化父类属性时使用的是同一个工具:它会沿着 MRO 继续查找指定方法。
重写策略对比
| 重写方式 | 是否调用 super() | 适用情况 |
|---|---|---|
| 完全覆盖 | 否 | 父类逻辑不适合子类,需要全部替换 |
| 基于父类扩展 | 是 | 父类逻辑仍然有用,子类只增加部分操作 |
选择哪一种方式,取决于父类逻辑是否还需要保留。不要为了使用 super() 而强行调用父类方法,也不要在可以复用父类逻辑时重复编写相同代码。
案例:内置列表方法扩展
需求:继承 list,给 append() 增加一条操作提示,同时保留原有的列表追加行为。
功能分析:在子类中重写 append(),先输出提示,再用 super() 调用内置列表的实现。
class LoggedList(list):
def append(self, value):
print(f'准备添加:{value}')
super().append(value)
names = LoggedList(['张三', '李四'])
names.append('王五')
print(names)运行结果:
准备添加:王五
['张三', '李四', '王五']LoggedList.append() 先输出提示,再调用 list.append() 完成真正的添加操作。原方法负责的核心行为和返回规则都没有改变,只增加了日志功能。
这个示例先了解即可。继承内置类时,应尽量保持原有方法的使用习惯,避免让同名方法表现出完全不同的含义。
小结
本节重点掌握三件事:
- 方法重写就是在子类中重新定义父类的同名方法
- 完全覆盖只执行子类逻辑,不会修改父类原来的方法
- 使用
super().方法名()可以先复用父类逻辑,再增加子类操作
练习
- 定义
Animal和Dog,让Dog完全覆盖父类的eat() - 修改
Dog.eat(),先调用父类方法,再输出小狗自己的进食动作 - 分别创建父类和子类对象,对比它们调用
eat()的运行结果 - 继承
dict,重写clear(),清空前先输出字典中原有的数据