
047、抽象基类与 ABC接口定义、子类强制与 collections.abc 源码阅读从一次诡异的 AttributeError 说起去年接手一个遗留项目代码里有个BaseProcessor类注释写着“所有处理器必须实现 process 方法”。结果上线第三天某个新同事写的CustomProcessor忘了实现 process直接调父类的空方法返回了 None导致下游数据管道全部断裂。更坑的是这个错误直到生产环境才暴露——因为 Python 压根不会在实例化时检查你有没有实现某个方法。那次事故之后我养成了一个习惯凡是定义接口的地方必须用抽象基类ABC把契约写死。今天这篇笔记就聊聊 Python 的抽象基类怎么用、为什么用、以及 collections.abc 里那些你天天用但没注意过的抽象基类。抽象基类不是接口但比接口更灵活Java 程序员转 Python 经常会问“Python 有没有 interface 关键字”答案是没有。但 Python 的抽象基类Abstract Base Class简称 ABC可以做到接口该做的事甚至更多。先看一个最简单的例子fromabcimportABC,abstractmethodclassBaseProcessor(ABC):abstractmethoddefprocess(self,data):处理数据子类必须实现pass# 这里踩过坑pass 只是占位不写 abstractmethod 装饰器就没用classMyProcessor(BaseProcessor):defprocess(self,data):returndata*2# p BaseProcessor() # TypeError: Cant instantiate abstract class关键点加了abstractmethod的方法子类如果不实现实例化时直接报错。这比运行时才报 AttributeError 强一百倍。但注意一个坑——__init__方法也可以被标记为抽象方法。别这样写除非你真的需要强制子类实现特定的初始化逻辑。我见过有人把__init__写成抽象方法结果子类构造函数签名不一致调试了一下午。抽象属性、抽象类方法、抽象静态方法抽象基类不止能约束普通方法。如果你需要强制子类定义某个属性用property配合abstractmethodclassConfigurable(ABC):propertyabstractmethoddefconfig(self):返回配置字典passclassMyConfig(Configurable):propertydefconfig(self):return{timeout:30}这里有个细节abstractmethod必须放在最内层装饰器也就是紧挨着def。顺序错了会报错我踩过这个坑。类方法和静态方法同理classFactory(ABC):classmethodabstractmethoddefcreate(cls,*args):passstaticmethodabstractmethoddefvalidate(data):pass虚拟子类Python 的鸭子类型“官方认证”抽象基类最骚的操作是“虚拟子类”——通过register方法让一个类“假装”是某个抽象基类的子类而不需要实际继承它。fromabcimportABCclassIterableMixin(ABC):abstractmethoddef__iter__(self):passclassMyList:# 没有继承 IterableMixindef__iter__(self):returniter([1,2,3])IterableMixin.register(MyList)print(isinstance(MyList(),IterableMixin))# True这有什么用场景一你无法修改第三方库的类但希望它通过你的类型检查。场景二你想让某个内置类型比如 list、dict符合你的接口定义而不需要包装一层。但注意register只是注册了继承关系不会检查子类是否真的实现了抽象方法。如果你注册了一个没有实现__iter__的类isinstance依然返回 True但运行时调用__iter__会炸。所以虚拟子类适合用在“我知道这个类肯定有这个方法”的场景比如注册list到某个序列抽象基类。collections.abc 源码阅读那些你天天用的抽象基类collections.abc模块是 Python 标准库中最值得读的源码之一。它定义了容器类型的抽象基类比如Iterable、Sequence、MutableMapping等。打开源码Python 3.10 在_collections_abc.py你会发现这些抽象基类都是用ABCMeta元类实现的。ABCMeta是abc模块的核心它接管了类的创建过程在实例化时检查抽象方法是否全部实现。看一个我经常用的SequenceclassSequence(Reversible,Collection):abstractmethoddef__getitem__(self,index):passabstractmethoddef__len__(self):passdef__contains__(self,value):forvinself:ifvvalue:returnTruereturnFalsedef__iter__(self):i0try:whileTrue:yieldself[i]i1exceptIndexError:passdef__reversed__(self):foriinrange(len(self)-1,-1,-1):yieldself[i]defindex(self,value,start0,stopNone):# ... 省略实现passdefcount(self,value):# ... 省略实现pass注意看Sequence只要求子类实现__getitem__和__len__然后自动提供了__contains__、__iter__、__reversed__、index、count这些方法的默认实现。这就是抽象基类的威力——你只需要实现最核心的两个方法就能免费获得一整套序列行为。同理MutableSequence在Sequence基础上增加了__setitem__、__delitem__、insert三个抽象方法然后自动提供append、clear、reverse、extend、pop、remove、__iadd__等方法的实现。这就是为什么你自定义一个类只要实现了__getitem__和__len__就能用for循环遍历它——因为collections.abc.Sequence的__iter__方法已经帮你写好了。自定义抽象基类的最佳实践写抽象基类时有几个原则我踩过坑后总结出来的原则一抽象方法越少越好。像Sequence那样只定义最核心的方法其他用默认实现。这样子类的负担最小也更容易满足里氏替换原则。原则二用abstractmethod装饰器不要用raise NotImplementedError。后者是运行时才报错前者在实例化时就拦截。我见过有人写classBase:defprocess(self):raiseNotImplementedError这种写法等于没写——子类不重写 process只有调用时才报错而且如果父类 process 被其他方法间接调用错误栈会让人摸不着头脑。原则三抽象基类里可以写具体方法。抽象基类不全是抽象方法它完全可以包含具体实现。比如Sequence的__contains__就是具体方法。这比 Java 的 interface 灵活得多。原则四慎用__init_subclass__做强制检查。有人喜欢在抽象基类的__init_subclass__里检查子类是否实现了某些方法但这样会破坏 Python 的动态特性而且容易和ABCMeta的检查冲突。用abstractmethod就够了。一个实战案例插件系统的接口定义假设你要写一个插件系统每个插件必须实现run和stop方法可选实现setup和teardown。用抽象基类可以这样写fromabcimportABC,abstractmethodimportlogging loggerlogging.getLogger(__name__)classPluginBase(ABC):abstractmethoddefrun(self,context):执行插件核心逻辑passabstractmethoddefstop(self):停止插件passdefsetup(self,config):可选初始化配置默认什么都不做logger.info(f{self.__class__.__name__}setup with config:{config})defteardown(self):可选清理资源默认什么都不做logger.info(f{self.__class__.__name__}teardown)classLogPlugin(PluginBase):defrun(self,context):print(fLogging:{context})defstop(self):print(Log plugin stopped)# 插件管理器classPluginManager:def__init__(self):self._plugins[]defregister(self,plugin):ifnotisinstance(plugin,PluginBase):raiseTypeError(f{plugin}is not a PluginBase instance)self._plugins.append(plugin)defstart_all(self,context):forplugininself._plugins:plugin.setup(context.get(config,{}))plugin.run(context)defstop_all(self):forplugininreversed(self._plugins):plugin.stop()plugin.teardown()这里isinstance(plugin, PluginBase)检查确保了只有实现了run和stop的类才能被注册。如果某个插件忘了实现run在PluginBase()实例化时就会报错而不是等到start_all调用时才炸。关于 collections.abc 源码的几点感悟读collections.abc源码时我最大的收获是理解了 Python 的“协议”是如何通过抽象基类实现的。比如Hashable抽象基类它只要求实现__hash__方法然后通过ABCMeta的__instancecheck__钩子让所有定义了__hash__的类自动成为Hashable的虚拟子类。# 源码中的简化逻辑classHashable(metaclassABCMeta):__slots__()abstractmethoddef__hash__(self):return0classmethoddef__subclasshook__(cls,C):ifclsisHashable:return_check_methods(C,__hash__)returnNotImplemented__subclasshook__是抽象基类的另一个黑科技它允许你自定义issubclass和isinstance的行为。上面这段代码的意思是任何定义了__hash__方法的类都被认为是Hashable的子类即使它没有显式继承。这就是 Python 鸭子类型的官方认证机制。个人经验性建议别滥用抽象基类。如果你的接口只有一两个方法而且只在项目内部使用用鸭子类型就够了。抽象基类适合在以下场景多人协作的公共接口、插件系统、需要做类型检查的框架代码。优先用collections.abc里的现成抽象基类。比如你想定义一个“可迭代的容器”直接继承collections.abc.Iterable或collections.abc.Collection比自己从头写ABC更省事而且别人一看就懂。abstractmethod和property组合时注意装饰器顺序。property在外层abstractmethod在内层这是 Python 3.3 的要求。虚拟子类register要谨慎使用。它绕过了抽象方法的检查适合用在“我知道这个类肯定有这些方法”的场景比如把第三方库的类注册到你的抽象基类。如果用在不可控的代码上等于给自己埋雷。调试时用__abstractmethods__属性。如果一个类实例化时报错“Can’t instantiate abstract class”可以打印ClassName.__abstractmethods__它会返回一个 frozenset里面是所有未实现的抽象方法名。这个技巧帮我省了不少时间。抽象基类是 Python 在动态类型和静态契约之间找到的一个平衡点。它不像 Java 接口那样强制也不像鸭子类型那样完全放任。用好它你的代码会少很多“运行时才发现接口没实现”的尴尬。