Python中的接口、抽象基类和协议

接口与抽象基类

在面向对象的世界中,我们应该对接口编程,而不是实现。我们的代码应该依赖于对象能做什么(抽象),而不是对象具体是谁(具体实现)。

下面通过吃汉堡的例子来演示一下。

依赖具体实现

python 复制代码
class BigMac:
    def eat(self):
        return "巨无霸:双层牛肉,酱汁浓郁!"

class ChickenBurger:
    def eat(self):
        return "香辣鸡腿堡:外酥里嫩!"

class Human:
    def eat_lunch(self, food: BigMac):
        # 【面向实现编程】
        # 这里写死了:我只检查是不是巨无霸。
        # 如果想吃香辣鸡腿堡,必须修改 Human 类的代码!没有任何拓展性
        if isinstance(food, BigMac):
            print(f"人类正在吃: {food.eat()}")
        else:
            print("人类拒绝进食:这不是巨无霸!")

# --- 代码运行 ---
person = Human()
mcd = BigMac()
kfc = ChickenBurger()

# 一旦换了不同的汉堡(具体实现)就吃不了了。。。
person.eat_lunch(mcd)
person.eat_lunch(kfc)

通过上面的例子可以看到,一旦要换不同具体实现的汉堡,就必须修改人类的代码。

面向接口编程实现依赖倒置

python 复制代码
from abc import ABC, abstractmethod

# 1. 定义接口,这是汉堡基类,可以实现出不同的汉堡
class Burger(ABC):
    @abstractmethod
    def eat(self):
        pass

# 2. 具体实现(必须继承 Burger)
class BigMac(Burger):
    def eat(self):
        return "巨无霸:双层牛肉!"

class ChickenBurger(Burger):
    def eat(self):
        return "香辣鸡腿堡:脆皮炸鸡!"

# 3. 客户端
class Human:
    # 【面向接口编程】后续想挑战什么汉堡都可以,继承Burger实现eat就可以
    # 只要是 Burger 的子类,我都能接受
    def eat_lunch(self, food: Burger):
        print(f"人类正在吃: {food.eat()}")

# --- 代码执行 ---
person = Human()
person.eat_lunch(BigMac())        
person.eat_lunch(ChickenBurger())

通过上面的代码可以看到,我们使用了abc写了一个抽象基类Burger,对于类Human的eat_lunch方法只是依赖了Burger,而不是依赖了具体某个诸如ChickenBurger的实现类。后续Human想要吃别的汉堡,我们一行代码都不需要改,只需要继承基类,然后实现具体的类就可以了,这就是依赖倒置!

而这种使用抽象基类实现依赖倒置的方式,是一种硬契约!

Python中的类型检查

请注意,在Python中的类型注解并不会强制检查,而只是一种静态检查,Python解释器不会去理会参数的类型注解。

python 复制代码
from abc import ABC, abstractmethod

# 1. 定义接口,这是汉堡基类,可以实现出不同的汉堡
class Burger(ABC):
    @abstractmethod
    def eat(self):
        pass

class Dumpling(object):
    def eat(self):
        return "玉米饺子"

# 3. 客户端
class Human:
    # 【面向接口编程】后续想挑战什么汉堡都可以,继承Burger实现eat就可以
    # 只要是 Burger 的子类,我都能接受
    def eat_lunch(self, food: Burger):
        print(f"人类正在吃: {food.eat()}")

# --- 代码执行 ---
person = Human()
# 传入的对象是饺子的实例对象,但是也可以运行成功,因为饺子有eat方法
person.eat_lunch(Dumpling())

可以看到,即使上面传入的不是显式继承汉堡基类的实例,也可以成功运行。因为类型注解只是静态检查。如果我们想要运行时检查,就得使用isinstance()显式检查。

python 复制代码
from abc import ABC, abstractmethod

# 1. 定义接口,这是汉堡基类,可以实现出不同的汉堡
class Burger(ABC):
    @abstractmethod
    def eat(self):
        pass

class Dumpling(object):
    def eat(self):
        return "玉米饺子"

# 3. 客户端
class Human:
    # 【面向接口编程】后续想挑战什么汉堡都可以,继承Burger实现eat就可以
    # 只要是 Burger 的子类,我都能接受
    def eat_lunch(self, food: Burger):
        if not isinstance(food, Burger):
            raise TypeError(f"必须传入 Burger 的子类,当前传入的是: {type(food).__name__}")
        print(f"人类正在吃: {food.eat()}")

# --- 代码执行 ---
person = Human()
person.eat_lunch(Dumpling())

虚拟子类

但是这样做,其实也可以通过骚操作绕过去------虚拟子类

python 复制代码
from abc import ABC, abstractmethod

# 1. 定义接口,这是汉堡基类,可以实现出不同的汉堡
class Burger(ABC):
    @abstractmethod
    def eat(self):
        pass

# 注册为汉堡的虚拟子类
@Burger.register
class Dumpling(object):
    def eat(self):
        return "玉米饺子"

# 3. 客户端
class Human:
    # 【面向接口编程】后续想挑战什么汉堡都可以,继承Burger实现eat就可以
    # 只要是 Burger 的子类,我都能接受
    def eat_lunch(self, food: Burger):
        if not isinstance(food, Burger):
            raise TypeError(f"必须传入 Burger 的子类,当前传入的是: {type(food).__name__}")
        print(f"人类正在吃: {food.eat()}")

# --- 代码执行 ---
person = Human()
person.eat_lunch(Dumpling())

上面代码通过把饺子注册为汉堡的虚拟子类,代码可以跑通。
但是,从纯粹的面向对象理论来看,频繁使用普通类去进行**isinstance()** 检查,确实在很大程度上违背了 Python 核心的"鸭子类型"(Duck Typing)设计哲学。在Python中其实并不怎么关注血统,更在意的其实是能力。

协议

在计算机世界里面,在不同语境下的协议具有不同的意思。比如我们最熟悉的HTTP这种网络协议指明了客户端可向服务器发送的命令,例如get,put,post。而Python中的对象协议 则指明了为履行某个角色,对象必须实现哪些方法。协议相对于上面展示的面向接口编程,其实是更高层级的抽象,完美诠释了鸭子类型的设计哲学,只关注对象拥有什么能力,而不是去关注血统(父子类显式继承)。

一个展示Python协议神奇之处的例子:

python 复制代码
from collections.abc import Iterable

class MyIter:
    # MyIter类只是定义了__iter__方法
    def __iter__(self):
        pass

counter = MyIter()

# 神奇的事情发生了,MyIter和Iterable并没有任何显式继承的关系
print(isinstance(counter, Iterable))

# 验证一下MyIter并没有继承Iterable
print(MyIter.__mro__)

如果不熟悉Python的读者一定会对代码的结果感到惊讶,明明MyIter和Iterable并没有任何显式继承的关系,但是isinstance(counter, Iterable)居然返回了Ture!这就是Python协议的强大之处,目标有__iter__(迭代)的能力,我就认为你是一个迭代器,无需任何显式继承。

上下文管理器协议(Context Manager Protocol)

Python 的协议(Protocol)打破了接口的最后一道枷锁------显式继承。它告诉我们,最高级的抽象不是去画一张完美无缺的物种分类图,而是去定义一套纯粹的行为契约。

python 复制代码
# 1. 一个正经的后端对象:数据库连接
class DatabaseConnection:
    def __enter__(self):
        print("🔗 [Database] 开启数据库连接...")
        return self
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("🔌 [Database] 释放数据库连接...")
        return False

# 2. 一个毫不相干的奇葩对象:魔法传送门
class MagicalPortal:
    def __enter__(self):
        print("🌀 [Portal] 念动咒语,打开时空传送门...")
        return self
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("💥 [Portal] 传送结束,关闭传送门防止怪物入侵...")
        return False

# 3. 见证协议的威力
print("--- 场景 A:后端工程师在工作 ---")
# with 语句根本不在乎你是数据库还是什么,它只看协议
with DatabaseConnection():
    print("   执行 SQL 查询: SELECT * FROM users")

print("\n--- 场景 B:法师在施法 ---")
# 魔法传送门没有继承任何基类,但它满足了协议,照样能用 with!
with MagicalPortal():
    print("   正在穿越到艾泽拉斯大陆...")

通过上面的例子(ai写的)可以看出,两个上下文管理对象没有任何显式继承,但是却能被with这个上下文管理器。原因就在于DatabaseConnection和MagicalPortal都实现了上下文管理器的协议。

静态协议

Protocol(静态协议)是Python在3.8引入的,通过上面的关于协议的讲解,我们可以发现,协议的坏处就是没有类型提示,所以往往我们可能去调用一个对象不存在的方法,而我们很难在写代码的时候通过Mypy或者是IDE发现。

python 复制代码
from typing import Protocol

# 1. 定义静态协议 (契约)
# 注意:我们继承的是 typing.Protocol,而不是常规的基类
class MessageSender(Protocol):
    def send(self, message: str) -> bool:
        """只要一个类拥有接受 str 并返回 bool 的 send 方法,
           它在静态检查器眼里,就是 MessageSender。"""

# 2. 具体实现 (完全不需要继承 MessageSender!)
class EmailSender:
    def send(self, message: str) -> bool:
        print(f"📧 发送邮件: {message}")
        return True

class SMSSender:
    def send(self, message: str) -> bool:
        print(f"📱 发送短信: {message}")
        return True

# 3. 一个"假装"能发送,但其实方法名不对的类
class Printer:
    def print_doc(self, message: str) -> bool:
        print(f"🖨️ 打印文档: {message}")
        return True

# 4. 客户端调用方
# 在类型注解中指定依赖 Protocol
def alert_admin(sender: MessageSender, alert_msg: str):
    sender.send(alert_msg)

# --- 测试环节 ---

email_sender = EmailSender()
sms_sender = SMSSender()
printer = Printer()

# ✅ 这两行在 IDE(如 VS Code / PyCharm)或 mypy 中完全合法
alert_admin(email_sender, "服务器 CPU 超过 90%!")
alert_admin(sms_sender, "数据库连接断开!")

# ❌ 这一行会在代码运行前,直接被 IDE 标红报错!
alert_admin(printer, "机房起火了!")

@runtime_checkable

在使用 typing.Protocol 时,我们获得了极佳的静态类型提示体验(IDE 提供智能补全,mypy 静态检查能通过)。但是,由于 Protocol 纯粹是为"静态检查"设计的,如果你尝试在代码运行阶段使用 isinstance() 去验证一个对象是否符合静态协议,Python 会直接拒绝执行并抛出异常!

python 复制代码
from typing import Protocol

class Burger(Protocol):
    def eat(self) -> str:
        ...

class BigMac:
    def eat(self) -> str:
        return "巨无霸:双层牛肉!"

# 运行这段代码会直接报错:
print(isinstance(BigMac(), Burger))

为了解决这个问题,让协议既能享受静态类型检查的红利,又能像普通的类一样在运行时使用 isinstance() 进行鸭子类型判断,Python 的 typing 模块提供了 @runtime_checkable 装饰器。

它的底层原理,正是我们在上文提到的 **subclasshook**黑魔法。加上这个装饰器后,Python 会自动在底层拦截 isinstance() 调用,去动态检查对象是否拥有协议中定义的那些方法或属性。

python 复制代码
from typing import Protocol, runtime_checkable

# 加上装饰器,赋予协议在运行时被 isinstance 检查的能力
@runtime_checkable
class Burger(Protocol):
    def eat(self) -> str:
        ...

class BigMac:
    def eat(self) -> str:
        return "巨无霸:双层牛肉!"

class Hotdog:
    def lick(self) -> str:
        return "吃热狗"

mac = BigMac()
dog = Hotdog()

# ✅ 运行时完美通过!输出 True,因为 BigMac 拥有 eat 方法
print(f"mac 是 Burger 吗? {isinstance(mac, Burger)}") 

# ✅ 完美拦截!输出 False,因为 Hotdog 没有 eat 方法
print(f"dog 是 Burger 吗? {isinstance(dog, Burger)}")

使用协议实现依赖倒置

上面我们在讲接口和抽象基类的时候,通过一个汉堡的例子向读者展示怎样实现依赖倒置。这是通过定义抽象基类,让具体实现继承抽象基类Burger这个"硬契约"来实现的。而Python中的协议其实是更加深层次的抽象,我们无需显式继承,通过实现一套"软契约"来实现。如果说继承是强硬法律的话,协议则更像是一种君子协定。

python 复制代码
from typing import Protocol

# 1. 定义协议(静态接口/结构化子类型)
class Burger(Protocol):
    def eat(self) -> str:
        ...

# 2. 具体实现(隐式实现了 Burger 协议,完全解耦,无需显式继承)
class BigMac:
    def eat(self) -> str:
        return "巨无霸:双层牛肉!"

class ChickenBurger:
    def eat(self) -> str:
        return "香辣鸡腿堡:脆皮炸鸡!"

# 3. 客户端
class Human:
    # 【面向协议编程/静态鸭子类型】
    # 只要传入的对象拥有和 Burger 协议定义一致的 eat 方法,就能通过静态类型检查
    def eat_lunch(self, food: Burger) -> None:
        print(f"人类正在吃: {food.eat()}")

# --- 代码执行 ---
person = Human()
person.eat_lunch(BigMac())        
person.eat_lunch(ChickenBurger())

通过上面的代码,我们可以看到,我们实现的这套"软契约"其实就是目标对象有没有eat这个方法,有我就认为你是汉堡。为什么说这是更加抽象的方式,因为我们在解耦了汉堡的具体实现与人类的耦合的同时,还无需去显式继承任何基类。

  • 显式继承 (ABC) :不仅要求类具备某些方法,还强制要求类在"族谱"上属于某个基类(强耦合的 is-a 关系)。

  • 协议 (Protocol) :完全不在乎对象是从哪里来的、继承自谁,只关心它能不能做这件事(can-do 关系)。实现类根本不需要知道协议的存在,也不需要导入协议所在的模块,这就实现了真正的业务逻辑与接口定义的彻底解耦。

协议黑魔法

现在回到我们开头那个迭代器协议的例子,Python是如何"偷偷"实现这个协议的

其实秘密就在于**__subclasshook__**这个魔法方法,

当我们调用 isinstance(obj, SomeABC) 时,Python 内部的逻辑流如下:

  1. 显式继承,检查 obj 的类是否继承自 SomeABC

  2. 虚拟子类注册,检查 obj 的类是否通过 SomeABC.register() 注册过。

  3. 关键点 :调用 SomeABC.__subclasshook__(cls)。如果这个方法返回 True,那么即使前两条都不满足,isinstance 也会返回 True

所以Iterable协议就是通过定义**__subclasshook__**这个方法,这个方法会去检查__dict__里面会不会存在__iter__这个键,如果存在就返回True。

本文向读者讲解了如何面向接口编程,不去依赖具体的实现,实现依赖倒置。讲解了两种方式,强契约:基类继承,软契约:协议。

相关推荐
深圳华秋电子1 小时前
靠谱的EDA AI助手生产厂家——华秋KiCad
人工智能·python
十五年专注C++开发1 小时前
Qt deleteLater作用及源码分析
开发语言·c++·qt·qobject
xyq20241 小时前
Redis 列表(List)
开发语言
稻草猫.1 小时前
TCP与UDP:传输层协议深度解析
笔记·后端·网络协议
you-_ling1 小时前
线程及进程间通信
java·开发语言
徐先生 @_@|||1 小时前
时间序列异常检测框架CrossAD论文阅读
经验分享·python·机器学习
Moment2 小时前
此 KFC 不是肯德基,Kafka、Flink、ClickHouse 怎么搭、何时省掉 Flink
前端·后端·面试
Charlie_lll2 小时前
力扣解题-438. 找到字符串中所有字母异位词
后端·算法·leetcode
绝无仅有2 小时前
Java多线程并发问题解决方案全解析
后端·面试·架构