Python中的接口、抽象基类和协议
接口与抽象基类
在面向对象的世界中,我们应该对接口编程,而不是实现。我们的代码应该依赖于对象能做什么(抽象),而不是对象具体是谁(具体实现)。
下面通过吃汉堡的例子来演示一下。
依赖具体实现
通过上面的例子可以看到,一旦要换不同具体实现的汉堡,就必须修改人类的代码。
面向接口编程实现依赖倒置
通过上面的代码可以看到,我们使用了abc写了一个抽象基类Burger,对于类Human的eat_lunch方法只是依赖了Burger,而不是依赖了具体某个诸如ChickenBurger的实现类。后续Human想要吃别的汉堡,我们一行代码都不需要改,只需要继承基类,然后实现具体的类就可以了,这就是依赖倒置!
而这种使用抽象基类实现依赖倒置的方式,是一种硬契约!
Python中的类型检查
请注意,在Python中的类型注解并不会强制检查,而只是一种静态检查,Python解释器不会去理会参数的类型注解。
可以看到,即使上面传入的不是显式继承汉堡基类的实例,也可以成功运行。因为类型注解只是静态检查。如果我们想要运行时检查,就得使用isinstance()显式检查。虚拟子类
但是这样做,其实也可以通过骚操作绕过去——虚拟子类
上面代码通过把饺子注册为汉堡的虚拟子类,代码可以跑通。
但是,从纯粹的面向对象理论来看,频繁使用普通类去进行isinstance()检查,确实在很大程度上违背了 Python 核心的“鸭子类型”(Duck Typing)设计哲学。在Python中其实并不怎么关注血统,更在意的其实是能力。协议
在计算机世界里面,在不同语境下的协议具有不同的意思。比如我们最熟悉的HTTP这种网络协议指明了客户端可向服务器发送的命令,例如get,put,post。而Python中的对象协议则指明了为履行某个角色,对象必须实现哪些方法。协议相对于上面展示的面向接口编程,其实是更高层级的抽象,完美诠释了鸭子类型的设计哲学,只关注对象拥有什么能力,而不是去关注血统(父子类显式继承)。
一个展示Python协议神奇之处的例子:
如果不熟悉Python的读者一定会对代码的结果感到惊讶,明明MyIter和Iterable并没有任何显式继承的关系,但是isinstance(counter, Iterable)居然返回了Ture!这就是Python协议的强大之处,目标有__iter__(迭代)的能力,我就认为你是一个迭代器,无需任何显式继承。
上下文管理器协议(Context Manager Protocol)
Python 的协议(Protocol)打破了接口的最后一道枷锁——显式继承。它告诉我们,最高级的抽象不是去画一张完美无缺的物种分类图,而是去定义一套纯粹的行为契约。
通过上面的例子(ai写的)可以看出,两个上下文管理对象没有任何显式继承,但是却能被with这个上下文管理器。原因就在于DatabaseConnection和MagicalPortal都实现了上下文管理器的协议。
静态协议
Protocol(静态协议)是Python在3.8引入的,通过上面的关于协议的讲解,我们可以发现,协议的坏处就是没有类型提示,所以往往我们可能去调用一个对象不存在的方法,而我们很难在写代码的时候通过Mypy或者是IDE发现。 @runtime_checkable
在使用 typing.Protocol 时,我们获得了极佳的静态类型提示体验(IDE 提供智能补全,mypy 静态检查能通过)。但是,由于 Protocol 纯粹是为“静态检查”设计的,如果你尝试在代码运行阶段使用 isinstance() 去验证一个对象是否符合静态协议,Python 会直接拒绝执行并抛出异常!
为了解决这个问题,让协议既能享受静态类型检查的红利,又能像普通的类一样在运行时使用 isinstance() 进行鸭子类型判断,Python 的 typing 模块提供了 @runtime_checkable 装饰器。
它的底层原理,正是我们在上文提到的 __subclasshook__ 黑魔法。加上这个装饰器后,Python 会自动在底层拦截 isinstance() 调用,去动态检查对象是否拥有协议中定义的那些方法或属性。
使用协议实现依赖倒置
上面我们在讲接口和抽象基类的时候,通过一个汉堡的例子向读者展示怎样实现依赖倒置。这是通过定义抽象基类,让具体实现继承抽象基类Burger这个“硬契约”来实现的。而Python中的协议其实是更加深层次的抽象,我们无需显式继承,通过实现一套“软契约”来实现。如果说继承是强硬法律的话,协议则更像是一种君子协定。
通过上面的代码,我们可以看到,我们实现的这套“软契约”其实就是目标对象有没有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。
本文向读者讲解了如何面向接口编程,不去依赖具体的实现,实现依赖倒置。讲解了两种方式,强契约:基类继承,软契约:协议。