问题:为什么在Python中使用抽象基类?
因为我习惯了Python中鸭子输入的旧方法,所以我无法理解对ABC(抽象基类)的需求。的帮助下是如何使用它们好。
我试图阅读PEP中的基本原理,但它使我感到头疼。如果我正在寻找一个可变序列容器,我会检查__setitem__
,或更可能尝试使用它(EAFP)。我还没有真正使用过数字模块,它确实使用了ABC,但这是我必须了解的最接近的数字。
有人可以向我解释理由吗?
回答 0
精简版
ABC在客户端和已实现的类之间提供了更高级别的语义协定。
长版
类与其调用者之间存在合同。该类承诺做某些事情并具有某些属性。
合同有不同的级别。
在非常低的级别上,合同可能包含方法名称或其参数数量。
在静态类型的语言中,该约定实际上将由编译器强制执行。在Python中,您可以使用EAFP或键入内省以确认未知对象是否符合此预期合同。
但是合同中还有更高层次的语义承诺。
例如,如果有__str__()
方法,则期望返回对象的字符串表示形式。它可以删除对象的所有内容,提交事务并在打印机上吐出空白页…但是,Python手册中对此有一个普遍的了解。
这是一种特殊情况,其中在手册中描述了语义约定。该print()
方法应该做什么?它应该将对象写入打印机还是将行写入屏幕,或者其他?这取决于-您需要阅读评论以了解此处的完整合同。一段简单地检查该print()
方法是否存在的客户代码已确认了合同的一部分-可以进行方法调用,但未就该调用的较高层语义达成协议。
定义抽象基类(ABC)是在类实现者和调用者之间产生合同的一种方式。它不仅是方法名称的列表,而且是对这些方法应该做什么的共识。如果您从该ABC继承,则承诺遵守注释中描述的所有规则,包括print()
方法的语义。
与静态类型相比,Python的鸭子类型在灵活性方面具有许多优势,但是并不能解决所有问题。ABC在Python的自由形式和静态类型的语言的约束与约束之间提供了一种中间解决方案。
回答 1
@Oddthinking的答案是正确的,但我认为它没有想到在鸭蛋式的世界中Python具有ABC 的真实,实际原因。
抽象方法很简洁,但我认为它们并没有真正填补鸭子类型尚未涵盖的任何用例。抽象基类的真正力量在于__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定义的一组抽象方法一致。
回答 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
回答 3
这将使确定对象是否支持给定协议而不必检查协议中所有方法的存在,或者无需由于“不支持”而在“敌人”领域内引发异常就容易得多。
回答 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()”