Python闲聊-从github一线项目看继承

背景

最近逛开源项目llamaindex,在读到vector_stores相关代码的时候,看到父类VectorStore继承自一个叫Protocol的类型,然后父类VectorStore定义了一系列抽象函数,这个Protocol是什么?一线项目这么用,一定有其原因,咱们一探究竟。

内容

顺藤摸瓜找到了python官网的PEP544,PEP544在python3.8推出了protocol的概念,主要优点如下:

  1. 隐式(implicit)地实现子类对于父类的“继承”。
  2. 结合IDE的静态类型检查,可以让我们写代码更加丝滑。
  3. 比传统的继承方式, 有更少的性能开销

那么protocol该怎么用呢,用我们的核查场景举个例子:

  1. 使用Protocol创建一个核查类
    from typing import Protocol

    class Checker(Protocol):

    def financial_check(self) -> None:
    """ 财务指标核查操作 """
    ...
    def typo_check(self) -> None:
    """ 错别字核查操作 """
    ...
  2. 创建招股书核查类
    class IPOChecker:

    def financial_check(self) -> None:
    """ IPO财务指标核查操作 """
    print('执行IPO财务指标核查操作')
    def typo_check(self) -> None:
    """ IPO错别字核查操作 """
    print('执行IPO错别字核查操作')
  3. 创建ABS文件核查类
    class ABSChecker:

    def financial_check(self) -> None:
    """ ABS财务指标核查操作 """
    print('执行ABS财务指标核查操作')
  4. 创建核查流程类
    class CheckFlow:

    def do_check(self, obj:Checker) -> None:
    """ 执行错别字核查操作 """
    return obj.typo_check()
  5. 执行代码
    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函数

细看一下代码,发现

  1. IPOChecker类甚至没有基于Checker类做继承,但是竟然通过了类型检查。
  2. IPOChecker只是因为实现了相同的功能罢了:也就是实现了typo_check功能。

那么传统通过ABC继承的方式难道不行吗,干嘛费这劲?

  1. 使用ABC做一个抽象虚拟类,然后继承,当然也可以做到。但需要显式(explicit)地声明继承关系,比如
    1. class Checker(ABC)
    2. class IPOChecker(Checker)
  2. 使用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

柚子

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Index