Python 用抽象基类避免继承错误,抽象基类(abstract base class,ABC)用来确保派生类实现了基类中的特定方法。本节将学习其优点以及如何使用Python内置的abc
模块来定义抽象基类。
那么抽象基类适用于哪些地方呢?前一段时间,我跟同事讨论了在Python中哪种模式适合用来实现可维护的类层次结构。具体来说,就是想以方便程序员且可维护的方式为服务端定义简单的类层次结构。
我们有一个BaseService
类定义了一个通用接口和几个具体的实现。这些具体的实现(MockService
和RealService
等)各自做不同的事情,但都提供了相同的接口。明确一下这种关系:所有这些具体实现都是BaseService
的子类。
为了使这些代码尽可能易于维护和方便程序员使用,我们希望确保以下几点:
- 无法实例化基类;
-
如果忘记在其中一个子类中实现接口方法,那么要尽早报错。
什么要使用Python的abc
模块来解决这个问题?上述设计在复杂的系统中很常见,为了强制派生类实现基类中的许多方法,通常使用如下Python惯用法:
class Base:
def foo(self):
raise NotImplementedError()
def bar(self):
raise NotImplementedError()
class Concrete(Base):
def foo(self):
return 'foo() called'
# 忘记重载bar()了……
# def bar(self):
# return "bar() called"
第一次尝试解决问题时,会发现在Base
的实例上调用方法能正确引发NotImplementedError
异常:
>>> b = Base()
>>> b.foo()
>NotImplementedError
此外,Concrete
类也能正确地实例化和使用。如果在其实例上调用未实现的方法bar()
也会引发异常:
>>> c = Concrete()
>>> c.foo()
'foo() called'
>>> c.bar()
NotImplementedError
第一个实现还不错,但不够完美,有以下缺点可以改进:
- 实例化
Base
时没有报错; -
提供了不完整的子类,即实例化
Concrete
并不会报错,只有在调用缺失的bar()
方法时才报错。
使用自Python 2.6添加的abc
模块可以更好地解决剩下的这些问题。下面这个改进版使用abc
模块定义了抽象基类:
from abc import ABCMeta, abstractmethod
class Base(metaclass=ABCMeta):
@abstractmethod
def foo(self):
pass
@abstractmethod
def bar(self):
pass
class Concrete(Base):
def foo(self):
pass
# 又忘记声明bar()了……
这种方式仍然能按预期运行并正确地创建类层次结构:
assert issubclass(Concrete, Base)
这么做有额外的好处。如果忘记实现某个抽象方法,实例化Base
的子类时会引发TypeError
。引发的异常会告诉我们缺少哪些方法:
>>> c = Concrete()
TypeError:
"Can't instantiate abstract class Concrete
with abstract methods bar"
不用abc
模块的话,如果缺失某个方法,则只有在实际调用这个方法时才会抛出NotImplementedError
。在实例化时就告知缺少某个方法的好处很多,这样更难编写出无效的子类。如果你正在编写新的代码可能还体会不到,但几周或几个月后就会感觉到这个优点了。
当然,这种模式并不能完全替代编译时的类型检查,但能使类层次更稳健且易于维护。使用ABC可以清楚地说明程序员的意图,从而使代码更易于理解。建议你阅读abc
模块文档,并留意适用这种模式的情形。
关键要点
-
抽象基类(ABC)能在派生类实例化时检查其是否实现了基类中的某些特定方法。
-
使用ABC可以帮助避免bug并使类层次易于理解和维护。