[python] python抽象基类使用总结
在Python中,抽象基类是一类特殊的类,它不能被实例化,主要用于作为基类被其他子类继承。抽象基类的核心作用是为一组相关的子类提供统一的蓝图或接口规范,明确规定子类必须实现的方法,从而增强代码的规范性和可维护性。Python通过abc(Abstract Base Classes)模块提供了对抽象基类的支持,允许开发者创建和使用抽象基类。
抽象基类的主要特点和用途包括:
- 接口一致性:通过定义抽象方法,抽象基类确保所有子类必须实现这些方法,从而保证子类具有一致的接口;
- 避免不完整实现:若子类未实现抽象基类中的所有抽象方法,该子类仍会被视为抽象基类,无法实例化;
- 提高代码可维护性:清晰定义的接口使代码结构更清晰,便于团队协作和系统扩展。
Python要创建抽象基类,需继承abc.ABC并使用@abstractmethod装饰器标记必须被重写的方法。在实际应用中,抽象基类广泛用于以下场景:
- 框架设计:定义接口规范,强制子类实现特定方法,确保框架扩展的一致性;
- 插件系统:规定插件必须实现的通用接口,方便系统动态加载第三方模块;
- 团队协作:明确模块间的交互契约,避免开发人员遗漏关键方法的实现;
- 代码复用:通过抽象基类封装通用逻辑,子类只需实现差异化部分;
- 类型检查:结合isinstance()进行运行时类型验证,确保对象符合预期接口;
- 复杂系统架构:构建多层次的类继承体系,清晰划分各层级的职责边界。
通过合理使用抽象基类,开发者可以创建更健壮、更具扩展性的代码架构,同时减少因接口不一致导致的错误。
1 使用入门
创建基础抽象基类
以下代码展示了Python中面向对象编程的几个重要概念:
- 抽象基类 (Abstract Base Class, ABC)Animal(ABC) 是一个抽象基类,继承自ABC(需要从abc模块导入)。作用:抽象基类用于定义一组方法的接口规范,但不能被直接实例化。它要求子类必须实现这些方法,否则会报错。关键点:
抽象基类通过@abstractmethod装饰器标记抽象方法。
如果子类没有实现所有抽象方法,Python会阻止子类的实例化。 - 抽象方法 (@abstractmethod)move()和sound()是抽象方法,用@abstractmethod装饰。作用:强制子类必须实现这些方法。定义了一个统一的接口规范(例如所有动物都必须能“移动”和“发声”)。关键点:抽象方法只有声明,没有实现(用pass关键字占位)。如果子类不实现这些方法,尝试实例化时会引发 TypeError。
- 继承 (Bird和Fish继承Animal)Bird和Fish是Animal的子类。子类必须实现父类中所有的抽象方法。不同子类对同一方法有不同的实现。
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def move(self):
pass
@abstractmethod
def sound(self):
pass
class Bird(Animal):
def __init__(self, name):
self.name = name
def move(self):
return f"{self.name} is flying"
def sound(self):
return "Chirp chirp"
class Fish(Animal):
def __init__(self, name):
self.name = name
def move(self):
return f"{self.name} is swimming"
def sound(self):
return "Blub blub"
# 创建实例并调用方法
sparrow = Bird("Sparrow")
print(sparrow.move()) # 输出: Sparrow is flying
print(sparrow.sound()) # 输出: Chirp chirp
salmon = Fish("Salmon")
print(salmon.move()) # 输出: Salmon is swimming
print(salmon.sound()) # 输出: Blub blub
# animal = Animal() # 尝试实例化抽象基类会引发TypeError
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.
Sparrow is flying
Chirp chirp
Salmon is swimming
Blub blub
抽象属性Abstract property
以下示例将name方法声明为抽象属性,要求所有继承Person的子类必须实现这个属性。使用@property表示这应该是一个属性而不是普通方法。通过@property装饰器,用于将类的方法转换为"属性",使得可以像访问属性一样访问方法,而不需要使用调用语法(即不需要加括号)。注意子类必须同样使用@property装饰器来实现该属性。
使用@property的优势在于能够控制访问权限,定义只读属性,防止属性被意外修改。例如:
emp = Employee("Sarah", "Johnson")
emp.name = "Alice" # 会报错,AttributeError: can't set attribute
1.2.
示例代码如下:
from abc import ABC, abstractmethod
class Person(ABC):
"""抽象基类,表示一个人"""
@property
@abstractmethod
def name(self) -> str:
"""获取人的姓名"""
pass
@abstractmethod
def speak(self) -> str:
"""人说话的抽象方法"""
pass
class Employee(Person):
"""表示公司员工的类"""
def __init__(self, first_name: str, last_name: str):
"""
初始化员工对象
Args:
first_name: 员工的名字
last_name: 员工的姓氏
"""
if not first_name or not last_name:
raise ValueError("名字和姓氏不能为空")
self._full_name = f"{first_name} {last_name}"
@property
def name(self) -> str:
"""获取员工的全名"""
return self._full_name
def speak(self) -> str:
"""员工打招呼的具体实现"""
return f"Hello, my name is {self.name}"
# 创建员工实例并测试
emp = Employee("Sarah", "Johnson")
# emp.name = "Alice" # 会报错,AttributeError: can't set attribute
print(emp.name) # 输出: Sarah Johnson
print(emp.speak()) # 输出: Hello, my name is Sarah Johnson
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.
Sarah Johnson
Hello, my name is Sarah Johnson
带类方法的抽象基类
当方法不需要访问或修改实例状态(即不依赖self属性)时,使用类方法可以避免创建不必要的实例,从而提高效率并简化代码。
from abc import ABC, abstractmethod
# 抽象基类:包裹
class Package(ABC):
@classmethod
@abstractmethod
def pack(cls, items):
pass
@classmethod
@abstractmethod
def unpack(cls, packed_items):
pass
# 具体实现类:纸箱包裹
class CardboardBox(Package):
@classmethod
def pack(cls, items):
return f"纸箱包裹: {items}"
@classmethod
def unpack(cls, packed_items):
return packed_items.replace("纸箱包裹: ", "")
# 具体实现类:泡沫包裹
class FoamPackage(Package):
@classmethod
def pack(cls, items):
return f"泡沫包裹: {items}"
@classmethod
def unpack(cls, packed_items):
return packed_items.replace("泡沫包裹: ", "")
# 打包物品
cardboard_packed = CardboardBox.pack(["衣服", "鞋子"])
foam_packed = FoamPackage.pack(["玻璃制品", "陶瓷"])
# 解包物品,使用不同的对象
cardboard_items = CardboardBox.unpack(cardboard_packed)
foam_items = FoamPackage.unpack(foam_packed)
# 输出结果
print("解包后 - 纸箱:", cardboard_items)
print("解包后 - 泡沫:", foam_items)
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.
解包后 - 纸箱: ['衣服', '鞋子']
解包后 - 泡沫: ['玻璃制品', '陶瓷']
带有具体方法的抽象基类
该示例呈现了一个兼具抽象方法与具体方法(实例方法)的抽象基类。抽象基类中既包含子类必须实现的抽象方法,也有提供共享功能的具体方法。operate具体方法界定了 “启动→运行→停止” 的通用操作流程,而具体实现则由子类负责。此模式让抽象基类能够把控算法结构,同时将细节实现延迟至子类。这不仅提升了代码的可维护性,还便于在不改动现有代码结构的前提下添加摩托车、飞机等新的交通工具。
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def start(self):
pass
@abstractmethod
def stop(self):
pass
def operate(self):
self.start()
print("Vehicle is in operation...")
self.stop()
class Car(Vehicle):
def start(self):
print("Starting car engine")
def stop(self):
print("Turning off car engine")
class Bicycle(Vehicle):
def start(self):
print("Starting to pedal bicycle")
def stop(self):
print("Applying bicycle brakes")
# 使用示例
car = Car()
car.operate()
bicycle = Bicycle()
bicycle.operate()
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.
Starting car engine
Vehicle is in operation...
Turning off car engine
Starting to pedal bicycle
Vehicle is in operation...
Applying bicycle brakes
非显式继承
这个示例展示了如何在不进行显式继承的情况下,将类注册为抽象基类的虚拟子类。register方法允许声明某个类实现了抽象基类,却无需直接继承该基类。
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def move(self):
pass
class Car:
def move(self):
return "Driving on the road!"
# 注册Car类为Vehicle的虚拟子类
Vehicle.register(Car)
car = Car()
# 输出: True(因为 Car 已被注册为 Vehicle 的虚拟子类)
print(isinstance(car, Vehicle))
# 输出: True(同上)
print(issubclass(Car, Vehicle))
print(car.move())
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
True
True
Driving on the road!
一般来说虚拟子类必须实现所有的抽象方法,但这种检查要等到尝试调用这些方法时才会进行。在处理无法修改的类或者使用鸭子类型时,这种方式十分实用。注意鸭子类型是Python中的一个重要编程概念,源自一句谚语:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”(If it walks like a duck and quacks like a duck, then it must be a duck)。
在Python中,鸭子类型指的是:
- 不关注对象的类型本身,而是关注对象具有的行为(方法或属性);
- 只要一个对象具有所需的方法或属性,它就可以被当作特定类型使用,而不需要显式地继承或声明。
示例代码如下,duck_test函数并不关心传入的对象是Duck还是Person,只要该对象拥有quack和walk方法,就可以正常调用。
class Duck:
def quack(self):
print("嘎嘎嘎")
def walk(self):
print("摇摇摆摆地走")
class Person:
def quack(self):
print("人类模仿鸭子叫")
def walk(self):
print("人类两条腿走路")
def duck_test(duck):
duck.quack()
duck.walk()
# 创建Duck和Person的实例
donald = Duck()
john = Person()
# 调用duck_test函数
duck_test(donald)
duck_test(john)
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.
嘎嘎嘎
摇摇摆摆地走
人类模仿鸭子叫
人类两条腿走路
多重继承
以下例子展示了抽象基类在多重继承中的应用。通过多重继承,可以将多个抽象基类组合,创建出能实现多种接口的类。例如,RadioRecorder类同时继承了Listenable和Recordable两个抽象基类,并实现了它们的所有抽象方法。这种方式既满足了严格的实现要求,又能灵活地定义接口。
from abc import ABC, abstractmethod
# 定义可收听的抽象接口
class Listenable(ABC):
@abstractmethod
def listen(self):
pass
# 定义可录制的抽象接口
class Recordable(ABC):
@abstractmethod
def record(self, content):
pass
# 收音机录音机实现类
class RadioRecorder(Listenable, Recordable):
def __init__(self, channel):
self.channel = channel # 收音机频道
self.recording = [] # 录制内容存储
def listen(self):
return f"Listening to {self.channel}"
def record(self, content):
self.recording.append(content)
return f"Recording '{content}' on {self.channel}"
# 使用示例
radio = RadioRecorder("FM 98.6")
print(radio.listen())
print(radio.record("Music"))
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.
Listening to FM 98.6
Recording 'Music' on FM 98.6
如果两个抽象基类有相同的方法名,会导致方法冲突。 Python中,当多重继承的父类存在同名方法时,调用顺序由方法解析顺序。例如以下代码中抽象基类都存在change方法,在子类change方法内部可以根据参数类型分别处理不同的逻辑来避免冲突:
from abc import ABC, abstractmethod
# 定义可收听的抽象接口
class Listenable(ABC):
@abstractmethod
def listen(self):
pass
@abstractmethod
def change(self, channel):
"""切换收听频道"""
pass
# 定义可录制的抽象接口
class Recordable(ABC):
@abstractmethod
def record(self, content):
pass
@abstractmethod
def change(self, format):
"""切换录制格式"""
pass
# 收音机录音机实现类
class RadioRecorder(Listenable, Recordable):
def __init__(self, channel, format):
self.channel = channel # 收音机频道
self.format = format # 录制格式
self.recording = [] # 录制内容存储
def listen(self):
return f"Listening to {self.channel}"
def record(self, content):
self.recording.append(content)
return f"Recording '{content}' in {self.format}"
# 解决方法冲突
def change(self, param):
# 根据参数类型判断调用哪个父类的change方法
if isinstance(param, str): # 假设字符串参数是频道
self.channel = param
return f"Changed channel to {param}"
elif isinstance(param, int): # 假设整数参数是格式编号
formats = ["MP3", "WAV", "FLAC"]
if 0 <= param < len(formats):
self.format = formats[param]
return f"Changed format to {self.format}"
return "Invalid parameter"
# 使用示例
radio = RadioRecorder("FM 98.6", "MP3")
print(radio.listen())
print(radio.record("Music"))
print(radio.change("AM 1070"))
print(radio.change(2))
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.
Listening to FM 98.6
Recording 'Music' in MP3
Changed channel to AM 1070
Changed format to FLAC
此外在Python的多重继承中,方法解析顺序(Method Resolution Order, MRO)是一个重要的概念,它决定了当子类调用一个方法时,Python解释器会按照什么顺序在父类中查找这个方法。MRO的规则:
- 深度优先,从左到右:Python会先检查第一个父类及其祖先,然后再检查第二个父类及其祖先,以此类推。
- C3线性化算法:Python2.3之后使用C3线性化算法计算MRO,确保每个类只被访问一次,解决了经典类中的 “菱形继承” 问题。
以上述代码为例,RadioRecorder继承自Listenable和Recordable。Listenable排在Recordable前面,这意味着当两个父类有同名方法时,Listenable的方法会被优先调用。因此这里的MRO顺序是:RadioRecorder -> Listenable -> Recordable -> object。这意味着:
- 当调用radio.change()时,Python 会先在RadioRecorder中查找change方法。
- 如果没找到,会在Listenable中查找。
- 如果还没找到,会在Recordable中查找。
- 最后查找object类(所有类的基类)。
可以使用__mro__属性或mro()方法查看类的MRO顺序:
print(RadioRecorder.__mro__)
1.
(<class '__main__.RadioRecorder'>, <class '__main__.Listenable'>, <class '__main__.Recordable'>, <class 'abc.ABC'>, <class 'object'>)
2 参考
- Python Abstract Classes
- Python MRO方法解析顺序详解