@Oddthinking的回答没有错,但我认为它忽略了Python在鸭子类型的世界中拥有抽象基类的真正和实际原因。
抽象方法很好,但我认为它们并没有填补任何鸭子类型已经覆盖的用例。抽象基类的真正力量在于它们允许您自定义isinstance
和issubclass
的行为方式。(__subclasshook__
基本上是Python __instancecheck__
和__subclasscheck__
钩子的友好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 类,也被视为 Container 的子类。因此我可以写出这个例子:class ContainAllTheThings(object):
def __contains__(self, item):
return True
>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True
话虽如此,我发现我很少需要编写自己的ABC,并且通常是通过重构发现需要一个。如果我看到一个多态函数进行了许多属性检查,或者许多函数进行了相同的属性检查,则这种味道表明存在一个等待提取的ABC。
*不涉及Java是否为“传统”面向对象系统的争论......
附录: 即使抽象基类可以重写 isinstance
和 issubclass
的行为,它仍然不会进入虚拟子类的MRO。这是客户端的一个潜在陷阱:并非每个对象都有 MyABC
定义的方法,即使 isinstance(x, MyABC) == True
。
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
__subclasshook__
和非抽象方法定义ABC。此外,您应该使__subclasshook__
的定义与ABC定义的抽象方法集一致。ABCs在客户端和实现类之间提供了更高级别的语义契约。
类与其调用者之间存在一种契约。该类承诺执行某些操作并具有某些属性。
契约有不同的层次。
在非常低的层次上,契约可能包括方法的名称或其参数数量。
在静态类型语言中,编译器实际上会强制执行该契约。在Python中,您可以使用EAFP或类型内省来确认未知对象是否符合预期的契约。
但是,在契约中还存在更高级别的语义承诺。
例如,如果存在__str__()
方法,则应返回对象的字符串表示形式。它可能删除对象的所有内容,提交事务并从打印机中输出空白页面...但是,Python手册中描述了它应该执行的常见理解。
这是一个特殊情况,语义契约在手册中有所描述。 print()
方法应该做什么?它应该将对象写入打印机或屏幕上的一行,还是其他什么?这取决于 - 您需要阅读注释以了解此处完整的契约。一个简单检查 print()
方法是否存在的客户端代码已经确认了契约的一部分 - 即可以进行方法调用,但并不意味着对调用的更高级别语义达成了共识。
定义抽象基类(ABC)是产生类实现者和调用者之间契约的一种方式。它不仅仅是方法名称列表,而是对这些方法应该执行的操作的共同理解。如果您从此 ABC 继承,那么您承诺遵循注释中描述的所有规则,包括 print()
方法的语义。
Python 的鸭子类型在灵活性方面具有许多优势,但并不能解决所有问题。ABC 提供了 Python 自由形式和静态类型语言束缚与纪律之间的中间解决方案。
__contains__
的类和继承自 collections.Container
的类之间有什么区别?在你的例子中,Python 中始终存在着对 __str__
的共识。实现 __str__
承诺的内容与继承某些 ABC 然后实现 __str__
相同。在两种情况下,您都可以违反合同;没有像静态类型检查中那样的可证明语义。 - Muhammad Alkarouricollections.Container
是一个退化情况,它仅包括\_\_contains\_\_
方法,并且仅用于表示预定义约定。单独使用ABC并不能增加多少价值,我同意这一点。我怀疑它被添加是为了允许(例如)Set
继承它。当你到达Set
时,突然属于该ABC具有相当大的语义。一个项不能属于集合两次。这不可以通过方法的存在来检测。 - OddthinkingSet
比print()
更适合作为例子。我试图找到一个含义模糊、仅凭名称和Python手册无法确定其正确性的方法名称。 - OddthinkingABC的一个方便之处在于,如果您没有实现所有必要的方法(和属性),则在实例化时会出现错误,而不是在您实际尝试使用缺少的方法时可能会出现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。
使用该技术,可以更轻松地确定对象是否支持给定的协议,而无需检查协议中所有方法的存在性,也不会由于不支持而在“敌方”领土深处触发异常。
抽象方法确保在父类中调用的任何方法也必须出现在子类中。以下是调用和使用抽象方法的常规方式。该程序是用Python3编写的。
常规调用方式:
class Parent:
def method_one(self):
raise NotImplemented()
def method_two(self):
raise NotImplementedError()
class Son(Parent):
def method_one(self):
return 'method_one() is called'
c = Son()
c.method_one()
c.method_two()
from abc import ABCMeta, abstractmethod
class Parent(metaclass=ABCMeta):
@abstractmethod
def method_one(self):
raise NotImplementedError()
@abstractmethod
def method_two(self):
raise NotImplementedError()
class Son(Parent):
def method_one(self):
return 'method_one() is called'
c = Son()
from abc import ABCMeta, abstractmethod
class Parent(metaclass=ABCMeta):
@abstractmethod
def method_one(self):
raise NotImplementedError()
@abstractmethod
def method_two(self):
raise NotImplementedError()
class Son(Parent):
def method_one(self):
return 'method_one() is called'
def method_two(self):
return 'method_two() is called'
c = Son()
c.method_one()
abstractmethod
会发生什么。 - Louis Yang@abstractmethod
装饰器定义的函数必须在子类中实现。如果我们有一个没有实现的函数,就会得到 Can't instantiate abstract class
错误。如果这是真的,那么这是非常有用的事情。 - Prakhar Sharma面向接口编程,而不是面向实现(继承会破坏封装性;面向接口编程促进松耦合/控制反转/“好莱坞原则”:我们不会打电话给你,我们会叫你)
优先使用对象组合而非类继承(委托工作)
封装变化的概念(开闭原则使类对扩展开放,但对修改关闭)
mypy
)的出现,可以将 ABC 用作类型,而不是对每个函数接受的参数或返回的对象使用 Union[...]
。想象一下,每当您的代码库支持新对象时,都必须更新类型而不是实现?这会非常快地变得难以维护(不具有可扩展性)。
isinstance(x, collections.Iterable)
对我来说更清晰,我也了解Python。 - Muhammad AlkarouriContainer.register(ContainAllTheThings)
才能工作吗? - BoZenKhaa__subclasshook__
的意思是“任何满足此谓词的类都被视为子类,用于isinstance
和issubclass
检查,无论它是否已在 ABC 中注册,以及 无论它是否是直接子类”。正如我在答案中所说,如果你实现了正确的接口,你就是一个子类! - Benjamin Hodgson