因为我习惯了Python中duck的老方法,所以我无法理解ABC(抽象基类)的需求。这本书很好地说明了如何使用它们。
我试图阅读PEP的基本原理,但我无法理解。如果我正在寻找可变序列容器,我会检查__setitem__,或者更有可能尝试使用它(EAFP)。我还没有遇到过数字模块的实际应用,它确实使用了abc,但这是我最接近的理解。
谁能给我解释一下原因吗?
因为我习惯了Python中duck的老方法,所以我无法理解ABC(抽象基类)的需求。这本书很好地说明了如何使用它们。
我试图阅读PEP的基本原理,但我无法理解。如果我正在寻找可变序列容器,我会检查__setitem__,或者更有可能尝试使用它(EAFP)。我还没有遇到过数字模块的实际应用,它确实使用了abc,但这是我最接近的理解。
谁能给我解释一下原因吗?
当前回答
短的版本
abc在客户机和实现的类之间提供了更高级别的语义契约。
长版本
类和它的调用者之间有一个契约。该类承诺做某些事情并具有某些属性。
合同有不同的层次。
在非常低的级别上,契约可能包括方法的名称或其参数的数量。
在静态类型语言中,该契约实际上由编译器强制执行。在Python中,您可以使用EAFP或类型自省来确认未知对象是否符合此预期契约。
但是契约中也有更高层次的语义承诺。
例如,如果存在__str__()方法,则期望它返回对象的字符串表示形式。它可以删除对象的所有内容,提交事务并从打印机中吐出空白页……但是对于它应该做什么有一个共同的理解,在Python手册中有描述。
这是一种特殊情况,手册中描述了语义契约。print()方法应该做什么?它应该将对象写入打印机,还是将一行写入屏幕,还是其他什么?这要视情况而定——你需要阅读评论来理解这里的完整合同。一段客户端代码只是检查print()方法是否存在,从而确认了部分契约——可以进行方法调用,但不确定是否在调用的高级语义上达成了一致。
定义抽象基类(ABC)是在类实现者和调用者之间产生契约的一种方式。它不仅仅是一个方法名称列表,而是对这些方法应该做什么的共同理解。如果您继承了这个ABC,则承诺遵守注释中描述的所有规则,包括print()方法的语义。
Python的duck-typing在灵活性方面比静态类型有很多优势,但它并不能解决所有问题。abc提供了一种介于Python的自由形式和静态类型语言的约束和规则之间的中间解决方案。
其他回答
短的版本
abc在客户机和实现的类之间提供了更高级别的语义契约。
长版本
类和它的调用者之间有一个契约。该类承诺做某些事情并具有某些属性。
合同有不同的层次。
在非常低的级别上,契约可能包括方法的名称或其参数的数量。
在静态类型语言中,该契约实际上由编译器强制执行。在Python中,您可以使用EAFP或类型自省来确认未知对象是否符合此预期契约。
但是契约中也有更高层次的语义承诺。
例如,如果存在__str__()方法,则期望它返回对象的字符串表示形式。它可以删除对象的所有内容,提交事务并从打印机中吐出空白页……但是对于它应该做什么有一个共同的理解,在Python手册中有描述。
这是一种特殊情况,手册中描述了语义契约。print()方法应该做什么?它应该将对象写入打印机,还是将一行写入屏幕,还是其他什么?这要视情况而定——你需要阅读评论来理解这里的完整合同。一段客户端代码只是检查print()方法是否存在,从而确认了部分契约——可以进行方法调用,但不确定是否在调用的高级语义上达成了一致。
定义抽象基类(ABC)是在类实现者和调用者之间产生契约的一种方式。它不仅仅是一个方法名称列表,而是对这些方法应该做什么的共同理解。如果您继承了这个ABC,则承诺遵守注释中描述的所有规则,包括print()方法的语义。
Python的duck-typing在灵活性方面比静态类型有很多优势,但它并不能解决所有问题。abc提供了一种介于Python的自由形式和静态类型语言的约束和规则之间的中间解决方案。
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
抽象方法确保你在父类中调用的任何方法都必须出现在子类中。下面是常用的调用和使用抽象的方法。 该程序用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()
不能用抽象方法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()被调用
它将使确定一个对象是否支持给定的协议变得更加容易,而不必检查协议中是否存在所有方法,也不必因为不支持而在“敌方”领土深处触发异常。
@Oddthinking的回答没有错,但我认为它忽略了Python在鸭子类型世界中拥有abc的真实、实际的原因。
抽象方法很简洁,但在我看来,它们并不能真正满足duck typing尚未涵盖的任何用例。抽象基类的真正力量在于它们允许你自定义isinstance和is子类的行为。(__subclassshook__基本上是Python的__instancecheck__和__subclasscheck__钩子之上的一个更友好的API。)调整内置构造来处理自定义类型是Python哲学的重要组成部分。
Python的源代码就是一个例子。以下是如何收集。容器在标准库中定义(在编写时):
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提供了一种在Python中定义接口的正式方式,同时保持了duck-typing的精神。此外,这是一种尊重开闭原则的工作方式。
Python的对象模型从表面上看类似于更“传统”的OO系统(我指的是Java*)——我们有你的类,你的对象,你的方法——但当你触及表面时,你会发现一些更丰富和更灵活的东西。类似地,Python的抽象基类概念对于Java开发人员来说可能是可以识别的,但实际上它们的用途非常不同。
我有时发现自己在编写可以作用于单个项或项的集合的多形函数,并且我发现isinstance(x, collections.Iterable)比hasattr(x, '__iter__')或等效的try…除了块。(如果你不懂Python,这三者中哪一个能最清楚地表达代码的意图?)
也就是说,我发现我很少需要编写自己的ABC,我通常是通过重构发现对ABC的需求。如果我看到一个多态函数进行了大量属性检查,或者许多函数进行了相同的属性检查,那么这种气味就表明存在一个等待提取的ABC。
*没有卷入Java是否是“传统的”OO系统的争论…
补充:即使一个抽象基类可以覆盖isinstance和is子类的行为,它仍然不会进入虚子类的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中相对较少!):避免同时使用__subclasshook__和非抽象方法来定义abc。此外,你应该使你的__subclasshook__的定义与你的ABC定义的抽象方法集一致。