面向接口编程与依赖倒置
'对接口编程,而不是对实现编程'这句话在 Python 里并不难理解,难的是怎么写得顺手。很多时候,你并不想把业务代码绑死在某个具体类上,只想表达一件事:只要它会做这件事,就够了。
先从一个简单的'人吃汉堡'例子开始。
抽象基类:更硬的约束
抽象基类(ABC)适合那些你就是想把边界画清楚的场景。它要求具体类继承某个基类,并实现规定的方法,这种约束比较硬,优点是清晰,代价是更强的耦合。
from abc import ABC, abstractmethod
class Burger(ABC):
@abstractmethod
def eat(self):
pass
class ChickenBurger(Burger):
def eat(self):
print("啃鸡腿堡")
class Human:
def eat_lunch(self, burger: Burger):
burger.eat()
# 调用示例
Human().eat_lunch(ChickenBurger())
这里 Human 只依赖 Burger,不关心传进来的是鸡腿堡还是牛肉堡。后面你要换实现,只要新类继承 Burger,并把 eat 补上,Human 不用动。这就是依赖倒置真正省事的地方。
不过,ABC 也有它的毛病:它要求'出身'。有时你只是想让对象具备某个能力,不一定非得让它继承一棵家谱树。
类型注解不会替你拦住错误
Python 的类型注解更像提示,不是运行时的硬限制。解释器不会因为参数写了 Burger,就强制你传一个 Burger 的实例。
# 即使传入的不是显式继承 Burger 基类的实例,也可以成功运行
class Dumpling:
def eat(self):
print("吃饺子")
Human().eat_lunch(Dumpling()) # 运行时不会报错
这段代码能跑通,因为 Dumpling 也实现了 eat。如果你真要让 isinstance() 也认它,有两种路:要么显式继承,要么注册虚拟子类。
Burger.register(Dumpling)
print(isinstance(Dumpling(), Burger)) # True
但我个人不太喜欢把这套机制用得太重。Python 本来就更偏鸭子类型,平时看能力就够了,没必要总盯着血统不放。
协议:更像能力清单
Python 里的协议不是网络里的 HTTP 那种协议,而是一份行为约定:对象要扮演某个角色,就把对应的方法准备好。
这套思路比 ABC 更松一点,也更贴近 Python 的习惯。它关心的是对象'能不能做',不是'从哪来'。
协议和鸭子类型
如果一个对象有 __iter__,那它就能被当成可迭代对象;有 __enter__ 和 __exit__,它就能进 with。这类判断不看继承关系,只看行为。
下面这类结果经常会让刚接触的人愣一下:自定义类明明没显式继承 Iterable,isinstance() 却可能返回 True。原因很简单,Python 只是在检查它是不是具备那个角色需要的能力。
上下文管理器协议
上下文管理器是协议最直观的例子之一。数据库连接和'魔法传送门'当然没有任何继承关系,但只要它们都实现了 __enter__ 和 __exit__,就都能放进 with 里。
class DatabaseConnection:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
class MagicalPortal:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
with DatabaseConnection():
pass
with MagicalPortal():
pass
这类写法的好处很直接:调用方只认协议,不认具体实现。对上层代码来说,连接数据库和打开某个资源入口,最终都只是'进来、用完、退出'这三步。
typing.Protocol 和 @runtime_checkable
Protocol 是 Python 3.8 引入的静态协议工具。它的价值主要在类型检查阶段:IDE 能补全,mypy 也能检查出很多问题。缺点也很明显,它本身是为静态分析设计的,不是给运行时强校验准备的。
这意味着,直接拿 isinstance() 去测一个纯 Protocol,Python 会拒绝。
如果你确实希望协议既能做静态检查,又能在运行时参与 isinstance() 判断,就要加上 @runtime_checkable。
from typing import Protocol, runtime_checkable
@runtime_checkable
class Eatable(Protocol):
def eat(self) -> None:
...
class Pizza:
def eat(self): pass
print(isinstance(Pizza(), Eatable)) # True
加了这个装饰器后,协议就不只是类型提示了,它还能在运行时按'有没有这个能力'来判断对象。这种用法挺实用,但也别过度依赖;协议毕竟还是在帮助你表达约束,不是拿来替代所有校验逻辑的。
硬契约和软契约
ABC 和 Protocol 的区别,核心不在'谁更高级',而在'你想管到什么程度'。
- ABC:要求继承关系明确,适合你想把实现边界锁死一点的场景。
- Protocol:只看方法,不看父类,适合更松、更灵活的接口表达。
如果你在写框架、SDK 或者需要多人协作的公共抽象,ABC 能把事情说清楚;如果你在写业务代码,想少一点绑定、多一点可替换性,Protocol 往往更顺手。
__subclasshook__ 背后的判断
Iterable 这类协议之所以能'认'一个没继承它的类,靠的就是 __subclasshook__。
当你调用 isinstance(obj, SomeABC) 时,Python 大致会走这几步:先看有没有显式继承,再看有没有做过虚拟子类注册,最后才会问 __subclasshook__。只要这个钩子返回 True,即使前两条都不满足,结果也可以成立。
Iterable 的实现思路就是检查对象的类里有没有 __iter__。有,就认为它满足协议;没有,就不认。它不关心对象到底是谁生的,只关心它能不能迭代。
这套机制看起来像黑魔法,但其实和 Python 一贯的思路一致:少一点教条,多一点实际能力。


