深入解析Python抽象基类与协议:构建健壮代码的基石

在Python编程中,当我们尝试对接口进行建模并执行子类型检查时,“抽象基类(ABCs)”和“协议”这两个概念经常被提及。本文将深入探讨这些核心概念,帮助读者理解它们的角色、差异以及它们如何在底层运作,从而为构建更健壮、更可维护的Python代码奠定坚实基础。

本文旨在构建对抽象基类和协议背后关键思想的扎实理解。我们将超越简单的用法示例,深入探索理解它们所需的基础知识引入它们的缘由以及它们底层的工作机制

本文不涉及以下内容:

  • abc 模块的全部特性。
  • Protocol 类的所有方面(例如,泛型、递归协议等)。
  • 使用ABCs或协议的性能考量。
  • 选择ABCs或协议的指导方针或最佳实践。

理解基础概念

在深入探讨抽象基类(ABCs)和协议之前,理解一些与类型理论相关的基础概念至关重要。我们将探讨名义子类型与结构子类型以及静态检查与动态检查等概念,所有这些都在ABCs和协议的工作方式中扮演着关键角色。

首先,让我们探索Python中不同形式的子类型,它们是ABCs和协议行为的基础。

子类型:核心概念

子类型关注的是一个类型何时可以替代另一个类型——具体来说,如果类型 BA 的子类型,那么在任何期望 A 的地方都可以使用 B

名义子类型与结构子类型

名义子类型

名义子类型中,类型之间通过名称建立关系。这意味着,一个类型只有在显式声明为另一个类型的子类型时(通过继承或其他类似机制),才被认为是其子类型。

结构子类型

结构子类型中,类型之间通过结构建立关系——这意味着如果一个类型拥有另一个类型的所有字段和方法,那么它就是后者的子类型,即使它没有被显式声明

考虑以下与语言无关的例子:

class Animal:
    def speak(self):
      pass
    def walk(self):
      pass

class Dog(Animal):
    def speak(self):
      print("Woof Woof")
    def walk(self):
      print("Dogo Walking")    

class Robot:
    def speak(self):
      print("Beep boop")
    def walk(self):
      print("Robo walking")

DogAnimal名义子类型,因为它通过继承显式声明了这种关系。另一方面,Robot 不是 Animal 的名义子类型,因为它没有继承自 Animal——但它一个结构子类型,因为它提供了相同的结构(即所需的方法和属性)。

在区分了名义子类型和结构子类型之后,考虑这些检查何时发生也很重要——是在运行时还是在静态分析期间。

静态类型与动态类型:检查时机

静态类型

在静态类型语言中,每个变量的类型在程序运行前就已经确定。这意味着:

  • 类型检查在编译时发生。
  • 你会在早期捕获类型错误——在它们溜进生产环境之前。
  • 代码通常更安全,但通常也更冗长。

动态类型

在动态类型语言(如Python!)中,类型在程序运行期间才确定。这意味着:

  • 无需预先声明类型。
  • 你可以更快速、更灵活地编写代码。
  • 但类型错误可能只会在运行时出现——有时会在意想不到的地方。

子类型的两个维度

当我们说 AB 的子类型时,我们实际上是在做一个取决于两个关键维度的声明:

  1. 我们谈论的是哪种类型的子类型?是名义子类型还是结构子类型?
  2. 子类型何时被强制执行或检查?是在静态时还是在运行时

请始终牢记这些问题——它们有助于澄清“AB 的子类型”实际意味着什么。

现在我们已经剖析了名义与结构以及静态与动态类型化的概念,让我们将所有这些都聚焦到Python上——一个动态类型语言,但可选地支持使用Python 3.5中引入的类型提示进行静态类型化,以提高可读性、文档化和更安全的规模化开发。

让我们详细了解Python中的动态类型和静态类型。

Python中的动态类型

让我们深入探讨Python中动态类型的三个方面:

  • 变量的动态性
  • 鸭子类型(运行时结构子类型)
  • isinstanceissubclass

变量的动态性质:变量不绑定特定类型

在Python中,变量不绑定到特定类型。这意味着你可以在程序的任何点为变量分配任何类型的值,并且其类型可以在运行时动态更改。

x = 10          # x 是一个整数
print(type(x))  # <class 'int'>

x = "Hello"     # x 现在是一个字符串
print(type(x))  # <class 'str'>

x = [1, 2, 3]   # x 现在是一个列表
print(type(x))  # <class 'list'>

在上面的例子中:

  • x 开始时是一个整数,然后被重新赋值为一个字符串,最后又被重新赋值为一个列表。
  • Python不关心 x 的类型,它的类型由其在运行时持有的值决定。

鸭子类型:运行时结构子类型

Python的动态特性与一个名为“鸭子类型”的概念密切相关。鸭子类型是一种类型风格,其中对象的特定操作的适用性由是否存在某些方法或属性来确定,而不是由其实际类型或类继承来确定。

它通常用短语来描述:“如果它走起来像鸭子,叫起来像鸭子,那么它就一定是鸭子。”

鸭子类型本质上是运行时结构子类型。结构子类型意味着如果一个类型具有相同的结构(即相同的方法和属性集),则认为它与另一个类型兼容。由于这种兼容性在Python中是在运行时检查的,因此它是动态/运行时结构子类型

考虑以下示例:

class Duck:
    def quack(self):
        print("Quack!")    
    def fly(self):
        print("Duck flying")

class ToyDuck:
    def quack(self):
        print("Squeak!")    
    def fly(self):
        print("Toy duck cannot fly")

class Bird:
    def fly(self):
        print("Bird flying")

def make_it_quack(obj):
    obj.quack()  # 我们只关心对象是否有一个 quack() 方法

def make_it_fly(obj):
    obj.fly()    # 我们只关心对象是否有一个 fly() 方法

my_duck = Duck()
my_toy_duck = ToyDuck()
my_bird = Bird()

make_it_quack(my_duck)
make_it_quack(my_toy_duck)
# make_it_quack(my_bird) # 错误: 'Bird' 对象没有 'quack' 属性

make_it_fly(my_duck)
make_it_fly(my_toy_duck)
make_it_fly(my_bird)

解释:

  • 函数 make_it_quack() 不关心对象是否是 Duck。它只关心对象是否有一个 quack() 方法。
  • DuckToyDuck 都有一个 quack() 方法,因此这两个类的实例都可以传递给 make_it_quack() 函数而不会出现任何类型错误。
  • 然而,my_bird 没有 quack() 方法,因此将其传递给 make_it_quack() 将在运行时导致 AttributeError
  • make_it_fly() 函数适用于任何具有 fly 方法的对象。
  • 这表明Python的鸭子类型侧重于对象的行为(它是否具有所需的方法)而不是其类型或继承关系
  • 这是结构子类型,因为我们只关心对象的结构。这是动态的,因为对 quack() 方法存在的检查发生在调用 make_it_quack() 函数时。

尽管鸭子类型不受ABCs或协议的影响,但它自然地引出了结构子类型的思想——我们在运行时使用的相同直觉正是协议试图为静态类型检查形式化的内容。

issubclass():运行时子类型检查

Python提供了内置的 issubclass() 函数,用于在运行时检查一个类是否被认为是另一个类的子类型。

现在,issubclass()只检查名义子类型只检查结构子类型,还是两者都检查——答案可能因抽象基类(ABCs)和协议的使用而异。我们将在讨论ABCs和协议时再回过头来讨论这一点。

Python中的静态类型

虽然Python本质上是一种动态类型语言,但它通过 PEP 484 中引入的类型提示支持可选的静态类型。这意味着你可以用类型注释你的代码,并使用 mypypyright 或现代IDE等工具来执行静态类型检查——即在不运行代码的情况下检查你的类型。这使得更好的工具、文档和某些错误的早期检测成为可能——同时保持了Python在运行时的灵活性。

def greet(name: str) -> str:
    return "Hello, " + name

greet("Alice")   # ✅ OK
greet(42)        # ❌ mypy: Argument 1 to "greet" has incompatible type "int"; expected "str"

在静态类型检查期间,成为子类型意味着什么?

当我们注释一个函数以期望某种类型——比如 Animal——一个像 mypy 这样的静态类型检查器将接受它认为是 Animal 子类型的任何参数。

def make_it_speak(a: Animal): ...

什么_算作_ Animal 的子类型?

类型检查器会只接受显式继承自 Animal 的类(即名义子类型)吗?还是也会接受仅与其结构匹配的类型(即结构子类型)?答案取决于 Animal 是如何定义的——它是一个抽象基类(ABC)、一个协议,还是一个普通类。我们将在讨论ABC和协议时回答这个问题。

回顾:两个子类型问题

我们现在已经讨论了通过 issubclass() 进行的运行时子类型化和通过类型提示和静态类型检查器进行的静态子类型化。因此,在这两种情况下,我们都回答了关于子类型何时被检查或强制执行的第二个问题。在 issubclass 的情况下,它是在运行时,在类型提示的情况下,它是由类型检查器在静态时。但第一个问题仍然存在:

成为子类型到底意味着什么?它只是名义上的还是结构上的,抑或是两者兼而有之?

一个快速插曲:理解类如何使用元类方法

在我们深入研究ABCs以及它们如何与Python的子类型检查挂钩之前,理解Python对象模型中一个微妙但强大的思想很重要:

在Python中,一切都是某个东西的实例——甚至类本身也是如此。

  • 一个普通的对象(例如 c = Circle())是Circle类的一个实例。
  • 一个本身(例如Circle)是一个元类的实例。

默认情况下,所有类都是内置 type 元类的实例。但你可以通过定义自己的元类来定制行为。

就像一个类的实例可以访问其类中定义的方法和属性一样,一个可以访问其元类中定义的方法。

为什么?——因为一个类本身就是元类的一个实例

记住:类也是对象——它们的行为来自于它们的元类!

# 定义一个向类添加行为的元类
class ShapeMeta(type):
    def describe(cls):
        print(f"{cls.__name__} is a shape.")

# 定义一个使用该元类的类
class Circle(metaclass=ShapeMeta):
    def area(self, radius):
        return 3.14 * radius * radius

# 实例调用实例方法
c = Circle()
print(c.area(5))  # 输出: 78.5

# 类调用其元类中的方法
Circle.describe()  # 输出: Circle is a shape.

注意:你不需要掌握元类才能使用ABCs——但记住这种心智模型将使我们深入探讨内部机制时更容易理解。

既然我们已经看到类本身如何是元类的实例——以及元类如何控制类级别行为——我们已经准备好深入了解 abc 模块。

abc 模块引入了什么?

Python的 abc 模块(抽象基类)为执行运行时子类型检查和定义形式接口契约提供了基础。它通过以下两个关键能力引入了这些功能:

  1. ABC 基类(通过 ABCMeta 元类)
    这支持使用 issubclass()isinstance() 进行更灵活的子类型检查。在底层,这涉及在 ABCMeta 中定义的方法,如 __subclasscheck____instancecheck__register,以及在 Object 中定义的 __subclasshook__——我们将在下一节中探讨。
  2. @abstractmethod@abstractproperty 装饰器
    这些允许你定义必须由子类实现的方法和属性,有助于定义类似接口的契约。

简而言之:

第一部分是关于在运行时检查类A是否是类B的子类

另一部分是关于强制子类必须实现哪些方法和属性

在我们深入研究 ABCMeta 之前,让我们看看如何在Python中定义一个抽象基类。

在Python中定义抽象基类

在Python中,你可以通过两种方式定义抽象基类(ABC):

  • 直接使用 ABCMeta 作为元类

from abc import ABCMeta

class MyBase(metaclass=ABCMeta):
    pass
  • 使用 ABC 助手类,这是更常见和方便的方法。

from abc import ABC

class MyBase(ABC):
    pass

在底层,ABC 只是一个薄薄的包装器,它将 ABCMeta 设置为其元类。所以当你从 ABC 继承时,你实际上是在说,“让我的类使用ABC机制。” 这就是 ABCabc 模块中定义的方式。

class ABC(metaclass=ABCMeta):
    """Helper class that provides a standard way to create an ABC using
    inheritance.
    """
    pass

使用 ABCMeta 进行运行时子类型检查

abc 模块引入后 issubclass() 行为的演变

abc 模块引入之前,Python的 issubclass(A,B) 严格依赖于名义子类型——只有当类 A 明确继承自类 B(直接或间接)时才返回 True

但随着 abc 模块的出现,这种行为变得更加灵活。现在,当你调用 issubclass(A,B) 时,Python会检查 B 的元类(不一定是 ABCMeta)是否定义了一个名为 __subclasscheck__ 的特殊方法。如果定义了,Python会将决定权委托给该方法——允许类自定义运行时如何确定子类型关系。如果不存在这样的方法,Python会回退到原始的名义检查。下面的代码示例清楚地说明了这一点。

class BaseAllMeta(type):
    def __subclasscheck__(self, subclass):
        return True

class BaseNoneMeta(type):
    def __subclasscheck__(self, subclass):
        return False

class BaseAll(metaclass=BaseAllMeta):
    pass

class BaseNone(metaclass=BaseNoneMeta):
    pass

class A:
    pass

class B(BaseNone):
    pass

print(issubclass(A, BaseAll))   # ✅ True — 调用 BaseAll.__subclasscheck__(A)
print(issubclass(B, BaseNone))  # ❌ False — 调用 BaseNone.__subclasscheck__(B)

注意 issubclass(A, BaseAll) 返回 True,即使它没有继承BaseAll;而 issubclass(B, BaseNone) 返回 False,即使 B 继承BaseNone,这表明 __subclasscheck__ 如何覆盖 issubclass 的默认行为。

注意:尽管 BaseAll 没有直接定义 __subclasscheck__,但它的元类定义了。因此 BaseAll.__subclasscheck__(A) 之所以有效,是因为 BaseAll 将该方法调用委托给 BaseAllMeta,就像实例将方法查找推迟到其类一样(BaseAll 实际上是 BaseAllMeta 的一个实例)。

这正是我们之前为什么要绕道去了解元类的原因——为了建立正确的心理模型来理解当Python使用ABCs调用 issubclass() 时发生了什么。

isinstanceissubclass 之间的相似性

就像 issubclass(A, B)B 的元类有一个自定义 __subclasscheck__ 时委托给 B.__subclasscheck__(A) 一样,函数 isinstance(obj, cls) 类似地在 cls 的元类定义了一个自定义 __instancecheck__ 方法时调用 cls.__instancecheck__(obj)

由于这种行为与 issubclass__subclasscheck__ 的行为相似,我将跳过 isinstance__instancecheck__ 的进一步细节,因为这些概念从根本上是相同的。

在前一节的基础上,你可能会考虑定义一个带有自己 __subclasscheck__ 实现的自定义元类,以修改运行时子类型检查,然后将该元类应用于所有类。然而,ABCMeta 已经提供了高度灵活的 __subclasscheck__ 实现,使开发人员能够以更受控的方式自定义子类型检查行为。接下来的章节将更详细地讨论**ABCMeta__subclasscheck__**。

ABCMeta.__subclasscheck__ 的工作原理

让我们仔细看看 ABCMeta.__subclasscheck__ 的实际实现。这是一个简化版本(不包括内部缓存逻辑):

def __subclasscheck__(cls, subclass):
    """Override for issubclass(subclass, cls)."""    # 步骤 1: 委托给 __subclasshook__
    ok = cls.__subclasshook__(subclass)
    if ok is not NotImplemented:
        assert isinstance(ok, bool)
        return ok

    # 步骤 2: 检查直接继承 (名义)
    if cls in getattr(subclass, '__mro__', ()):
        return True

    # 步骤 3: 检查子类是否已注册 (虚拟子类)
    for rcls in cls._abc_registry:
        if issubclass(subclass, rcls):
            return True

    # 步骤 4: 递归检查 ABC 的子类
    for scls in cls.__subclasses__():
        if issubclass(subclass, scls):
            return True

    return False

让我们一步步分解:

1. __subclasshook__:自定义子类型逻辑

第一个检查委托给 cls.__subclasshook__,如果ABC定义了它。这允许任何自定义子类型逻辑覆盖所有其他逻辑。如果它返回 TrueFalse,则使用该结果。如果它返回 NotImplemented,则检查继续。

我们将在下一节中详细探讨这种强大的机制。

2. 直接子类检查(名义子类型)

如果钩子没有决定结果(返回 NotImplemented),Python 会回退到检查 cls 是否出现在 subclass__mro__(方法解析顺序)中。这是通常的名义子类型机制:它检查 subclass 是否直接或间接继承自 cls

这是ABC出现之前 issubclass() 的传统行为。

3. 通过 register() 进行虚拟子类检查

如果 SomeClass 之前使用 cls.register(SomeClass) 注册到 cls,此步骤会检查 SomeClass 是否已显式注册为 cls 的虚拟子类。本质上,SomeClass 被视为 cls 的子类,因为它已手动注册,即使它没有直接继承自 cls

就像 __subclasshook__ 一样,register() 方法是另一种在不改变类定义的情况下自定义子类行为的关键方式——我们将在下一节中彻底讨论它。

4. 递归子类探索

如果前面的检查都没有成功,ABCMeta 会使用 cls.__subclasses__() 探索 cls 的所有直接子类,并对它们递归应用 issubclass()

但等等——第二步不是已经检查了子类关系吗?

是的,但仅通过标准方法解析顺序 (__mro__)。第二步将捕获任何名义上直接或间接继承自 ABC 的类。然而,第四步的存在是为了捕获间接或动态声明的子类化——特别是那些涉及 register() 或自定义 __subclasshook__() 逻辑的子类化——即使它们在子类树中深层。

考虑这个例子:

from abc import ABC

class Base(ABC): pass

class Mid: pass
Base.register(Mid)

class Leaf(Mid): pass

issubclass(Leaf, Base)  # True

事情是这样展开的:

  • 步骤2失败:Base 不在 Leaf.__mro__ 中。
  • 步骤3失败,因为 Leaf 没有通过 register 直接注册到 Base
  • 步骤4成功:它检查 Base 的子类,找到 Mid,然后调用 issubclass(Leaf, Mid)——返回 True,满足虚拟子类关系。

这最后一步对于捕获不是通过继承,而是通过显式注册或通过 __subclasshook__ 产生的关系至关重要——即使它们深入多个层级。

它确保了Python的子类型检查保持一致和灵活,即使在复杂或非传统层次结构中也是如此。

注意: 如果这个例子现在感觉有点微妙,别担心——一旦你阅读了下一节关于 register() 的内容,再回来重新阅读这个步骤。它就会清晰起来。

引入 register():手动声明子类型

在上一节中,我们看到 __subclasscheck__ 包含一个特定步骤(步骤3),它通过 _abc_registry 查找已注册的虚拟子类。这正是 register() 方法的作用。

register() 方法定义在 ABCMeta 中,它允许你显式声明一个给定的类应该被视为一个ABC的虚拟子类——无需继承

from abc import ABC

class MyABC(ABC):
    pass

class UnrelatedClass:
    pass

MyABC.register(UnrelatedClass)

print(issubclass(UnrelatedClass, MyABC))  # True

尽管 UnrelatedClass 继承自 MyABC,但在注册后 issubclass() 仍返回 True。这是因为 register() 直接更新了 __subclasscheck__ 在子类型检查期间查询的注册表。

当处理第三方代码或内置类型时,此功能特别有用,因为这些类型你无法直接子类化。如果此类符合你关心的接口,register() 允许你将其清晰且显式地集成到你的类型层次结构中。

到现在,你已经知道 __subclasscheck__ 如何处理子类型关系。当你调用:

MyABC.register(UnrelatedClass)

你正在将 UnrelatedClass 添加到 MyABC 的内部 _abc_registry。稍后,在调用 issubclass(UnrelatedClass, MyABC) 期间,__subclasscheck__ 的步骤3生效——并返回 True,因为该类之前已注册。

register() 方法定义在 ABCMeta 中,这意味着它适用于任何元类是(或派生自)ABCMeta 的类

使用 __subclasshook__ 自定义子类型逻辑

在我们深入探讨 __subclasscheck__ 时,我们看到确定类 A 是否是类 B 的子类(其中类 B 是任何元类是/派生自 ABCMeta 的类)的第一步是调用类 B__subclasshook__() 方法。

这就是Python提供了一个强大的扩展点的地方:__subclasshook__() 类方法允许你覆盖和定义抽象基类的子类型意味着什么,完全独立于名义继承。

让我们来看看 __subclasshook__ 的特殊之处:

1. 它定义在ABC本身,而不是元类中

register()(定义在 ABCMeta 中)不同,__subclasshook__() 方法定义在你的ABC类中——就像任何其他方法一样。这是一个最小的例子:

from abc import ABC

class MyABC(ABC):
    @classmethod
    def __subclasshook__(cls, subclass):
        if hasattr(subclass, 'quack'):
            return True
        return NotImplemented

在这种情况下,任何具有 quack 方法的类都将被视为 MyABC 的子类,即使它没有直接继承它。

2. 它必须返回 TrueFalseNotImplemented

  • 如果它返回 True,则 issubclass(A, B) 返回 True
  • 如果它返回 False,则 issubclass(A, B) 返回 False
  • 如果它返回 NotImplemented,Python 会回退到 __subclasscheck__ 内部的正常逻辑。

如果你没有在你的ABC中定义 __subclasshook__() 方法,则使用来自 object 的默认实现,它总是返回 NotImplemented

这意味着:如果你不覆盖 __subclasshook__,则什么都不会改变——issubclass() 只使用标准检查(MRO、注册表等)。

3. 它是运行时结构子类型的钩子

虽然 register() 允许你手动声明子类型关系,但 __subclasshook__() 能够基于类结构进行自动运行时检查

当你希望你的ABC匹配任何符合特定接口的类,而不管其继承关系如何时,这尤其有用。例如:

class Duck:
    def quack(self): print("Quack!")

class MyABC(ABC):
    @classmethod
    def __subclasshook__(cls, subclass):
        if hasattr(subclass, 'quack'):
            return True
        return NotImplemented

print(issubclass(Duck, MyABC))  # True, 即使没有继承

尽管 Duck 没有继承自 MyABC,但由于它提供了预期的方法,因此被认为是子类。这是运行时结构子类型的实际应用——在精神上类似于“鸭子类型”,但现在已集成到Python的形式类型系统中。

总结

  • __subclasshook__ 赋予你一种灵活的机制来定义子类的含义——动态地结构地
  • 它位于ABC中,而不是元类中。
  • 它应该返回 TrueFalseNotImplemented
  • 它在 __subclasscheck__ 的第一步中扮演着核心角色。
  • 如果省略,则使用默认的 object.__subclasshook__,Python 会继续执行名义或注册的子类检查。

这种机制——经过深思熟虑的使用——可以使你的抽象基类既强大又直观,允许它们接受任何“看起来正确”的对象,而无需强制执行严格的继承层次结构。

总结:issubclass() 实际检查了什么

在前面的章节中,我们提出了一个基本问题:

当你编写 issubclass(A, B) 时,在运行时实际检查了哪种子类型——名义子类型、结构子类型,还是两者兼而有之?

现在我们已经探讨了 __subclasscheck__()register()__subclasshook__(),我们终于可以清晰地回答这个问题:

默认情况下,issubclass() 只检查名义子类型——即类 A 是否(直接或间接)继承自类 B

然而,当你使用抽象基类(ABC)——它们依赖于 ABCMeta——Python打开了通往更灵活行为的大门:

  • **register()** 允许你手动声明一个类为ABC的虚拟子类,即使它没有继承自它。这是显式的虚拟子类型。
  • **__subclasshook__()** 允许在运行时根据你自己的逻辑启用隐式结构子类型——通常依赖于接口一致性(例如,“它有方法X吗?”)。

这两种机制扩展了传统的名义模型,并有效地允许 issubclass() 表现为混合模式——检查名义子类型结构子类型,这取决于ABC的设计方式。

但这里有一个关键点:

所有这些动态行为——包括 register()__subclasshook__()——都是通过 __subclasscheck__() 方法实现的,因此严格发生在运行时。这意味着:

这些功能不影响静态类型检查器,如 mypypyrightpytype。从类型角度来看,使用 register()__subclasshook__() 只影响运行时语义——它们重新定义在类型注释或静态分析中类作为子类的含义。

现在我们已经探讨了Python如何使用 issubclass() 执行动态类型检查,让我们将重点转移到如何使用 @abstractmethod 装饰器在类定义时强制执行类似接口的契约

通过 @abstractmethod 装饰器强制执行接口契约

在许多面向对象的语言中,接口充当具体类必须遵守的形式契约——指定子类必须实现哪些方法,而不规定它们如何实现。Python没有Java或C#意义上的接口,但它提供了功能上相似的东西,使用 @abstractmethod 装饰器和 abc 模块。

@abstractmethod 装饰器允许你在Python中定义此类契约。当在元类是(或派生自)ABCMeta 的类中使用时,它将一个方法标记为抽象——这意味着子类必须覆盖它才能实例化。

契约的强制执行方式如下:

  • 如果子类没有覆盖所有抽象方法,尝试实例化它将引发 TypeError
  • 如果子类只覆盖了部分抽象方法,它本身仍然是抽象的——并且仍然无法实例化。
  • 只有当所有抽象方法都实现后,子类才成为具体类并可以正常实例化。

这种机制确保了关键方法始终在子类中定义,强制执行了一种结构纪律——即使Python仍然是一种动态类型语言。

简而言之:@abstractmethod 不仅仅是“你应该实现这个”的文档;它在运行时保证了你必须这样做

这是一个代码示例:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass    
    @abstractmethod
    def move(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"    
    def move(self):
        return "Runs"

class Cat(Animal):
    def speak(self):
        return "Meow"
        # 注意: move() 没有实现

# ✅ Dog 实现了所有抽象方法
dog = Dog()
print(dog.speak())  # Woof!

# ❌ Cat 缺少一个抽象方法,因此无法实例化
cat = Cat()  # TypeError: Cannot instantiate abstract class Cat with abstract method move

使用 @abstractmethod 的重要注意事项

@abstractmethod 装饰器是Python中强制执行类似接口契约的强大工具,但它带有一些重要的限制和注意事项,开发人员应该牢记。

1. 必须定义在元类是(或派生自)ABCMeta 的类中

你应该只在元类是(或元类派生自)ABCMeta 的类体内使用 @abstractmethod。将其定义在类之外或在常规(非ABC)类中没有效果——它会默默地未能强制执行任何抽象。

from abc import ABC, abstractmethod

class Animal:
    @abstractmethod  # ❌ 这将没有任何效果,因为 Animal 不继承自 ABC
    def speak(self):
        pass

class Dog(Animal):
    def bark(self):
        print('Woof')

dog = Dog()  # ✅ 没有错误——speak() 从未被强制为抽象
dog.bark()

2. 抽象方法可以有实现

一个抽象方法可以包含一个方法体。这允许子类使用 super() 调用它,同时仍然要求它们显式地覆盖该方法。

from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def process(self):
        print("Base logic")

class Derived(Base):
    def process(self):
        super().process()
        print("Custom logic")

d = Derived()
d.process()
# 输出:
# Base logic
# Custom logic

3. 只影响继承的子类

只有继承自抽象基类的子类才需要实现抽象方法。

如果你使用 register() 声明了一个虚拟子类,则不会强制执行抽象方法——这意味着注册是选择性的,并且基于信任。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Robot:
    pass

# 将 Robot 注册为 Animal 的虚拟子类
Animal.register(Robot)

r = Robot()  # ✅ 没有错误——即使没有实现 `speak()`
print(isinstance(r, Animal))  # ✅ True——因为 Robot 已注册

4. 检查纯粹是存在性的——不感知签名

当子类继承自抽象基类时,它必须覆盖所有 @abstractmethod 才能实例化。然而,Python 的强制执行极其宽松

  • 它不检查覆盖属性是否是一个方法。
  • 它不验证方法的签名(名称、参数、返回类型)。
  • 它甚至不要求方法是可调用的。

检查只验证子类是否定义了某个同名的东西——无论它实际上是什么。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self, volume):
        pass

class Dog(Animal):
    def speak(self):  # 签名不匹配——缺少 'volume' 参数
        return "Woof"

class Cat(Animal):
    speak = "Meow"  # 不是方法——只是一个字符串

# 运行时结果
print(Dog().speak())     # 有效,打印 "Woof",尽管签名不匹配
print(Cat().speak)       # 有效,打印 "Meow",但 speak 不可调用

上面的代码运行没有错误,甚至像 mypy 这样的静态类型检查器也不会报告任何问题。如果你希望 mypy 报告问题,你必须在 @abstractmethod 中添加类型提示,如下所示。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self, volume: int) -> str:
        pass

class Dog(Animal):
    def speak(self) -> str:  # 缺少参数 'volume'
        return "Woof"

class Cat(Animal):
    speak: str = "Meow"  # 不可调用

# 运行时结果
print(Dog().speak())     # 有效,打印 "Woof",尽管签名不匹配
print(Cat().speak)       # 有效,打印 "Meow",但 speak 不可调用

如果子类没有实现所有 @abstractmethod,Python 会在运行时引发 TypeError——像 mypy 这样的工具也会报告此问题。然而,如果子类提供了一个签名不匹配的方法或一个非可调用对象来代替抽象方法,Python 仍然允许实例化。mypy 是否捕获此问题取决于是否为方法提供了类型注解。

到目前为止,我们已经了解了ABCs可以(和不能)做什么。现在让我们转向协议,它们弥补了ABCs留下的静态类型化空白。

从ABCs到协议:拥抱静态结构类型

为什么引入协议?

如前所述,在 abc 模块引入之前,issubclass(A, B) 只检查 A 是否是 B名义子类型——即它是否显式继承自 Babc 模块通过引入 ABCMeta__subclasshook__ 方法扩展了这一点,允许在运行时进行结构子类型检查。

然而,这种通过 __subclasshook__ 进行的结构检查只发生在运行时。它对 mypy静态类型检查器没有影响

这是一个例子:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self) -> None:
        pass    
    @classmethod
    def __subclasshook__(cls, subclass):
        if hasattr(subclass, "speak") and callable(subclass.speak):
            return True
        return NotImplemented

class Dog:
    def speak(self) -> None:
        print("Woof!")

# 运行时检查通过
print(issubclass(Dog, Animal))  # True

# 静态检查失败 (mypy 会抱怨)
def make_it_speak(animal: Animal):
    animal.speak()

make_it_speak(Dog())  # mypy: Argument of type "Dog" is incompatible

所以 mypy 在静态时无法识别 DogAnimal 的结构子类型。因此,我们仍然没有办法在静态时检查结构子类型。这正是协议旨在填补的空白,即协议的引入是为了支持静态时的结构子类型,也称为静态鸭子类型

定义协议类

在Python中,你可以通过继承 typing.Protocol 来定义一个协议。这允许你描述一组方法和属性,一个类型必须实现它们才能在静态时被认为是结构兼容的。

如果一个类定义了该协议指定的所有方法和属性,并具有兼容的类型,则称该类实现了该协议。

换句话说,如果类 A 实现了协议 B 所需的所有方法和属性,那么 A 在静态时就是 B结构子类型

让我们从最基本的形式开始:一个只有方法的协议。

1. 仅包含方法的协议

from typing import Protocol

class Animal(Protocol):
    def speak(self) -> None:
        ...

class Dog:
    def speak(self) -> None:
        print("Woof!")

def make_it_speak(animal: Animal):
    animal.speak()

make_it_speak(Dog())  # mypy 现在很满意

尽管 Dog 没有继承自 Animal,但它仍然被认为实现了 Animal 协议,因为它提供了所有必需的方法。换句话说,DogAnimal 的静态结构子类型,并且像 mypy 这样的类型检查器会识别这一点。这是结构子类型的实际应用——也称为静态鸭子类型

2. 包含方法和属性的协议

from typing import Protocol

class Pet(Protocol):
    name: str    
    def speak(self) -> None:
        ...

class Cat:
    def __init__(self, name: str):
        self.name = name    
    def speak(self) -> None:
        print("Meow!")

def introduce(pet: Pet):
    print(f"This is {pet.name}.")
    pet.speak()

introduce(Cat("Whiskers"))  # ✅ 符合 Pet 协议

同样,Cat 不继承自 Pet,但它实现了所有必需的方法和属性,因此被静态类型检查器认为是有效的 Pet

注意: 对于一个类要被认为是协议的结构子类型,像 mypy 这样的静态类型检查器不仅检查方法或属性的存在——它们还确保签名匹配,包括参数类型、返回类型和方法种类。

一旦我们了解了协议的定义方式以及它们如何实现静态时的结构子类型,自然就会出现两个问题。

1. 协议可以与名义类型一起使用吗?

也就是说,如果一个类显式子类化一个协议会发生什么?静态类型检查器会区别对待它吗?Python的运行时行为会与常规类继承有什么不同吗?

2. 协议的运行时结构子类型支持如何?

我们已经看到协议如何启用静态鸭子类型。但是我们也可以用它们进行运行时鸭子类型吗?换句话说,我们可以使用 issubclass()isinstance() 来检查一个类是否符合协议吗?

在我们回答这两个问题之前,理解一个关键事实很重要:在运行时,一个 Protocol 类是 ABCMeta 的实例,就像一个抽象基类(ABC)一样。因此,ABCs可用的运行时行为和机制——例如 @abstractmethod__subclasshook__——对协议也可用。我们将在下一节讨论 @abstractmethod,其中我们讨论协议的名义子类型。稍后我们将在讨论协议的运行时结构子类型支持时讨论 __subclasshook__

协议可以与名义类型一起使用吗?

可以。为了显式声明一个类实现了给定的协议,协议类可以作为一个常规的基类使用。然而,对于像 mypy 这样的静态类型检查器来说,不需要显式子类化来验证兼容性——它们依赖于结构子类型并可以自动推断关系。

协议成员的默认实现呢?

继承的语义保持不变。

  • 如果一个类显式子类化一个协议,它可以继承并使用协议基类中定义的协议成员的默认实现
  • 如果该类只是隐式符合协议(即没有显式子类化),它必须重新实现所有协议成员——包括协议中具有默认实现的任何成员——才能在静态时满足结构子类型。在这种情况下不继承默认实现。

协议类中的 @abstractmethod 呢?

@abstractmethod 的语义得以保留,就像在ABCs中一样。

  • 如果一个类显式子类化一个协议,它成为一个名义子类型,并且必须实现所有抽象方法才能实例化——否则,Python将在运行时引发 TypeError
  • 如果一个类显式子类化一个协议,它必须实现所有协议成员——包括抽象和非抽象的——才能被静态类型检查器认为是有效的结构子类型。

from typing import Protocol
from abc import abstractmethod

class PColor(Protocol):
    @abstractmethod
    def draw(self, i: int) -> str:
        ...    
    def complex_method(self) -> str:
        print("Inside PColor's Complex Method")
        return "PColor's complex method"

# ✅ 显式子类化协议——获得对默认方法的访问
class PColorExplicitImpl(PColor):
    def draw(self, i: int) -> str:
        print("Inside PColorExplicitImpl's draw")
        super().complex_method()  # ✅ 有效,因为它是显式子类
        return "PColorExplicitImpl's Draw Method"    # 不需要实现 complex_method

# ✅ 结构上实现协议——必须重新实现所有方法
class PColorImplicitImpl:
    def draw(self, i: int) -> str:
        print("Inside PColorImplicitImpl's draw")
        # super().complex_method()  # ❌ 错误: 不允许,PColor 不是基类
        return "PColorImplicitImpl's Draw Method"    
    def complex_method(self) -> str:  # ✅ 必须显式实现
        print("Inside PColorImplicitImpl's Complex Method")
        return "PColorImplicitImpl's complex method"

def represent(c: PColor) -> None:
    c.draw(10)
    c.complex_method()
    print("-" * 40)

# ✅ 结构上都满足 PColor 协议
explicit_color = PColorExplicitImpl()
implicit_color = PColorImplicitImpl()

represent(explicit_color)
represent(implicit_color)

**PColorExplicitImpl** 显式子类化 PColor

  • 可以通过 super() 访问 complex_method(),因为它继承自协议。
  • 不需要实现 complex_method,因为它继承了默认实现。

**PColorImplicitImpl** 子类化 PColor

  • 不能使用 super() 访问 complex_method——它不在类层次结构中。
  • 必须实现 draw(抽象)和 complex_method(非抽象)才能在结构上满足协议。

协议能否支持运行时结构子类型?

如前所述,issubclass() 是在运行时检查一个类是否是另一个类的子类型的内置方法。当你调用 issubclass(A, B) 时,Python 会内部调用 B.__subclasscheck__(A)前提是 B 的元类定义了这个方法。由于 ProtocolABCMeta 的实例,并且 ABCMeta 定义了 __subclasscheck__,所有协议类都继承了这种机制。这意味着协议在技术上能够通过 __subclasshook__ 支持运行时结构子类型。

自然地,人们可能会想:“为了让协议在运行时实现结构子类型,我应该覆盖 __subclasshook__。”

虽然这在技术上是可行的,但Python提供了更清晰、标准化的方法,使用 @runtime_checkable 装饰器。

注意: issubclass(SomeClass, MyProtocol) 只有在 MyProtocol@runtime_checkable 装饰时才有效。然而,@runtime_checkable 只检查属性/方法的存在——它验证方法签名。如果你需要自定义且更严格的运行时子类型检查,你可以在你的协议类中覆盖 __subclasshook__ 来实现你自己的逻辑。实际上,@runtime_checkable 内部依赖于 __subclasshook__,因此如果你覆盖它,你的自定义逻辑将在 issubclass() 检查期间使用。

from typing import Protocol, runtime_checkable

class Bird(Protocol):
    def fly(self, i: int) -> None: ...

@runtime_checkable
class Animal(Protocol):
    def walk(self, i: int) -> None: ...

class Dog:
    def walk(self, i: int) -> None:
        print("Walking dog")

class BadDog:
    def walk(self) -> None:  # 签名错误!
        print("BadDog walking")

class Parrot:
    def fly(self, i: int) -> None:
        print("Parrot flying")

print(issubclass(Dog, Animal))      # ✅ True: 方法存在且名称正确
print(issubclass(BadDog, Animal))   # ✅ True: @runtime_checkable 只检查存在性
# print(issubclass(Parrot, Bird))   # ❌ TypeError: Bird 不是运行时可检查的

ABCs和协议的实际应用:collections.abc.Hashable

让我们看一个Python标准库中ABCs和协议的用例示例:collections.abc.Hashable

静态类型检查器(如mypy)在模块有可用的存根文件(.pyi)时会使用它们。这些存根描述了标准库和第三方包的类型和接口。当存根存在时,类型检查器会使用它们而不是实际的运行时实现进行静态分析。如果没有定义存根,类型检查器会回退到分析实际的类定义。对于标准库和许多流行的第三方包,这些存根维护在 typeshed 中。

collections.abc 中,Hashable 的实现如下:

from abc import ABCMeta, abstractmethod

class Hashable(metaclass=ABCMeta):
    __slots__ = ()    
    @abstractmethod
    def __hash__(self):
        return 0    
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

这里,__subclasshook__ 检查一个类是否实现了 __hash__。因此,任何实现了 __hash__ 方法的类在运行时都将被视为 Hashable 的子类

class MyHashable:
    def __hash__(self) -> int:
        return 10

print(issubclass(MyHashable, Hashable))  # True

然而,正如我们所讨论的,__subclasshook__ 只在运行时工作。因此,理论上,这种行为不应该适用于静态类型检查时。例如,这个函数应该引发一个类型检查器错误,因为 MyHashable 没有显式继承Hashable,而且 Hashable 也不是作为协议定义的。

def do_hashing(h: Hashable) -> None:
    pass

h = MyHashable()
do_hashing(h)

但是它没有引发错误!为什么?

这是因为类型存根(在typeshed中)为静态类型检查以不同方式定义了 Hashable

from typing import Protocol, runtime_checkable
from abc import ABCMeta, abstractmethod

@runtime_checkable
class Hashable(Protocol, metaclass=ABCMeta):
    @abstractmethod
    def __hash__(self) -> int: ...

这里,Hashable 是一个使用结构子类型进行静态分析的协议——这意味着任何具有 __hash__ 方法的类型都被 mypy 等类型检查器视为 Hashable

我们已经涵盖了许多内容。让我们总结一下我们对Python中ABCs和协议的探索中的主要收获。

总结

在确定 A 是否是 B 的子类(或子类型)时,从两个维度思考会有所帮助:

  • 名义与结构
  • 静态与动态

子类型检查

在运行时

  • issubclass(A, B) 用于确定 A 是否是 B 的子类。
  • abc 模块之前,这种检查只支持名义子类型——即它只适用于显式继承自 B 的类。
  • 随着 abc 的引入,使用 __subclasscheck____subclasshook__ 实现了运行时结构子类型

在静态时

  • mypy 这样的静态类型检查器最初只支持名义子类型
  • 随着 Protocol 的添加,静态结构子类型成为可能——允许类型检查器仅基于结构验证兼容性,而无需继承。

下图描述了静态/动态和名义/结构维度上的子类型检查。

接口契约强制

使用ABCs

  • 你可以使用 @abstractmethod 定义类似接口的契约。
  • 运行时,这些检查是存在性的——它们只确保方法存在,而不检查它的外观。
  • 通过向抽象方法添加类型注释,你可以让静态检查器执行感知签名的检查。

使用协议

  • 你可以结构化地强制执行契约——任何实现了所需方法且签名匹配的类都会被接受,即使它没有继承自协议。

from typing import Protocol

class SupportsClose(Protocol):
    def close(self): ...

class File:
    def open(self):
        print("Opening file")

def close_resource(resource: SupportsClose):
    resource.close()

close_resource(File())  # 错误: 'File' 未实现 'close'

在上面的例子中,File 的实例只有在它定义了 close 方法时才能传递给 close_resource。这说明了协议如何间接地强制执行契约——基于方法的存在和签名,而不是继承。

好了,如果你坚持读到这里,给自己一个掌声。我希望这篇文章能帮助你澄清Python中ABCs和协议的一些基础概念。如果你发现任何错误,有问题,或者想分享不同的观点,请随时留言——我很高兴听到你的想法!