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

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

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

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

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

Because I am used to the old ways of duck typing in Python, I fail to understand the need for ABC (abstract base classes). The help is good on how to use them.

I tried to read the rationale in the PEP, but it went over my head. If I was looking for a mutable sequence container, I would check for __setitem__, or more likely try to use it (EAFP). I haven’t come across a real life use for the numbers module, which does use ABCs, but that is the closest I have to understanding.

Can anyone explain the rationale to me, please?


回答 0

精简版

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

长版

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

合同有不同的级别。

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

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

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

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

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

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

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

Short version

ABCs offer a higher level of semantic contract between clients and the implemented classes.

Long version

There is a contract between a class and its callers. The class promises to do certain things and have certain properties.

There are different levels to the contract.

At a very low level, the contract might include the name of a method or its number of parameters.

In a staticly-typed language, that contract would actually be enforced by the compiler. In Python, you can use EAFP or type introspection to confirm that the unknown object meets this expected contract.

But there are also higher-level, semantic promises in the contract.

For example, if there is a __str__() method, it is expected to return a string representation of the object. It could delete all contents of the object, commit the transaction and spit a blank page out of the printer… but there is a common understanding of what it should do, described in the Python manual.

That’s a special case, where the semantic contract is described in the manual. What should the print() method do? Should it write the object to a printer or a line to the screen, or something else? It depends – you need to read the comments to understand the full contract here. A piece of client code that simply checks that the print() method exists has confirmed part of the contract – that a method call can be made, but not that there is agreement on the higher level semantics of the call.

Defining an Abstract Base Class (ABC) is a way of producing a contract between the class implementers and the callers. It isn’t just a list of method names, but a shared understanding of what those methods should do. If you inherit from this ABC, you are promising to follow all the rules described in the comments, including the semantics of the print() method.

Python’s duck-typing has many advantages in flexibility over static-typing, but it doesn’t solve all the problems. ABCs offer an intermediate solution between the free-form of Python and the bondage-and-discipline of a staticly-typed language.


回答 1

@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定义的一组抽象方法一致。

@Oddthinking’s answer is not wrong, but I think it misses the real, practical reason Python has ABCs in a world of duck-typing.

Abstract methods are neat, but in my opinion they don’t really fill any use-cases not already covered by duck typing. Abstract base classes’ real power lies in the way they allow you to customise the behaviour of isinstance and issubclass. (__subclasshook__ is basically a friendlier API on top of Python’s __instancecheck__ and __subclasscheck__ hooks.) Adapting built-in constructs to work on custom types is very much part of Python’s philosophy.

Python’s source code is exemplary. Here is how collections.Container is defined in the standard library (at time of writing):

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

This definition of __subclasshook__ says that any class with a __contains__ attribute is considered to be a subclass of Container, even if it doesn’t subclass it directly. So I can write this:

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

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

In other words, if you implement the right interface, you’re a subclass! ABCs provide a formal way to define interfaces in Python, while staying true to the spirit of duck-typing. Besides, this works in a way that honours the Open-Closed Principle.

Python’s object model looks superficially similar to that of a more “traditional” OO system (by which I mean Java*) – we got yer classes, yer objects, yer methods – but when you scratch the surface you’ll find something far richer and more flexible. Likewise, Python’s notion of abstract base classes may be recognisable to a Java developer, but in practice they are intended for a very different purpose.

I sometimes find myself writing polymorphic functions that can act on a single item or a collection of items, and I find isinstance(x, collections.Iterable) to be much more readable than hasattr(x, '__iter__') or an equivalent try...except block. (If you didn’t know Python, which of those three would make the intention of the code clearest?)

That said, I find that I rarely need to write my own ABC and I typically discover the need for one through refactoring. If I see a polymorphic function making a lot of attribute checks, or lots of functions making the same attribute checks, that smell suggests the existence of an ABC waiting to be extracted.

*without getting into the debate over whether Java is a “traditional” OO system…


Addendum: Even though an abstract base class can override the behaviour of isinstance and issubclass, it still doesn’t enter the MRO of the virtual subclass. This is a potential pitfall for clients: not every object for which isinstance(x, MyABC) == True has the methods defined on 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

Unfortunately this one of those “just don’t do that” traps (of which Python has relatively few!): avoid defining ABCs with both a __subclasshook__ and non-abstract methods. Moreover, you should make your definition of __subclasshook__ consistent with the set of abstract methods your ABC defines.


回答 2

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

A handy feature of ABCs is that if you don’t implement all necessary methods (and properties) you get an error upon instantiation, rather than an AttributeError, potentially much later, when you actually try to use the missing method.

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"

Example from https://dbader.org/blog/abstract-base-classes-in-python

Edit: to include python3 syntax, thanks @PandasRocks


回答 3

这将使确定对象是否支持给定协议而不必检查协议中所有方法的存在,或者无需由于“不支持”而在“敌人”领域内引发异常就容易得多。

It will make determining whether an object supports a given protocol without having to check for presence of all the methods in the protocol or without triggering an exception deep in “enemy” territory due to non-support much easier.


回答 4

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

Abstract method make sure that what ever method you are calling in the parent class has to be appear in child class. Below are noraml way of calling and using abstract. The program written in python3

Normal way of calling

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() is called’

c.methodtwo()

NotImplementedError

With Abstract method

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: Can’t instantiate abstract class Son with abstract methods methodtwo.

Since methodtwo is not called in child class we got error. The proper implementation is below

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() is called’