ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

python 抽象类的使用详解

python 抽象类的使用详解 前言抽象类abstract class要解决的问题很具体我想定义一个概念上的基类它规定了子类必须实现哪些方法自己却不提供完整实现也不应该被直接实例化。Python 里靠自己写一个方法体里raise NotImplementedError也能模拟出效果但那只是软约束——类照样能被实例化只有真正调用到那个方法时才会报错。要得到没实现完就不许实例化这种硬约束得用标准库abc模块里的抽象基类Abstract Base ClassABC。围绕抽象类有两个常见误解需要先澄清第一以为 ABC 的主要用途是跑起来时做类型检查。其实 ABC 最大的价值在接口约定——它在类被实例化的那一刻就拦住实现不全的子类把错误从运行时推迟到更早、更容易定位的位置。第二以为用了register()注册的类就跟继承一样了。实际上虚拟子类只是通过issubclass()/isinstance()检查它不出现在 MRO 里ABC 定义的方法也不会被它继承到。本文讲abc.ABC、abstractmethod的用法与边界并给出可直接运行的示例。一、abc.ABC 与 abstractmethodabc模块提供辅助类ABC它本身的元类metaclass是ABCMeta。从ABC派生就能以最普通的继承写法创建抽象基类而不必手动折腾metaclass。abstractmethod是声明抽象方法的装饰器。文档说明使用它要求所在类的元类是ABCMeta或它的子类而带有ABCMeta派生元类的类只要还有未覆盖的抽象方法或抽象属性就不能被实例化。# 适用于 Python 3.8from abc import ABC, abstractmethodclass Storage(ABC):abstractmethoddef save(self, key, value):...abstractmethoddef load(self, key):...class MemoryStorage(Storage):def __init__(self):self._data {}def save(self, key, value):self._data[key] valuedef load(self, key):return self._data.get(key)s MemoryStorage()s.save(a, 1)print(s.load(a)) # 1如果此时直接写Storage()会立刻抛TypeError: Cant instantiate abstract class Storage with abstract methods load, save。如果子类只实现了save没有实现load那么MemoryStorage()同样会抛TypeError错误信息里会点名还没实现的方法。注意上面save/load的方法体是...省略号。抽象方法可以带实现子类可以用super()调到它——文档明确说抽象方法可以通过任何常规的 super 调用机制被调用。所以下面这种写法是成立的# 适用于 Python 3.8from abc import ABC, abstractmethodclass Renderer(ABC):abstractmethoddef 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.8from abc import ABC, abstractmethodclass Config(ABC):propertyabstractmethoddef path(self):...classmethodabstractmethoddef from_env(cls):...class FileConfig(Config):def __init__(self, path):self._path pathpropertydef path(self):return self._pathclassmethoddef from_env(cls):return cls(default.conf)顺序写反abstractmethod在最外层不会达到预期效果因为外层的装饰器会先把它包装掉。另外早期常用的abc.abstractproperty从 Python 3.3 起就已弃用property与abstractmethod组合已经能正确表达抽象属性那个装饰器是多余的。新代码不要再用。三、虚拟子类与 __subclasshook__有些类已经存在甚至是内置类没法改它的继承关系但又希望isinstance认它。这时用register()ABC 的元类提供register(subclass)把参数注册成虚拟子类。# 适用于 Python 3.8from abc import ABCclass Sized(ABC):passSized.register(tuple)print(issubclass(tuple, Sized)) # Trueprint(isinstance((), Sized)) # True有个重要限制虚拟子类不会出现在 MRO 里ABC 定义的方法也不能被它通过super()调到。所以register()只影响类型检查结果不带来任何实现复用。__subclasshook__则更进一步它是一个必须在 ABC 里定义成classmethod的钩子让issubclass()的判定可以自定义从而不必给每个类都手动register()。它应返回True、False或NotImplemented返回NotImplemented时检查会回落到常规机制。标准库collections.abc.Iterable就是这么判定有没有__iter__的。下表梳理abc模块的几个常用入口名称类型作用ABC辅助基类元类为ABCMeta直接继承即可定义 ABCABCMeta元类需要自定义元类时使用abstractmethod装饰器声明抽象方法须放在装饰器最内层register(subclass)方法注册虚拟子类不影响 MRO__subclasshook__类方法自定义issubclass()判定update_abstractmethods(cls)函数3.10重算抽象状态通常用于类装饰器四、标准库里现成的 ABC不是所有场景都要自己定义 ABC。标准库已经准备了一批常用接口放在collections.abcIterable、Sequence、MutableSequence、Mapping等、numbers、io和importlib.abc里。# 适用于 Python 3.8from collections.abc import Sequenceclass 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.8class Base:def run(self):raise NotImplementedErrorBase()能正常创建错误要等到调用run()才暴露。✅# 适用于 Python 3.8from abc import ABC, abstractmethodclass Base(ABC):abstractmethoddef run(self):...Base()当场就抛TypeError。2. 子类只实现了部分抽象方法就实例化❌ 以为实现了主要的那个方法就行结果一实例化就TypeError报错还点名漏了哪个方法。✅ 要么把全部抽象方法补上要么给子类也加上abstractmethod让它继续当抽象类。3. 装饰器顺序写反❌# 适用于 Python 3.8class C(ABC):abstractmethodpropertydef x(self):...✅# 适用于 Python 3.8class C(ABC):propertyabstractmethoddef x(self):...4. 还在用abstractproperty/abstractclassmethod❌ 从abc导入abstractproperty当新代码用它们是历史遗留从 3.3 起已弃用。✅ 用propertyabstractmethod组合或classmethodabstractmethod。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用起来就没什么意外。
返回列表