268

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

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

谁能给我解释一下原因吗?

4

6 回答 6

289

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

抽象方法很简洁,但在我看来,它们并没有真正填补鸭子类型尚未涵盖的任何用例。抽象基类的真正威力在于它们允许您自定义 和 的行为的isinstance方式issubclass。(基本上是 Python__subclasshook__hooks之上的更友好的 API 。)调整内置结构以处理自定义类型是 Python 哲学的重要组成部分。__instancecheck____subclasscheck__

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*)——我们有你的类、你的对象、你的方法——但是当你触及表面时,你会发现一些更丰富和更丰富的东西。更灵活。同样,Java 开发人员可以识别 Python 的抽象基类的概念,但实际上它们的目的是非常不同的。

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

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

*没有进入关于Java是否是“传统”OO系统的辩论......


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

于 2013-10-11T22:20:29.890 回答
190

精简版

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

长版

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

合同有不同的层次。

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

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

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

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

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

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

Python 的鸭子类型在灵活性方面比静态类型有很多优势,但它并不能解决所有问题。ABC 提供了介于 Python 的自由形式和静态类型语言的束缚和纪律之间的中间解决方案。

于 2010-08-26T03:59:12.207 回答
129

ABCs 的一个方便的特性是,如果你没有实现所有必要的方法(和属性),你会在实例化时得到一个错误,而不是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

于 2015-05-19T14:46:33.100 回答
21

它将使得确定一个对象是否支持给定协议,而不必检查协议中所有方法的存在,或者不会由于不支持而在“敌人”领域深处触发异常。

于 2010-08-25T22:58:14.293 回答
11

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

未实现错误

使用抽象方法

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:无法使用抽象方法方法二实例化抽象类 Son。

由于在子类中没有调用方法二,我们得到了错误。正确的实现如下

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() 被调用'

于 2018-07-19T06:48:47.503 回答
1

ABC 能够创建设计模式和框架。请看 Brandon Rhodes 的这个 pycon 演讲:

Python 设计模式 1

Python 本身的协议(更不用说迭代器、装饰器和插槽(它们本身实现 FlyWeight 模式))都是可能的,因为 ABC 的(尽管在 CPython 中实现为虚拟方法/类)。

鸭子类型确实使 python 中的一些模式变得微不足道,Brandon 提到了这一点,但许多其他模式继续出现并在 Python 中有用,例如适配器。

简而言之,ABC 使您能够编写可扩展和可重用的代码。根据 GoF:

  1. 对接口编程,而不是实现(继承破坏封装;对接口编程促进松散耦合/控制反转/“好莱坞原则:不要打电话给我们,我们会打电话给你”)

  2. 优先考虑对象组合而不是类继承(委托工作)

  3. 封装变化的概念(开闭原则使类对扩展开放,对修改关闭)

此外,随着 Python 的静态类型检查器(例如mypy)的出现,ABC 可以用作类型,而不是Union[...]函数接受作为参数或返回的每个对象。想象一下,每次您的代码库支持新对象时,都必须更新类型,而不是实现?这会很快变得无法维护(无法扩展)。

于 2021-11-06T23:33:20.580 回答