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)通过上面的例子可以看到,一旦要换不同具体实现的汉堡,就必须修改人类的代码。
面向接口编程实现依赖倒置
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解释器不会去理会参数的类型注解。
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()显式检查。
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())虚拟子类
但是这样做,其实也可以通过骚操作绕过去——虚拟子类
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协议神奇之处的例子:
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)打破了接口的最后一道枷锁——显式继承。它告诉我们,最高级的抽象不是去画一张完美无缺的物种分类图,而是去定义一套纯粹的行为契约。
# 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发现。
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 会直接拒绝执行并抛出异常!
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() 调用,去动态检查对象是否拥有协议中定义的那些方法或属性。
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中的协议其实是更加深层次的抽象,我们无需显式继承,通过实现一套“软契约”来实现。如果说继承是强硬法律的话,协议则更像是一种君子协定。
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 内部的逻辑流如下:
显式继承,检查
obj的类是否继承自SomeABC。虚拟子类注册,检查
obj的类是否通过SomeABC.register()注册过。关键点:调用
SomeABC.__subclasshook__(cls)。如果这个方法返回True,那么即使前两条都不满足,isinstance也会返回True。
所以Iterable协议就是通过定义__subclasshook__这个方法,这个方法会去检查__dict__里面会不会存在__iter__这个键,如果存在就返回True。
本文向读者讲解了如何面向接口编程,不去依赖具体的实现,实现依赖倒置。讲解了两种方式,强契约:基类继承,软契约:协议。
总结
到此这篇关于Python中的接口、抽象基类和协议的文章就介绍到这了,更多相关Python接口、抽象基类和协议内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
