Answers:
ABC在客户端和已实现的类之间提供了更高级别的语义协定。
类与其调用者之间存在合同。该类承诺做某些事情并具有某些属性。
合同有不同的级别。
在非常低的级别上,合同可能包含方法名称或其参数数量。
在静态类型的语言中,该约定实际上将由编译器强制执行。在Python中,您可以使用EAFP或键入内省以确认未知对象是否符合此预期合同。
但是合同中还有更高层次的语义承诺。
例如,如果有__str__()
方法,则期望返回对象的字符串表示形式。它可以删除对象的所有内容,提交事务并在打印机上吐出空白页...但是,Python手册中对此有一个普遍的了解。
这是一种特殊情况,其中在手册中描述了语义约定。该print()
方法应该做什么?它应该将对象写入打印机还是将行写入屏幕,或者其他?这取决于-您需要阅读评论以了解此处的完整合同。一段简单地检查该print()
方法是否存在的客户代码已确认了合同的一部分-可以进行方法调用,但未就该调用的较高层语义达成协议。
定义抽象基类(ABC)是在类实现者和调用者之间产生合同的一种方式。它不仅是方法名称的列表,而且是对这些方法应该做什么的共识。如果您从该ABC继承,则承诺遵守注释中描述的所有规则,包括print()
方法的语义。
与静态类型相比,Python的鸭子类型在灵活性方面具有许多优势,但是并不能解决所有问题。ABC在Python的自由形式和静态类型的语言的约束与约束之间提供了一种中间解决方案。
collections.Container
是一个简并的情况,仅包括\_\_contains\_\_
,仅表示预定义的约定。我同意,使用ABC本身并不会增加太多价值。我怀疑它是为了允许(例如)Set
继承而添加的。在您到达时Set
,突然属于ABC的语义就变得相当多了。一个项目不能两次属于该集合。通过方法的存在无法检测到。
Set
是比更好的例子print()
。我试图找到一个含义不明确的方法名称,并且不能仅仅被该名称所困扰,因此您无法确定仅通过其名称和Python手册就可以正确地执行该方法。
Set
代替示例的方式重写答案print
?Set
很有意义,@ Oddthinking。
@Oddthinking的答案是正确的,但我认为它没有想到在鸭蛋式的世界中Python具有ABC 的真实,实际原因。
抽象方法很简洁,但我认为它们并没有真正填补鸭子类型尚未涵盖的任何用例。抽象基类的真正力量在于它们允许您自定义isinstance
and 行为的方式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定义的一组抽象方法一致。
isinstance(x, collections.Iterable)
对我来说更清楚,而且我确实知道Python。
C
子类中删除(或搞砸了无法修复)在abc_method()
从继承MyABC
。主要区别在于,搞砸继承契约的是超类,而不是子类。
Container.register(ContainAllTheThings)
为了给定的示例而工作吗?
__subclasshook__
是“为了满足isinstance
和issubclass
检查的目的,满足此谓词的任何类都将被视为子类,而不管它是否已在ABC中注册,并且无论它是否是直接子类 ”。就像我在答案中所说的那样,如果实现正确的接口,那么您就是一个子类!
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
这将使确定对象是否支持给定协议而不必检查协议中所有方法的存在,或者无需由于“不支持”而在“敌人”领域内引发异常就容易得多。
抽象方法确保您在父类中调用的任何方法都必须出现在子类中。以下是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()”
__contains__
的类与继承自的类之间有什么区别collections.Container
?在您的示例中,在Python中始终对达成共识__str__
。实现__str__
与从一些ABC继承然后实现的承诺相同__str__
。在两种情况下,您都可以违反合同;没有可证明的语义,例如我们在静态类型中所具有的语义。