深入解析Python抽象基类与协议:构建健壮代码的基石
在Python编程中,当我们尝试对接口进行建模并执行子类型检查时,“抽象基类(ABCs)”和“协议”这两个概念经常被提及。本文将深入探讨这些核心概念,帮助读者理解它们的角色、差异以及它们如何在底层运作,从而为构建更健壮、更可维护的Python代码奠定坚实基础。
本文旨在构建对抽象基类和协议背后关键思想的扎实理解。我们将超越简单的用法示例,深入探索理解它们所需的基础知识、引入它们的缘由以及它们底层的工作机制。
本文不涉及以下内容:
-
abc
模块的全部特性。 -
Protocol
类的所有方面(例如,泛型、递归协议等)。 -
使用ABCs或协议的性能考量。 -
选择ABCs或协议的指导方针或最佳实践。
理解基础概念
在深入探讨抽象基类(ABCs)和协议之前,理解一些与类型理论相关的基础概念至关重要。我们将探讨名义子类型与结构子类型以及静态检查与动态检查等概念,所有这些都在ABCs和协议的工作方式中扮演着关键角色。
首先,让我们探索Python中不同形式的子类型,它们是ABCs和协议行为的基础。
子类型:核心概念
子类型关注的是一个类型何时可以替代另一个类型——具体来说,如果类型 B 是 A 的子类型,那么在任何期望 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")
Dog
是 Animal
的名义子类型,因为它通过继承显式声明了这种关系。另一方面,Robot
不是 Animal
的名义子类型,因为它没有继承自 Animal
——但它是一个结构子类型,因为它提供了相同的结构(即所需的方法和属性)。
在区分了名义子类型和结构子类型之后,考虑这些检查何时发生也很重要——是在运行时还是在静态分析期间。
静态类型与动态类型:检查时机
静态类型
在静态类型语言中,每个变量的类型在程序运行前就已经确定。这意味着:
-
类型检查在编译时发生。 -
你会在早期捕获类型错误——在它们溜进生产环境之前。 -
代码通常更安全,但通常也更冗长。
动态类型
在动态类型语言(如Python!)中,类型在程序运行期间才确定。这意味着:
-
无需预先声明类型。 -
你可以更快速、更灵活地编写代码。 -
但类型错误可能只会在运行时出现——有时会在意想不到的地方。
子类型的两个维度
当我们说 A
是 B
的子类型时,我们实际上是在做一个取决于两个关键维度的声明:
-
我们谈论的是哪种类型的子类型?是名义子类型还是结构子类型? -
子类型何时被强制执行或检查?是在静态时还是在运行时?
请始终牢记这些问题——它们有助于澄清“A
是 B
的子类型”实际意味着什么。
现在我们已经剖析了名义与结构以及静态与动态类型化的概念,让我们将所有这些都聚焦到Python上——一个动态类型语言,但可选地支持使用Python 3.5中引入的类型提示进行静态类型化,以提高可读性、文档化和更安全的规模化开发。
让我们详细了解Python中的动态类型和静态类型。
Python中的动态类型
让我们深入探讨Python中动态类型的三个方面:
-
变量的动态性 -
鸭子类型(运行时结构子类型) -
isinstance
和issubclass
变量的动态性质:变量不绑定特定类型
在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()
方法。 -
Duck
和ToyDuck
都有一个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 中引入的类型提示支持可选的静态类型。这意味着你可以用类型注释你的代码,并使用 mypy、pyright 或现代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
模块(抽象基类)为执行运行时子类型检查和定义形式接口契约提供了基础。它通过以下两个关键能力引入了这些功能:
-
ABC
基类(通过ABCMeta
元类)
这支持使用issubclass()
和isinstance()
进行更灵活的子类型检查。在底层,这涉及在ABCMeta
中定义的方法,如__subclasscheck__
、__instancecheck__
和register
,以及在Object
中定义的__subclasshook__
——我们将在下一节中探讨。 -
@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机制。” 这就是 ABC
在 abc
模块中定义的方式。
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()
时发生了什么。
isinstance
和 issubclass
之间的相似性
就像 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定义了它。这允许任何自定义子类型逻辑覆盖所有其他逻辑。如果它返回 True
或 False
,则使用该结果。如果它返回 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. 它必须返回 True
、False
或 NotImplemented
-
如果它返回 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中,而不是元类中。 -
它应该返回 True
、False
或NotImplemented
。 -
它在 __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__()
方法实现的,因此严格发生在运行时。这意味着:
这些功能不影响静态类型检查器,如 mypy
、pyright
或 pytype
。从类型角度来看,使用 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
的名义子类型——即它是否显式继承自 B
。abc
模块通过引入 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 在静态时无法识别 Dog
是 Animal
的结构子类型。因此,我们仍然没有办法在静态时检查结构子类型。这正是协议旨在填补的空白,即协议的引入是为了支持静态时的结构子类型,也称为静态鸭子类型。
定义协议类
在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
协议,因为它提供了所有必需的方法。换句话说,Dog
是 Animal
的静态结构子类型,并且像 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
的元类定义了这个方法。由于 Protocol
是 ABCMeta
的实例,并且 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和协议的一些基础概念。如果你发现任何错误,有问题,或者想分享不同的观点,请随时留言——我很高兴听到你的想法!