接口与抽象基类
在面向对象的世界中,我们应该对接口编程,而不是实现。我们的代码应该依赖于对象能做什么(抽象),而不是对象具体是谁(具体实现)。
下面通过吃汉堡的例子来演示一下。
依赖具体实现
pythonclass 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)通过上面的例子可以看到,一旦要换不同具体实现的汉堡,就必须修改人类的代码。
面向接口编程实现依赖倒置
pythonfrom 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解释器不会去理会参数的类型注解。
pythonfrom 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()显式检查。
pythonfrom 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())虚拟子类
但是这样做,其实也可以通过骚操作绕过去------虚拟子类
pythonfrom 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协议神奇之处的例子:
pythonfrom 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发现。
pythonfrom 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 会直接拒绝执行并抛出异常!
pythonfrom 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() 调用,去动态检查对象是否拥有协议中定义的那些方法或属性。
pythonfrom 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中的协议其实是更加深层次的抽象,我们无需显式继承,通过实现一套"软契约"来实现。如果说继承是强硬法律的话,协议则更像是一种君子协定。
pythonfrom 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。
本文向读者讲解了如何面向接口编程,不去依赖具体的实现,实现依赖倒置。讲解了两种方式,强契约:基类继承,软契约:协议。