为什么在Python中使用抽象基类?
-
01-10-2019 - |
解决方案
简洁版本
ABC在客户和实施类之间提供更高水平的语义合同。
长版
班级及其呼叫者之间有合同。该课程承诺做某些事情并具有某些特性。
合同有不同的水平。
在非常低的级别上,合同可能包括方法的名称或其参数数量。
用静态的语言,该合同实际上将由编译器执行。在Python,您可以使用EAFP或内省来确认未知的对象符合该预期合同。
但是合同中也有更高级别的语义承诺。
例如,如果有一个 __str__()
方法,预计将返回对象的字符串表示。它 可以 删除对象的所有内容,提交交易并从打印机中吐出一个空白页面...但是对Python手册中描述的对其应采取的措施有一个共同的理解。
这是一个特殊情况,在手册中描述了语义合同。应该什么 print()
方法吗?它应该将对象写入打印机或屏幕上的行,还是其他东西?这取决于 - 您需要阅读评论以了解此处的完整合同。一块客户代码,简单地检查 print()
存在方法已确认合同的一部分 - 可以进行方法调用,但没有关于呼叫的更高级别的语义达成共识。
定义抽象基类(ABC)是一种在类实施者和呼叫者之间生产合同的方式。它不仅是方法名称的列表,而且对这些方法应该做什么的共同理解。如果您从此ABC继承,您将承诺遵循评论中描述的所有规则,包括 print()
方法。
Python的鸭型在静态型的灵活性方面具有许多优势,但并不能解决所有问题。 ABC在Python的自由形式与静态类型语言的束缚和文书之间提供了中间解决方案。
其他提示
@OddThinking的答案没有错,但我认为这错过了 真实的, 实际的 Python的原因在一个鸭型世界中拥有ABC。
抽象方法很整洁,但是在我看来,它们并没有真正填充鸭打字尚未涵盖的任何用例。抽象基类的真实力量在于 他们允许您自定义行为的方式 isinstance
和 issubclass
. (__subclasshook__
基本上是Python的友好的API __instancecheck__
和 __subclasscheck__
钩子。)适应内置的构造来处理自定义类型,这是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__
属性被认为是容器的子类,即使它没有直接亚类。所以我可以写这件事:
class ContainAllTheThings(object):
def __contains__(self, item):
return True
>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True
换句话说, 如果您实现正确的接口,则是子类! ABC提供了一种形式的方式来定义Python的界面,同时忠于鸭子的精神。此外,这以纪念 开放原则.
Python的对象模型在表面上看起来与更“传统”的OO系统(我的意思是Java*)相似 - 我们得到了您的类,您的对象,您的方法 - 但是当您刮擦表面时,您会发现一些更丰富的东西,并且更灵活。同样,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的陷阱相对较少!):避免将ABC定义为 __subclasshook__
和非抽象方法。而且,您应该对 __subclasshook__
与您的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,因此我们有错误。适当的实现如下
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()称为'