因为我习惯了 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?
发布评论
评论(6)
@Oddthinking 的答案并没有错,但我认为它忽略了 Python 在鸭子打字世界中拥有 ABC 的真实、实用原因。
抽象方法很简洁,但在我看来,它们并没有真正满足鸭子类型尚未涵盖的任何用例。抽象基类的真正力量在于它们允许您自定义的方式
isinstance
和issubclass
的行为。 (__subclasshook__
基本上是一个基于 Python__instancecheck__
和__subclasscheck__
挂钩。)调整内置构造以处理自定义类型是 Python 哲学的重要组成部分。Python 的源代码堪称典范。 这里是
collections.Container 在标准库中定义(在撰写本文时):
__subclasshook__
的定义表示任何具有__contains__
属性的类都被视为 Container 的子类,即使它不直接子类化它。所以我可以这样写:换句话说,如果你实现了正确的接口,你就是一个子类! ABC 提供了一种在 Python 中定义接口的正式方法,同时忠于鸭子的精神 -打字。此外,它的工作方式遵循开闭原则。
Python 的对象模型表面上看起来与更“传统”的 OO 系统(我指的是 Java*)相似——我们有类、对象、方法——但是当你触及表面时,你会发现更丰富和更丰富的东西。更灵活。同样,Java 开发人员可能会认识到 Python 的抽象基类概念,但实际上它们的目的却截然不同。
有时我发现自己编写了可以作用于单个项目或项目集合的多态函数,并且我发现 isinstance(x, collections.Iterable) 比 hasattr(x) 更具可读性、 '__iter__') 或等效的
try... except
块。 (如果您不了解 Python,那么这三者中哪一个能让代码的意图最清晰?)也就是说,我发现我很少需要编写自己的 ABC,并且通常通过重构来发现对 ABC 的需求。如果我看到一个多态函数进行大量属性检查,或者许多函数进行相同的属性检查,那么这种气味表明存在等待提取的 ABC。
*不涉及Java是否是“传统”OO系统的争论...
附录:即使抽象基类可以覆盖
isinstance的行为
和issubclass
,它仍然没有进入MRO。这对于客户端来说是一个潜在的陷阱:并非每个isinstance(x, MyABC) == True
的对象都具有在MyABC
上定义的方法。不幸的是,这是“不要这样做”的陷阱之一(Python 的陷阱相对较少!):避免使用
__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
andissubclass
. (__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):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: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 thanhasattr(x, '__iter__')
or an equivalenttry...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
andissubclass
, it still doesn't enter the MRO of the virtual subclass. This is a potential pitfall for clients: not every object for whichisinstance(x, MyABC) == True
has the methods defined onMyABC
.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.简短版本的
ABC 在客户端和实现的类之间提供了更高级别的语义契约。
长版本
类和它的调用者之间有一个契约。该类承诺做某些事情并具有某些属性。
合同有不同的级别。
在非常低的级别,契约可能包括方法的名称或其参数的数量。
在静态类型语言中,该约定实际上由编译器强制执行。在 Python 中,您可以使用 EAFP 或键入 introspection 来确认未知数对象满足此预期合同。
但合同中还有更高级别的语义承诺。
例如,如果有一个 __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 theprint()
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.
ABC 的一个方便的功能是,如果您没有实现所有必要的方法(和属性),您会在实例化时收到错误,而不是
AttributeError
,可能会在很久以后,当您实际尝试使用缺少的方法时。示例来自 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.Example from https://dbader.org/blog/abstract-base-classes-in-python
Edit: to include python3 syntax, thanks @PandasRocks
它将使得确定一个对象是否支持给定协议变得更加容易,而无需检查协议中所有方法的存在,也不会因不支持而触发深入“敌方”领域的异常。
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.
抽象方法确保您在父类中调用的任何方法也必须出现在子类中。以下是调用和使用抽象的常规方式。
程序是用python3编写的,
正常调用方式:
使用抽象方法:
由于 method_two 没有在子类中调用,我们得到了错误。正确的实现如下:
Abstract methods make sure that whatever method you are calling in the parent class, has to also appear in the child class. Below are normal ways of calling and using abstract.
The program is written in python3
Normal way of calling:
With Abstract method:
Since method_two is not called in child class we got error. The proper implementation is below:
ABC 能够创建设计模式和框架。请参阅 Brandon Rhodes 的 pycon 演讲:
Python 设计模式 1
Python 本身内的协议(更不用说迭代器、装饰器和槽(它们本身实现 FlyWeight 模式))都是可能的,因为 ABC 的(尽管在 CPython 中实现为虚拟方法/类)。
Brandon 提到,鸭子类型确实使 Python 中的某些模式变得微不足道,但许多其他模式继续出现并在 Python 中有用,例如适配器。
简而言之,ABC 使您能够编写可扩展且可重用的代码。根据 GoF:
对接口进行编程,而不是实现(继承破坏了封装;对接口进行编程会促进松散耦合/控制反转/“好莱坞原则:不要打电话给我们” ,我们会给您打电话”)
优先考虑对象组合而不是类继承(委派工作)
封装变化的概念(开闭原则使类对扩展开放,但对修改封闭)
此外,随着Python静态类型检查器的出现(例如
mypy
),对于函数接受作为参数或返回的每个对象,可以使用 ABC 作为类型而不是Union[...]
。想象一下,每次您的代码库支持新对象时,都必须更新类型而不是实现?这很快就会变得无法维护(无法扩展)。ABC's enable design patterns and frameworks to be created. Please see this pycon talk by Brandon Rhodes:
Python Design Patterns 1
The protocols within Python itself (not to mention iterators, decorators, and slots (which themselves implement the FlyWeight pattern)) are all possible because of ABC's (albeit implemented as virtual methods/classes in CPython).
Duck typing does make some patterns trivial in python, which Brandon mentions, but many other patterns continue to pop up and be useful in Python, e.g. Adapters.
In short, ABC's enable you to write scalable and reusable code. Per the GoF:
Program to an interface, not an implementation (inheritance breaks encapsulation; programming to an interface promotes loose-coupling/inversion of control/the "HollyWood Principle: Don't call us, we'll call you")
Favor object composition over class inheritance (delegate the work)
Encapsulate the concept that varies (the open-closed principle makes classes open for extension, but closed for modification)
Additionally, with the emergence of static type checkers for Python (e.g.
mypy
), an ABC can be used as a type instead ofUnion[...]
for every object a function accepts as an argument or returns. Imagine having to update the types, not the implementation, every time your code base supports a new object? That gets unmaintainable (doesn't scale) very fast.