为什么在Python中使用抽象基类?

习惯于Python中老式的鸭子打字方式,我不了解ABC(抽象基类)的必要性。 帮助是很好的如何使用它们。

我尝试阅读PEP中的理论基础,但是它已经超出了我的头脑。 如果我正在寻找一个可变序列容器,我会检查__setitem__ ,或者更可能尝试使用它( EAFP )。 我还没有遇到过使用ABC的数字模块的实际用途,但是这是我最需要理解的。

任何人都可以向我解释理由吗?

短版

ABC提供客户和实施类之间更高水平的语义契约。

长版

class级和来电者之间有合同。 class级承诺做某些事情,并具有一定的性质。

合同有不同的等级。

在很低的水平上,合同可能包括方法的名称或参数的数量。

在静态types语言中,该合约实际上将由编译器执行。 在Python中,您可以使用EAFP或内省来确认未知对象是否符合此预期合同。

但合同中也有较高层次的语义承诺。

例如,如果有一个__str__()方法,则需要返回该对象的string表示forms。 它可以删除对象的所有内容,提交事务并从打印机中吐出一个空白页面……但是对Python应该怎么做有一个共同的理解,在Python手册中有描述。

这是一个特例,手册中描述了语义契约。 print()方法应该做什么? 是否应该将对象写到打印机或屏幕上的某一行? 这取决于 – 你需要阅读的意见,以了解这里的全部合同。 一个简单地检查print()方法是否存在的客户端代码已经确认了合同的一部分 – 即可以进行方法调用,但不是在调用的更高级语义上达成一致。

定义抽象基类(ABC)是在类实现者和调用者之间产生契约的一种方式。 这不仅仅是方法名称列表,而且是对这些方法应该做什么的共同理解。 如果您inheritance了这个ABC,您将承诺遵守注释中描述的所有规则,包括print()方法的语义。

Python的鸭式input在静态input方面有很多优点,但并不能解决所有的问题。 ABCs提供了Python的自由forms和静态types语言的约束与规范之间的中间解决scheme。

@ Oddthinking的答案没有错,但是我认为它错过了Python在鸭子打字的世界中具有ABC的真实实际的原因。

抽象方法是整齐的,但在我看来,它们并没有真正填补鸭式打字所没有涉及的任何用例。 抽象基类的真正威力在于它允许你定制isinstanceissubclass的行为 。 ( __subclasshook__基本上是Python的__instancecheck____subclasscheck__钩子之上的一个友好的API。)调整内置结构以处理自定义types是Python哲学的重要组成部分。

Python的源代码是示例性的。 这里是collections.Container如何在标准库中定义(写作时):

 class Container(metaclass=ABCMeta): __slots__ = () @abstractmethod def __contains__(self, x): return False @classmethod def __subclasshook__(cls, C): if cls is Container: if any("__contains__" in B.__dict__ for B in C.__mro__): return True return NotImplemented 

__subclasshook__这个定义说任何具有__contains__属性的类都被认为是Container的一个子类,即使它没有直接__subclasshook__它的子类。 所以我可以写这个:

 class ContainAllTheThings(object): def __contains__(self, item): return True >>> issubclass(ContainAllTheThings, collections.Container) True >>> isinstance(ContainAllTheThings(), collections.Container) True 

换句话说, 如果你实现了正确的界面,你就是一个子类! ABCs提供了一种正式的方式来定义Python中的接口,同时坚持鸭式打字的精神。 此外,这也是一种尊重开放原则的方式 。

Python的对象模型看起来表面上类似于一个更传统的面向对象系统(我的意思是Java *) – 我们得到了类,你的对象,你的方法 – 但是当你抓表面时,你会发现更丰富的东西更灵活。 同样,Java开发人员也可以认识到Python的抽象基类的概念,但实际上它们是为了一个非常不同的目的。

我有时会发现自己正在编写可以作用于单个项目或多个项目集合的多态函数,而且我发现isinstance(x, collections.Iterable)hasattr(x, '__iter__')或同等try...except更具可读性try...except块。 (如果你不知道Python,那三个中的哪一个会使代码的意图清晰?)

我发现我很less需要写自己的ABC–我更喜欢依靠鸭子打字 – 而且通常我会通过重构来发现需要的东西。 如果我看到一个多态函数做了很多属性检查,或者很多函数做了相同的属性检查,那味道就表明存在一个等待提取的ABC。

*没有深入讨论Java是否是一个“传统”的面向对象系统。


附录 :尽pipe抽象基类可以覆盖isinstanceissubclass的行为,但它仍然不会进入虚拟子类的MRO 。 这对于客户端来说是一个潜在的问题:并不是每个实例对象isinstance(x, MyABC) == True都具有在MyABC定义的方法。

 class MyABC(metaclass=abc.ABCMeta): def abc_method(self): pass @classmethod def __subclasshook__(cls, C): return True class C(object): pass # typical client code c = C() if isinstance(c, MyABC): # will be true c.abc_method() # raises AttributeError 

不幸的是,其中一个“只是不这样做”的陷阱(其中Python相对较less!):避免用__subclasshook__和非抽象方法来定义ABCs。 而且,您应该使__subclasshook__的定义与您定义的ABC抽象方法集一致。

ABCs的一个方便function是,如果你没有实现所有必要的方法(和属性),你会在实例化的时候得到一个错误,而不是一个AttributeError ,当你实际尝试使用缺less的方法时可能会晚得多。

 from abc import ABCMeta, abstractmethod class Base(object): __metaclass__ = ABCMeta @abstractmethod def foo(self): pass @abstractmethod def bar(self): pass class Concrete(Base): def foo(self): pass # We forget to declare `bar` c = Concrete() # TypeError: "Can't instantiate abstract class Concrete with abstract methods bar" 

来自https://dbader.org/blog/abstract-base-classes-in-python的示例;

它将确定一个对象是否支持给定的协议,而不必检查协议中是否存在所有的方法,或者由于非支持更容易触发“敌方”领域深处的exception。