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

时间:2010-08-25 22:43:02

标签: python abstract-class abc

因为我习惯于在Python中使用旧的鸭子打字方式,所以我无法理解ABC(抽象基类)的必要性。 help很好用。如何使用它们。

我试图阅读PEP中的理由,但它超越了我的脑海。如果我正在寻找一个可变序列容器,我会检查__setitem__,或者更有可能尝试使用它(EAFP)。我没有遇到numbers模块的实际用途,它确实使用了ABC,但这是我最接近理解的模块。

有人可以向我解释理由吗?

5 个答案:

答案 0 :(得分:196)

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

抽象方法很简洁,但在我看来,它们并没有真正填充任何未被鸭子打字所涵盖的用例。抽象基类的实力在于the way they allow you to customise the behaviour of isinstance and issubclass。 (__subclasshook__基本上是Python __instancecheck__ and __subclasscheck__挂钩之上的友好API。)调整内置构造以处理自定义类型是Python理念的一部分。

Python的源代码是典型的。 Here是标准库(撰写本文时)中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的子类,即使它没有直接对它进行子类化。所以我可以这样写:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

换句话说,如果你实现了正确的接口,你就是一个子类! ABCs提供了一种在Python中定义接口的正式方法,同时坚持鸭子类型的精神。此外,它的工作方式是尊重Open-Closed Principle

Python的对象模型看起来表面上类似于更“传统”的OO系统(我的意思是Java *) - 我们得到了你的类,你的对象,你的方法 - 但是当你划伤表面时你会发现一些东西更丰富,更灵活。同样,Python的抽象基类概念对于Java开发人员来说可能是可识别的,但在实践中它们的目的非常不同。

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

那就是说,我发现我很少需要编写自己的ABC,而且我通常会发现需要通过重构。如果我看到一个多态函数进行了大量的属性检查,或者许多函数进行了相同的属性检查,那气味就表明存在等待提取的ABC。

*没有讨论Java是否是一个“传统的”OO系统...


附录:即使抽象基类可以覆盖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相对较少!):避免使用__subclasshook__和非抽象方法定义ABCs。此外,您应该使__subclasshook__的定义与ABC定义的抽象方法集合一致。

答案 1 :(得分:138)

短版

ABCs在客户端和已实现的类之间提供更高级别的语义契约。

长版

课程与其来电者之间存在合同。该班承诺做某些事情并具有某些特性。

合同有不同的级别。

在非常低的级别,合同可能包括方法的名称或参数的数量。

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

但合同中也有更高级别的语义承诺。

例如,如果存在__str__()方法,则应返回该对象的字符串表示形式。它可以删除对象的所有内容,提交事务并从打印机中吐出一个空白页......但是对于它应该做什么有共同的理解,如Python手册中所述。 / p>

这是一个特殊情况,其中语义合同在手册中描述。 print()方法应该做什么?它应该将对象写入打印机还是屏幕线或其他东西?这取决于 - 您需要阅读评论以了解完整的合同。一个简单地检查print()方法存在的客户端代码已经确认了合同的一部分 - 可以进行方法调用,但不是就调用的更高级别语义达成一致。

定义抽象基类(ABC)是一种在类实现者和调用者之间生成契约的方法。它不仅仅是方法名称列表,而是对这些方法应该做什么的共同理解。如果您继承此ABC,则承诺遵循评论中描述的所有规则,包括print()方法的语义。

Python的鸭子打字在灵活性方面比静态打字有很多优点,但它并没有解决所有问题。 ABCs提供了一种自由形式的Python与静态类型语言的束缚和规范之间的中间解决方案。

答案 2 :(得分:87)

ABCs的一个方便功能是,如果你没有实现所有必要的方法(和属性),你实例化时会出现错误,而不是AttributeError,可能更晚,当你真正尝试使用缺少方法。

VBA-Web - Installer.xlsm

来自https://dbader.org/blog/abstract-base-classes-in-python

的示例

编辑:包含python3语法,感谢@PandasRocks

答案 3 :(得分:15)

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

答案 4 :(得分:5)

抽象方法确保您在父类中调用的任何方法都必须出现在子类中。以下是noraml调用和使用摘要的方式。 用python3编写的程序

正常的通话方式

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()
  
    

'methodone()被称为'

  
c.methodtwo()
  
    

NotImplementedError

  

使用抽象方法

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()
  
    

TypeError:无法使用抽象方法methodtwo实例化抽象类Son。

  

由于在子类中未调用methodtwo,因此出现错误。正确的实现如下

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()
  
    

'methodone()被称为'