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


213

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

我试图阅读PEP中的基本原理,但它使我感到头疼。如果我正在寻找一个可变序列容器,我会检查__setitem__,或更可能尝试使用它(EAFP)。我还没有真正使用过数字模块,它确实使用了ABC,但这是我必须了解的最接近的数字。

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

Answers:


162

精简版

ABC在客户端和已实现的类之间提供了更高级别的语义协定。

长版

类与其调用者之间存在合同。该类承​​诺做某些事情并具有某些属性。

合同有不同的级别。

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

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

但是合同中还有更高层次的语义承诺。

例如,如果有__str__()方法,则期望返回对象的字符串表示形式。它可以删除对象的所有内容,提交事务并在打印机上吐出空白页...但是,Python手册中对此有一个普遍的了解。

这是一种特殊情况,其中在手册中描述了语义约定。该print()方法应该做什么?它应该将对象写入打印机还是将行写入屏幕,或者其他?这取决于-您需要阅读评论以了解此处的完整合同。一段简单地检查该print()方法是否存在的客户代码已确认了合同的一部分-可以进行方法调用,但未就该调用的较高层语义达成协议。

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

与静态类型相比,Python的鸭子类型在灵活性方面具有许多优势,但是并不能解决所有问题。ABC在Python的自由形式和静态类型的语言的约束与约束之间提供了一种中间解决方案。


12
我认为您在这里有一个要点,但我无法跟随您。那么,就契约而言,实现__contains__的类与继承自的类之间有什么区别collections.Container?在您的示例中,在Python中始终对达成共识__str__。实现__str__与从一些ABC继承然后实现的承诺相同__str__。在两种情况下,您都可以违反合同;没有可证明的语义,例如我们在静态类型中所具有的语义。
穆罕默德·阿尔卡鲁里

15
collections.Container是一个简并的情况,仅包括\_\_contains\_\_,仅表示预定义的约定。我同意,使用ABC本身并不会增加太多价值。我怀疑它是为了允许(例如)Set继承而添加的。在您到达时Set,突然属于ABC的语义就变得相当多了。一个项目不能两次属于该集合。通过方法的存在无法检测到。
2010年

3
是的,我认为Set是比更好的例子print()。我试图找到一个含义不明确的方法名称,并且不能仅仅被该名称所困扰,因此您无法确定仅通过其名称和Python手册就可以正确地执行该方法。
2010年

3
是否有可能以Set代替示例的方式重写答案printSet很有意义,@ Oddthinking。
Ehtesh Choudhury 2014年

1
我认为这篇文章讲得很好:dbader.org/blog/abstract-base-classes-in-python
szabgab 2015年

228

@Oddthinking的答案是正确的,但我认为它没有想到在鸭蛋式的世界中Python具有ABC 的真实实际原因。

抽象方法很简洁,但我认为它们并没有真正填补鸭子类型尚未涵盖的任何用例。抽象基类的真正力量在于它们允许您自定义isinstanceand 行为的方式issubclass。(__subclasshook__基本上,它是基于Python __instancecheck____subclasscheck__ hook 的更友好的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

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

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

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

换句话说,如果实现正确的接口,那么您就是一个子类!ABC提供了一种正式的方式来定义Python中的接口,同时忠实于鸭子式输入的精神。此外,这以尊重开放式原则的方式工作

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

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

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

*无需争论Java是否是“传统的” OO系统...


附录:即使抽象基类可以覆盖行为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

不幸的是,这些“只是不这样做”陷阱(Python相对来说很少!)陷阱:避免同时使用a __subclasshook__和非抽象方法来定义ABC 。此外,您应该使定义__subclasshook__与ABC定义的一组抽象方法一致。


21
“如果实现正确的接口,则说明您是子类”非常感谢。我不知道Oddthinking是否错过了,但我确实做到了。FWIW,isinstance(x, collections.Iterable)对我来说更清楚,而且我确实知道Python。
穆罕默德·阿尔卡鲁里

优秀的职位。谢谢。我认为,补充协议,“只是不这样做”的陷阱,有点像做普通的子类继承,但随后具有C子类中删除(或搞砸了无法修复)在abc_method()从继承MyABC。主要区别在于,搞砸继承契约的是超类,而不是子类。
Michael Scott Cuthbert 2015年

您不必Container.register(ContainAllTheThings)为了给定的示例而工作吗?
BoZenKhaa '16

1
@BoZenKhaa答案中的代码有效!试试吧!的含义__subclasshook__是“为了满足isinstanceissubclass检查的目的,满足此谓词的任何类都将被视为子类,而不管它是否已在ABC中注册,并且无论它是否是直接子类 ”。就像我在答案中所说的那样,如果实现正确的接口,那么您就是一个子类!
本杰明·霍奇森

1
您应将格式从斜体更改为粗体。“如果实现正确的接口,那么您就是一个子类!” 非常简洁的解释。谢谢!
马蒂

108

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"

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

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


5
该示例和链接都非常有帮助。谢谢!
nalyd88

如果Base是在单独的文件中定义的,则必须从Base继承为Base.Base或将导入行更改为“ from Base import Base”
Ryan Tennill

请注意,在Python3中,语法略有不同。看到这个答案:stackoverflow.com/questions/28688784/…–
PandasRocks

7
来自C#背景,这就是使用抽象类原因。您在提供功能,但声明该功能需要进一步实施。其他答案似乎错过了这一点。
Josh Noe


6

抽象方法确保您在父类中调用的任何方法都必须出现在子类中。以下是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()”


1
谢谢。上面在cerberos中回答的不是同一点吗?
Muhammad Alkarouri '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.