使用抽象基类与普通继承

21 投票
1 回答
5341 浏览
提问于 2025-04-17 22:38

我正在尝试理解使用抽象基类的好处。考虑这两段代码:

抽象基类:

from abc import ABCMeta, abstractmethod, abstractproperty

class CanFly:
    __metaclass__ = ABCMeta

    @abstractmethod
    def fly(self):
        pass

    @abstractproperty
    def speed(self):
        pass


class Bird(CanFly):

    def __init__(self):
        self.name = 'flappy'

    @property
    def speed(self):
        return 1

    def fly(self):
        print('fly')


b = Bird()

print(isinstance(b, CanFly))  # True
print(issubclass(Bird, CanFly))  #  True

普通继承:

class CanFly(object):

    def fly(self):
        raise NotImplementedError

    @property
    def speed(self):
        raise NotImplementedError()


class Bird(CanFly):

    @property
    def speed(self):
        return 1

    def fly(self):
        print('fly')


b = Bird()

print(isinstance(b, CanFly))  # True
print(issubclass(Bird, CanFly))  # True

如你所见,这两种方法都可以使用isinstanceissubclass来支持类型检查。

现在,我知道的一个区别是,如果你尝试创建一个抽象基类的子类,但没有重写所有的抽象方法或属性,你的程序会直接报错。但是,如果你使用普通继承,并且在代码中抛出NotImplementedError,那么你的代码不会在编写时就出错,而是在你真正调用那个方法或属性的时候才会出错。

除此之外,使用抽象基类还有什么不同之处呢?

1 个回答

12

在你提问中提到的内容之外,最重要的具体答案是,如果一个类使用了 @abstractmethod@abstractproperty 装饰器,并且继承自 ABC(或者使用了 ABCMeta 这个元类),那么你就不能实例化这个对象,也就是不能创建这个类的具体对象。

from abc import ABC, abstractmethod

class AbsParent(ABC):
    @abstractmethod
    def foo(self):
        pass

AbsParent()

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

不过,这里还有更多的内容。抽象基类(Abstract Base Classes)是在 Python 中引入的,具体可以参考 PEP 3119。我建议你看看“理由”这一部分,了解一下 Guido 为什么要引入它们。简单来说,它们并不是单纯为了某些具体的功能,而是有更深的哲学意义。它们的目的是向外部检查者表明这个对象是从 ABC 继承而来的,并且因为它是从 ABC 继承的,所以它会遵循一个诚信协议。这个“诚信协议”就是子类会遵循父类的意图。至于如何具体实现这个协议,就留给你自己去做了,这就是为什么它是一个诚信协议,而不是一个明确的合同。

这个主要通过 register() 方法体现出来。任何一个以 ABCMeta 作为元类的类(或者简单地说,继承自 ABC)都会有一个 register() 方法。通过将一个类注册到 ABC,你是在表明这个类继承自 ABC,尽管从技术上讲,它并没有真正继承。这就是诚信协议的体现。

from abc import ABC, abstractmethod

class MyABC(ABC):
    @abstractmethod
    def foo(self):
        """should return string 'foo'"""
        pass


class MyConcreteClass(object):
    def foo(self):
        return 'foo'

assert not isinstance(MyConcreteClass(), MyABC)
assert not issubclass(MyConcreteClass, MyABC)

虽然此时 MyConcreteClassMyABC 没有直接关系,但它确实按照评论中列出的要求实现了 MyABC 的 API。现在,如果我们把 MyConcreteClass 注册到 MyABC,它就会通过 isinstanceissubclass 的检查。

MyABC.register(MyConcreteClass)

assert isinstance(MyConcreteClass(), MyABC)
assert issubclass(MyConcreteClass, MyABC)

再次强调,这就是“诚信协议”的作用。你并不需要遵循 MyABC 中列出的 API。通过将具体类注册到 ABC,我们是在告诉任何外部检查者,我们这些程序员是遵循我们应该遵循的 API 的。

1 注意,@abstractproperty 现在不再推荐使用。你应该使用:

@property
@abstractmethod
def foo(self):
    pass

撰写回答