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

Muh*_*uri 192 python abstract-class abc

因为我习惯于在Python中使用旧的鸭子打字方式,所以我无法理解对ABC(抽象基类)的需求.的帮助下是如何使用它们好.

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

有人能解释一下我的理由吗?

Ben*_*son 209

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

抽象方法很简洁,但在我看来,它们并没有真正填充任何未被鸭子打字所涵盖的用例.抽象基类真正的强大之处在于他们允许您自定义的行为方式isinstanceissubclass.(__subclasshook__基本上是Python __instancecheck____subclasscheck__钩子之上的友好API .)调整内置构造以处理自定义类型是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
Run Code Online (Sandbox Code Playgroud)

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

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

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True
Run Code Online (Sandbox Code Playgroud)

换句话说,如果您实现了正确的接口,那么您就是一个子类!ABCs提供了一种在Python中定义接口的正式方法,同时坚持鸭子打字的精神.此外,这是以尊重开放原则的方式运作的.

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

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

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

*没有讨论Java是否是一个"传统的"面向对象系统......


附录:即使一个抽象基类可以重写的行为isinstance,并issubclass,它仍然没有进入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
Run Code Online (Sandbox Code Playgroud)

不幸的是,这些"只是不做那个"陷阱(其中Python相对较少!):避免使用a __subclasshook__和非抽象方法定义ABCs .此外,您应该使您的定义__subclasshook__与ABC定义的抽象方法集合一致.

  • "如果你实现了正确的接口,你就是一个子类"非常感谢你.我不知道Oddthinking是否错过了它,但我确实做到了.FWIW,`isinstance(x,collections.Iterable)`对我来说更清楚,我知道Python. (16认同)

Odd*_*ing 144

精简版

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

长版

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

合同有不同的级别.

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

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

但合同中也有更高层次的语义承诺.

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

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

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

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

  • `collections.Container`是一个退化的情况,只包含`\ _ _ _ contains\_\_ _`,并且只表示预定义的约定.我同意,使用ABC本身并没有增加太多价值.我怀疑它被添加到允许(例如)`Set`继承它.当你进入"Set"时,突然属于ABC有相当大的语义.一个项目不能属于该集合两次.方法的存在无法检测到这一点. (15认同)
  • 我认为你有一点意见,但我无法关注你.那么,在契约方面,实现`__contains__`的类和从`collections.Container`继承的类之间的区别是什么?在您的示例中,在Python中始终存在对`__str__`的共同理解.实现`__str__`与从某些ABC继承然后实现`__str__`做出相同的承诺.在这两种情况下,你都可以违反合同; 没有可证明的语义,例如我们在静态类型中使用的语义. (11认同)
  • 是的,我认为`Set`是一个比`print()`更好的例子.我试图找到一个方法名称,其含义不明确,并且不能单独用名称来表达,所以你不能确定它只是通过它的名称和Python手册来做正确的事情. (3认同)
  • 有没有机会用`Set`作为例子而不是'print`来重写答案?"Set"很有意义,@ Oddthinking. (3认同)
  • 我认为这篇文章解释得很好:https://dbader.org/blog/abstract-base-classes-in-python (3认同)
  • 我不明白 ABC 如何执行语义契约。派生类不是可以自由地实现它想要的任何内容并打破任何记录的语义契约吗? (2认同)

cer*_*ros 99

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

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
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"
Run Code Online (Sandbox Code Playgroud)

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

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

  • 最好,最清晰的插图.即时理解 (10认同)
  • 示例和链接都非常有用.谢谢! (5认同)
  • 来自C#背景,这就是使用抽象类的*原因。您在提供功能,但声明该功能需要进一步实施。其他答案似乎错过了这一点。 (2认同)

Ign*_*ams 17

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


sim*_*sim 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()
Run Code Online (Sandbox Code Playgroud)

'methodone()被称为'

c.methodtwo()
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

'methodone()被称为'

  • 我认为这个答案更好,因为它还向您展示了如果您不使用“抽象方法”会发生什么。 (5认同)
  • 谢谢。这不是与上面 cerberos 答案中提出的观点相同吗? (2认同)