背景
最近逛开源项目llamaindex,在读到vector_stores相关代码的时候,看到父类VectorStore继承自一个叫Protocol的类型,然后父类VectorStore定义了一系列抽象函数,这个Protocol是什么?一线项目这么用,一定有其原因,咱们一探究竟。
内容
顺藤摸瓜找到了python官网的PEP544,PEP544在python3.8推出了protocol的概念,主要优点如下:
- 隐式(implicit)地实现子类对于父类的“继承”。
- 结合IDE的静态类型检查,可以让我们写代码更加丝滑。
- 比传统的继承方式, 有更少的性能开销
那么protocol该怎么用呢,用我们的核查场景举个例子:
- 使用Protocol创建一个核查类
from typing import Protocol
class Checker(Protocol):
def financial_check(self) -> None:
""" 财务指标核查操作 """
...
def typo_check(self) -> None:
""" 错别字核查操作 """
... - 创建招股书核查类
class IPOChecker:
def financial_check(self) -> None:
""" IPO财务指标核查操作 """
print('执行IPO财务指标核查操作')
def typo_check(self) -> None:
""" IPO错别字核查操作 """
print('执行IPO错别字核查操作') - 创建ABS文件核查类
class ABSChecker:
def financial_check(self) -> None:
""" ABS财务指标核查操作 """
print('执行ABS财务指标核查操作') - 创建核查流程类
class CheckFlow:
def do_check(self, obj:Checker) -> None:
""" 执行错别字核查操作 """
return obj.typo_check() - 执行代码
def main()-> None:
check_flow = CheckFlow()
check_flow.do_check(IPOChecker()) # line1
check_flow.do_check(ABSChecker()) # line2
if __name__ =='__main__':
main()
这时候line1可以通过IDE的静态检查器检查,而line2会提示有问题,因为ABSChecker和之前定义的Checker(Protocol)不一致,因为ABSChecker没有实现typo_check函数
细看一下代码,发现
- IPOChecker类甚至没有基于Checker类做继承,但是竟然通过了类型检查。
- IPOChecker只是因为实现了相同的功能罢了:也就是实现了typo_check功能。
那么传统通过ABC继承的方式难道不行吗,干嘛费这劲?
- 使用ABC做一个抽象虚拟类,然后继承,当然也可以做到。但需要显式(explicit)地声明继承关系,比如
- class Checker(ABC)
- class IPOChecker(Checker)
- 使用protocol就不需要显式(explicit)地继承了,解耦了类(子类)和protocol(父类),代码变简单了,系统自动地帮我们去匹配子类中的函数和protocol所定义的函数。也叫做静态鸭子类型(static duck typing)。
思考
- Python著名的鸭子类型(duck typing)为这门动态语言提供了灵活性、伸缩性、便捷性的同时,也带来了可读性、可维护性、可调试性的问题。Protocol的推出,给这门灵活的语言加了一点约束,使得Python在动态语言和静态语言的某些优点之间做一些兼顾和平衡
- 回到Python之父在创建这门语言时候写的Python哲学(zen of python),我依稀记得有这么一句话:Explicit is better than implicit,似乎和我们这里介绍的Protocol的思想,有点违背的意思。我想也许是艺术家python面对现实工程问题的一种妥协吧,毕竟从某种程度来看,python慢慢也都有点java化了
- 之前写java的时候就感受到了,在现代IDE的加持下,静态语言的编码体验有多么丝滑,各种提示补全、自动生成,写起来其实也挺方便,因为有类型注释,所以维护起来也方便。同样的,静态类型检查器+IDE的集成,无疑对于Python程序员的编码体验会有提升,能在运行前就发现各种问题,出了bug也好定位,但代价是要写类型注释。
- 继承是面向对象编程的基石,但是一些新语言,比如go、rust,已经舍弃了继承的特性。继承的目的,本质上是代码复用,但是也会遇到一些问题:
- 高耦合
- 随之而来问题就是,有时候子类并不需要父类的所有东西,但是继承一股脑都把父类的东西塞给子类
- 一旦父类需要修改,一不小心,子类就容易出问题
- 多继承
- Python中的MRO曲折复杂,多继承还要去单独计算调用顺序,反人类
- 导致菱形继承问题
- 当继承的层级过多,记忆、管理继承下来的属性和方法就非常麻烦,使得代码更难读懂,更难理解
- 高耦合
ref
发表回复