Python中抽象类和接口的区别

638

Python中抽象类和接口的区别是什么?

8个回答

684

有时候你会看到以下内容:

class Abstract1:
    """Some description that tells you it's abstract,
    often listing the methods you're expected to supply."""

    def aMethod(self):
        raise NotImplementedError("Should have implemented this")

因为Python没有(也不需要)正式的接口合约,所以抽象和接口之间的Java风格的区别不存在。如果有人费心定义一个正式的接口,它也将是一个抽象类。唯一的区别在于docstring中陈述的意图。

当你使用鸭子类型时,抽象和接口之间的区别是微不足道的。

Java使用接口是因为它没有多重继承。

因为Python有多重继承,你也可能会看到像这样的东西

class SomeAbstraction:
    pass  # lots of stuff - but missing something

class Mixin1:
    def something(self):
        pass  # one implementation

class Mixin2:
    def something(self):
        pass  # another

class Concrete1(SomeAbstraction, Mixin1):
    pass

class Concrete2(SomeAbstraction, Mixin2):
    pass

使用了一种带混合组件的抽象超类来创建不相交的具体子类。


7
S. Lott,你的意思是由于鸭子类型(duck typing),拥有(接口)和是(继承)之间的区别并不重要吗? - Lorenzo
3
当你使用鸭子类型时,抽象类和接口之间的区别微不足道。我不知道“substantial”是什么意思。从设计的角度来看,它是“真实”的 - 具有实质性。但从语言的角度来看可能没有支持。在Python中,您可以采用约定来区分抽象类和接口类定义。 - S.Lott
30
@L.DeLeo - 你确定你对has-a和is-a的概念理解正确吗?通常我认为has-a表示成员变量,而is-a则表示继承(父类或接口)。比如在Java中,Comparable或List都是is-a关系,无论它们是接口还是抽象类。 - dimo414
50
"NotImplementedError("类%s没有实现aMethod()" %(self.class.__name__))"是更具信息性的错误消息:)" - naught101
12
“有一个”关系与继承、鸭子类型、接口和抽象类没有任何关系(这四个都涉及“是一个”关系)。 - Kalle Richter
3
Python可能不需要正式的接口协议,但编写Python程序的是“人类”,而非机器。接口的作用在于帮助“人类”编写和分享代码,因为计算机只需要二进制语言,而“人类”需要一种更易懂的语言。 - CpILL

228

Python中抽象类和接口的区别是什么?

对于一个对象而言,接口是该对象上的一组方法和属性。

在Python中,我们可以使用抽象基类来定义和强制实现一个接口。

使用抽象基类

例如,假设我们想要使用collections模块中的一个抽象基类:

import collections
class MySet(collections.Set):
    pass

如果我们尝试使用它,会出现TypeError错误,因为我们创建的类不支持集合的预期行为:

>>> MySet()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MySet with abstract methods
__contains__, __iter__, __len__

我们需要至少实现__contains____iter____len__。让我们使用来自文档的这个实现示例:

class ListBasedSet(collections.Set):
    """Alternate set implementation favoring space over speed
    and not requiring the set elements to be hashable. 
    """
    def __init__(self, iterable):
        self.elements = lst = []
        for value in iterable:
            if value not in lst:
                lst.append(value)
    def __iter__(self):
        return iter(self.elements)
    def __contains__(self, value):
        return value in self.elements
    def __len__(self):
        return len(self.elements)

s1 = ListBasedSet('abcdef')
s2 = ListBasedSet('defghi')
overlap = s1 & s2

实现:创建一个抽象基类

我们可以通过将元类设置为abc.ABCMeta,并在相关方法上使用abc.abstractmethod修饰符来创建自己的抽象基类。元类将会将修饰的函数添加到__abstractmethods__属性中,直到这些方法被定义之前都会阻止实例化。

import abc

例如,“effable”被定义为可以用言语表达的东西。假设我们想在Python 2中定义一个抽象基类,它是“effable”的:
class Effable(object):
    __metaclass__ = abc.ABCMeta
    @abc.abstractmethod
    def __str__(self):
        raise NotImplementedError('users must define __str__ to use this base class')

在Python 3中,元类声明有轻微变化:

class Effable(object, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def __str__(self):
        raise NotImplementedError('users must define __str__ to use this base class')

现在如果我们尝试创建一个未实现接口的可表达对象:
class MyEffable(Effable): 
    pass

尝试实例化它:

>>> MyEffable()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyEffable with abstract methods __str__

我们被告知工作没有完成。
现在,如果我们提供预期的接口:
class MyEffable(Effable): 
    def __str__(self):
        return 'expressable!'

我们随后可以使用从抽象类派生的具体类的版本:
>>> me = MyEffable()
>>> print(me)
expressable!

我们可以通过这种方法做其他事情,比如注册已经实现这些接口的虚拟子类,但我认为这超出了这个问题的范围。然而,这里演示的其他方法将不得不使用abc模块来适应此方法。

结论

我们已经证明了在Python中创建抽象基类可以为自定义对象定义接口。


1
感谢您的详细解释! 请问一下,class Effable(object, metaclass=abc.ABCMeta):class Effable(abc.ABC): 之间有什么区别(如果有的话)? - Donatas Olsevičius

102

Python >= 2.6有抽象基类

抽象基类(缩写为ABC)通过提供定义接口的方式,补充了鸭子类型,当使用其他技术(如hasattr())会很笨拙时。Python内置了许多用于数据结构(在collections模块中)、数字(在numbers模块中)和流(在io模块中)的ABC。您可以使用abc模块创建自己的ABC。

还有Zope接口模块,被zope之外的项目使用,比如twisted。我对它不太熟悉,但这里有一个维基页面here可能会有所帮助。

总的来说,在Python中不需要抽象类或接口的概念(已编辑-有关详细信息,请参见S.Lott的答案)。


3
在Python中使用ABCs能带来什么好处? - CpILL

51

更基本的解释是: 接口有点像一个空的松饼盘。 它是一个类文件,具有一组没有代码的方法定义。

抽象类也是同样的东西,但不是所有函数都需要为空。有些可以有代码。它不是严格为空的。

为什么要区分: 在Python中实际上没有太大的区别,但对于大型项目的规划,更常见的是讨论接口,因为它没有代码。特别是如果你正在与习惯于这个术语的Java程序员一起工作。


1
+1 是为了突出 ABC 可以有自己的实现方式 - 这似乎是一个非常聪明的自我超越的方式 - Titou

41

Python并没有这两个概念。

它使用鸭子类型,这样就不需要接口(至少对于计算机而言是这样的 :-))

Python <= 2.5: 基类显然存在,但没有明确的方法可以将方法标记为“纯虚拟”,所以该类实际上并不是抽象的。

Python >= 2.6: 抽象基类确实存在 (http://docs.python.org/library/abc.html)。并且允许您指定必须在子类中实现的方法。我不太喜欢语法,但该功能已经存在。大多数时候,从“使用”客户端方面来说,使用鸭子类型可能更好。


3
Python 3.0 引入了真正的抽象基类,它们被用于 collections 模块以及其他地方。http://docs.python.org/3.0/library/abc.html - Lara Dougan
一份关于为什么鸭子类型可以消除接口需求的参考资料会很有帮助。对我来说,似乎并不明显鸭子类型(我理解为能够“戳”任何对象上的任何方法或属性)意味着您不需要指定所需的行为(并让编译器提醒您实现它们),这就是我理解的抽象基类的方式。 - Reb.Cabin
不只是鸭子类型,而是支持多重继承的特性消除了人为接口和抽象类之间的分界线,比如Java所画的。 - omni

21
一般来说,只有使用单继承类模型的语言才会使用接口。在这些单继承语言中,如果任何类都可以使用特定的方法或一组方法,则通常会使用接口。此外,在这些单继承语言中,抽象类用于定义除了一个或多个方法之外的类变量,或利用单继承模型来限制可使用一组方法的类范围。
支持多重继承模型的语言倾向于仅使用类或抽象基类而不是接口。由于Python支持多重继承,因此不使用接口,您需要使用基类或抽象基类。 http://docs.python.org/library/abc.html

4

抽象类是包含一个或多个抽象方法的类。除了抽象方法外,抽象类还可以具有静态、类和实例方法。 但在接口的情况下,它只有抽象方法而没有其他方法。因此,不必继承抽象类,但必须继承接口。


0

为了完整起见,我们应该提到PEP3119,其中介绍了ABC并与接口进行了比较,以及Talin's的原始评论。

抽象类不是完美的接口:

  • 属于继承层次结构
  • 可变的

但如果您考虑自己编写它:

def some_function(self):
     raise NotImplementedError()

interface = type(
    'your_interface', (object,),
    {'extra_func': some_function,
     '__slots__': ['extra_func', ...]
     ...
     '__instancecheck__': your_instance_checker,
     '__subclasscheck__': your_subclass_checker
     ...
    }
)

ok, rather as a class
or as a metaclass
and fighting with python to achieve the immutable object
and doing refactoring
...

你很快就会意识到自己正在重新发明轮子,最终实现abc.ABCMetaabc.ABCMeta被提出作为缺失接口功能的有用补充,在像Python这样的语言中这是相当公平的。
当然,在编写版本3时,它能够得到更好的增强,并添加新的语法和不可变接口概念...
结论:
The abc.ABCMeta IS "pythonic" interface in python

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接