Python 中 NotImplementedError 和抽象基类

FreeGuideOnline 最新 2026-07-06

Python 中的 NotImplementedError 与抽象基类:从强制约定到结构化设计

当你编写一个基类,希望子类必须实现某些方法时,Python 为你提供了两条路径:手动抛出 NotImplementedError使用 abc 模块定义抽象基类 (Abstract Base Class)。本篇教程将带你从简单的约定式报错,逐步过渡到利用语言内置的抽象机制编写更健壮、更易维护的代码。

1. 初识 NotImplementedError:一个“此路不通”的信号

NotImplementedError 是 Python 内置的一个异常,通常用于表明某个方法或函数尚未实现。它不是语法错误,而是一种开发者之间的约定:任何调用未实现方法的代码都会在运行时触发这个错误,提醒使用者必须提供具体实现。

最典型的场景出现在基类中:

class Animal:
    def speak(self):
        raise NotImplementedError("子类必须实现 speak 方法")

class Dog(Animal):
    # 忘记实现 speak 方法
    pass

dog = Dog()
dog.speak()  # NotImplementedError: 子类必须实现 speak 方法

这种方式简单直观,但它有几个明显的缺陷:

  • 只有实际调用该方法时才会报错,如果你创建了对象却从未调用过那个方法,错误会一直隐藏。
  • 无法在实例化时就阻止创建不完整的子类对象。
  • 只能用于方法,无法强制要求子类实现某些属性

2. 抽象基类 (ABC):将“必须实现”写进类的规则里

Python 的 abc 模块提供了真正的抽象基类支持。通过 @abstractmethod 装饰器,你可以声明抽象方法;通过继承 ABC,你的基类变成抽象类。任何继承抽象基类的子类,只要没有实现所有抽象方法,就会在实例化时立即抛出 TypeError

2.1 定义抽象基类

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        """发出声音"""
        pass

# 尝试直接实例化 Animal 会报错:TypeError,因为它包含抽象方法
# animal = Animal()  # 取消注释将导致错误

2.2 子类必须实现所有抽象方法

class Cat(Animal):
    def speak(self):
        return "Meow"

cat = Cat()          # 正常实例化
print(cat.speak())   # Meow

class Fish(Animal):
    pass

# fish = Fish()      # TypeError: Can't instantiate abstract class Fish with abstract method speak

2.3 抽象属性:要求子类提供属性

abc 模块还支持抽象属性,可以强制子类定义类属性或通过 @property 实现的属性。

class Shape(ABC):
    @property
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)  # 正常

3. NotImplementedError 与抽象基类的关系:曾经的搭档,如今的职责分离

在早期 Python 代码或某些框架中,你会看到这样的写法:

class BaseClass(ABC):
    def process(self):
        raise NotImplementedError

这实际是两种机制的结合。由于类本身继承自 ABC,它可以拥有抽象方法;而方法体中又填入 NotImplementedError,则是在为“万一该方法被直接调用”提供一条后备的错误信息。

但严格来说,如果已经使用了 @abstractmethod 装饰器,再在方法体内写 raise NotImplementedError 是冗余的——因为抽象基类已经保证了该方法不会被直接成功调用(要么子类覆盖,要么实例化失败)。专业的做法是只保留 pass 或文档字符串。

# 推荐写法:清晰、简洁
class BaseClass(ABC):
    @abstractmethod
    def process(self):
        """处理数据,子类必须实现"""
        pass

那么 NotImplementedError 现在还能用在哪儿?它适用于非强制性的扩展点接口仅部分实现的场景。例如,一个混合类(mixin)提供了默认行为,但希望子类在必要时覆盖,若不覆盖则会触发一个明确的错误。这类场景不需要严格的抽象约束。

4. 对比与选择:一张表看懂何时用什么

场景需求 推荐方案 理由
希望子类必须实现某些方法,否则实例化时报错 abc.ABC + @abstractmethod 静态检查,最早发现错误,类型提示友好
基类中某个方法只是占位,稍后实现 raise NotImplementedError 简单,无需引入 abc 模块,适合快速原型
可选方法:子类可以实现也可以不实现 方法体为空或提供默认行为 如需要让调用者意识到该方法未被实现,可保留 NotImplementedError
需要强制子类提供特定属性 @property + @abstractmethod 唯一标准做法
需要注册虚拟子类,而不通过继承 ABCMeta.register()@register 鸭子类型与强制抽象的结合

5. 实战演练:构建一个插件式的数据加载器

下面的例子展示了如何用抽象基类设计一个可扩展的数据加载框架,同时演示 NotImplementedError 在非强制扩展点中的用法。

from abc import ABC, abstractmethod

class DataLoader(ABC):
    """抽象数据加载器,要求所有子类实现 load 方法"""
    
    @abstractmethod
    def load(self, path: str):
        """从指定路径加载数据"""
        pass
    
    def preprocess(self, data):
        """
        可选的数据预处理钩子。
        基类不做任何处理,子类可选择覆盖。
        如果直接调用基类的此方法,抛出 NotImplementedError 以提醒需要自定义。
        """
        raise NotImplementedError("如需预处理,请在子类中实现 preprocess 方法")

class CSVLoader(DataLoader):
    def load(self, path: str):
        # 模拟加载CSV
        print(f"正在从 {path} 加载CSV数据")
        return [{"row": i} for i in range(3)]
    
    def preprocess(self, data):
        # 实现预处理:将行转为大写(示例)
        return [str(row).upper() for row in data]

# 使用
loader = CSVLoader()
raw = loader.load("data.csv")
processed = loader.preprocess(raw)
print(processed)

# 如果有一个子类忘记实现 load,实例化时就会报错
class IncompleteLoader(DataLoader):
    pass

try:
    bad_loader = IncompleteLoader()
except TypeError as e:
    print(f"错误: {e}")

输出:

正在从 data.csv 加载CSV数据
["{'ROW': 0}", "{'ROW': 1}", "{'ROW': 2}"]
错误: Can't instantiate abstract class IncompleteLoader with abstract method load

在这个设计中,load@abstractmethod 严格约束,任何派生类都必须实现;而 preprocess 是一个扩展点,它不是必须的,但如果有人直接使用基类对象(不应该,因为基类是抽象类)或某个派生类没有覆盖它,调用时会得到清晰的 NotImplementedError 提示。

6. 高级技巧:自定义抽象基类与 __init_subclass__ 结合

你还可以通过 __init_subclass__ 钩子实现更精细的抽象检查,比如要求子类必须定义特定类属性。不过 abc 模块的 @abstractmethod 配合属性装饰器已经能覆盖大部分场景。当你需要跨多个方法检查一致性时,再考虑元类或 __init_subclass__

7. 总结

  • NotImplementedError 是一个运行时约定,适合快速标记未完成的方法或可选扩展点。
  • 抽象基类 (abc.ABC) 提供了编译时(实例化时)的强制保证,是构建接口、框架和大型项目的基石。
  • 在现代 Python 中,优先使用 @abstractmethod 来声明必须实现的方法;NotImplementedError 退居二线,作为补充信号。
  • 两者并非互斥,理解它们的定位能让你在“灵活约定”和“严格契约”之间自如切换。

现在,你可以根据实际需求,为你的代码选择最合适的抽象方式,写出既灵活又健壮的面向对象程序。