前言
"抽象类"(abstract class)要解决的问题很具体:我想定义一个概念上的"基类",它规定了子类必须实现哪些方法,自己却不提供完整实现,也不应该被直接实例化。
Python 里靠自己写一个方法体里raise NotImplementedError也能模拟出效果,但那只是"软约束"——类照样能被实例化,只有真正调用到那个方法时才会报错。要得到"没实现完就不许实例化"这种硬约束,得用标准库abc模块里的抽象基类(Abstract Base Class,ABC)。
围绕抽象类,有两个常见误解需要先澄清:
第一,以为 ABC 的主要用途是"跑起来时做类型检查"。其实 ABC 最大的价值在接口约定——它在类被实例化的那一刻就拦住"实现不全"的子类,把错误从运行时推迟到更早、更容易定位的位置。
第二,以为用了register()注册的类就跟继承一样了。实际上虚拟子类只是通过issubclass()/isinstance()检查,它不出现在 MRO 里,ABC 定义的方法也不会被它继承到。
本文讲abc.ABC、@abstractmethod的用法与边界,并给出可直接运行的示例。
一、abc.ABC 与 @abstractmethod
abc模块提供辅助类ABC,它本身的元类(metaclass)是ABCMeta。从ABC派生,就能以"最普通"的继承写法创建抽象基类,而不必手动折腾metaclass=。
@abstractmethod是声明抽象方法的装饰器。文档说明:使用它要求所在类的元类是ABCMeta或它的子类;而带有ABCMeta派生元类的类,只要还有未覆盖的抽象方法或抽象属性,就不能被实例化。
# 适用于 Python 3.8+
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key, value):
...
@abstractmethod
def load(self, key):
...
class MemoryStorage(Storage):
def __init__(self):
self._data = {}
def save(self, key, value):
self._data[key] = value
def load(self, key):
return self._data.get(key)
s = MemoryStorage()
s.save("a", 1)
print(s.load("a")) # 1如果此时直接写Storage(),会立刻抛TypeError: Can't instantiate abstract class Storage with abstract methods load, save。如果子类只实现了save,没有实现load,那么MemoryStorage()同样会抛TypeError,错误信息里会点名还没实现的方法。
注意上面save/load的方法体是...(省略号)。抽象方法可以带实现,子类可以用super()调到它——文档明确说"抽象方法可以通过任何常规的 super 调用机制被调用"。所以下面这种写法是成立的:
# 适用于 Python 3.8+
from abc import ABC, abstractmethod
class Renderer(ABC):
@abstractmethod
def render(self, text):
return text.strip() # 提供一份默认实现
class HtmlRenderer(Renderer):
def render(self, text):
return "<p>" + super().render(text) + "</p>"二、抽象属性与装饰器顺序
抽象方法也能和@property、@classmethod、@staticmethod组合。文档给出一条硬规则:@abstractmethod要放在最内层(也就是紧贴def的那一行)。
# 适用于 Python 3.8+
from abc import ABC, abstractmethod
class Config(ABC):
@property
@abstractmethod
def path(self):
...
@classmethod
@abstractmethod
def from_env(cls):
...
class FileConfig(Config):
def __init__(self, path):
self._path = path
@property
def path(self):
return self._path
@classmethod
def from_env(cls):
return cls("default.conf")顺序写反(@abstractmethod在最外层)不会达到预期效果,因为外层的装饰器会先把它包装掉。
另外,早期常用的abc.abstractproperty从 Python 3.3 起就已弃用:@property与@abstractmethod组合已经能正确表达"抽象属性",那个装饰器是多余的。新代码不要再用。
三、虚拟子类与 __subclasshook__
有些类已经存在(甚至是内置类),没法改它的继承关系,但又希望isinstance认它。这时用register():ABC 的元类提供register(subclass),把参数注册成"虚拟子类"。
# 适用于 Python 3.8+
from abc import ABC
class Sized(ABC):
pass
Sized.register(tuple)
print(issubclass(tuple, Sized)) # True
print(isinstance((), Sized)) # True有个重要限制:虚拟子类不会出现在 MRO 里,ABC 定义的方法也不能被它通过super()调到。所以register()只影响类型检查结果,不带来任何实现复用。
__subclasshook__则更进一步:它是一个必须在 ABC 里定义成classmethod的钩子,让issubclass()的判定可以自定义,从而不必给每个类都手动register()。它应返回True、False或NotImplemented;返回NotImplemented时,检查会回落到常规机制。标准库collections.abc.Iterable就是这么判定"有没有__iter__"的。
下表梳理abc模块的几个常用入口:
| 名称 | 类型 | 作用 |
|---|
ABC | 辅助基类 | 元类为ABCMeta,直接继承即可定义 ABC |
ABCMeta | 元类 | 需要自定义元类时使用 |
@abstractmethod | 装饰器 | 声明抽象方法,须放在装饰器最内层 |
register(subclass) | 方法 | 注册虚拟子类,不影响 MRO |
__subclasshook__ | 类方法 | 自定义issubclass()判定 |
update_abstractmethods(cls) | 函数(3.10+) | 重算抽象状态,通常用于类装饰器 |
四、标准库里现成的 ABC
不是所有场景都要自己定义 ABC。标准库已经准备了一批常用接口,放在collections.abc(Iterable、Sequence、MutableSequence、Mapping等)、numbers、io和importlib.abc里。
# 适用于 Python 3.8+
from collections.abc import Sequence
class MyList:
def __init__(self, data):
self._data = list(data)
def __getitem__(self, index):
return self._data[index]
def __len__(self):
return len(self._data)
Sequence.register(MyList)
print(isinstance(MyList([1, 2]), Sequence)) # True需要判断"能不能迭代"时,文档提醒过:isinstance(obj, Iterable)能识别注册过的类和带__iter__的类,但识别不了仅用__getitem__迭代的旧式类。
常见坑点
1. 以为raise NotImplementedError就等于抽象方法
❌
# 适用于 Python 3.8+
class Base:
def run(self):
raise NotImplementedErrorBase()能正常创建,错误要等到调用run()才暴露。
✅
# 适用于 Python 3.8+
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def run(self):
...Base()当场就抛TypeError。
2. 子类只实现了部分抽象方法就实例化
❌ 以为"实现了主要的那个方法就行",结果一实例化就TypeError,报错还点名漏了哪个方法。
✅ 要么把全部抽象方法补上,要么给子类也加上@abstractmethod让它继续当抽象类。
3. 装饰器顺序写反
❌
# 适用于 Python 3.8+
class C(ABC):
@abstractmethod
@property
def x(self):
...✅
# 适用于 Python 3.8+
class C(ABC):
@property
@abstractmethod
def x(self):
...4. 还在用abstractproperty/abstractclassmethod
❌ 从abc导入abstractproperty当新代码用,它们是历史遗留,从 3.3 起已弃用。
✅ 用@property+@abstractmethod组合,或@classmethod+@abstractmethod。
5. 以为register()注册的类能继承 ABC 的方法
❌Sized.register(tuple)之后,以为tuple会多出 ABC 里定义的size()方法。
✅ 虚拟子类只影响isinstance/issubclass,不进 MRO,也拿不到方法实现。
6. 想动态改抽象状态却直接改类属性
❌ 创建类之后再往__abstractmethods__上乱动手脚。
✅ 用abc.update_abstractmethods(cls)(需要 Python 3.10 及以上);该函数假定父类已经更新完,且不会自动更新子类。
7. 用ABCMeta手动配元类却和已有元类冲突
❌ 一个类既想用ABCMeta又想用别的元类,直接写两个metaclass=。
✅ 优先继承abc.ABC;确实需要组合元类时,得自己写一个派生自各方的元类。
8. 把 ABC 当性能敏感处的类型检查手段
❌ 在热点循环里对虚拟子类频繁isinstance检查。
✅ ABC 的虚拟子类判定要走额外逻辑,开销高于对普通类做isinstance;热点路径上优先用鸭子类型或hasattr。
总结
| 目标 | 做法 |
|---|
| 定义接口、禁止不完整实例化 | 继承abc.ABC,方法加@abstractmethod |
| 抽象属性 | @property在外、@abstractmethod在内 |
| 复用父类默认实现 | 抽象方法写方法体,子类用super()调用 |
| 让已有类通过类型检查 | ABC.register(类)(3.3+ 返回被注册的类) |
| 自动判定结构 | 重写__subclasshook__(类方法) |
| 用现成接口 | collections.abc、numbers、io里的 ABC |
抽象基类的核心价值是把"接口契约"提前到实例化那一刻强制检查,让缺失实现这类错误尽早暴露。同时要记住它的边界:虚拟子类只改变类型判定,不带来实现复用;抽象方法可以有实现,但装饰器顺序必须对。把这两点拿捏准,abc用起来就没什么意外。