Python and public APIs

By Jake Edge
July 31, 2019


按理说,Python standard library module的public API在它的文档里面应该有完整描述,不过实际上并没有那么理想。有一些方法能在module里面把一些API的名字指定清楚,就是希望让它们作为public API。也还有一些非通用的做法来规范命名规则来指明哪些API不是public的(例如函数名用下划线起始)。总之,目前在standard library(标准库)里还没有一个统一的方法来标记public API。7月中旬在python-dev mailing list上有人提出这个问题,以及一些可能的解决方案。目前看来讨论结果主要是希望把规则定义得更清晰一些。


首先需要强调一下,Python其实并没有对各个函数强加一些访问限制。任何一个程序在import了某个module之后,就可以访问module内部定义的所有顶层命名(包括函数和变量等)。所有用来管理访问限制的规则都其实仅仅是某种共识而已,经常是用来供module的维护者来标记哪些API今后可能会不经过通常的deprecation流程就被改掉了。public API中大部分其实就是module维护者承诺不会随意改变的那些name(要改的话肯定会走一些警告流程,例如提前两个开发周期的提醒)。


Rules?


Serhiy Storchaka在mailing list上提起的这个话题,他一来就列出他所意识到的各种public/private的命名管理规则。这些通常是解析__all__这个属性(是在module被利用“from module import *”方式真正import的所有name)。如果没有定义__all__的话,Python就会把所有不是以"_"开头的name都import进来,这样在他理解下,这些name都会成为事实意义上的public API。此外,他认为任何被文档里描述为public API的name也都需要得到相应的维护。


他提起有两例bug report,看起来都是违背了他所理解的public API的定义方式的。第一个bug里面,Raymond Hettinger提出希望calendar module里面的所有non-public function都需要被改名为"_"开头。而另一个bug里面,Gregory P.Smith希望在codecs module的文档里面解释清楚escape_decode()函数,因为从它的名字来看这应该是一个public API。他看到escape_decode()在Stack Overflow等处的回答里面经常被推荐使用,因此他觉得需要明确它是一个public API。


但在这两个bug里面, __all__属性其实都并没有列出这两个涉及到的函数,所以其实他们不应该被认为是public API。因此并不需要加上“_”,或者加入文档。并不应该以Stack Overflow的信息为准。


Hettinger争论道,calendar module一直以来就是使用下划线明明方式的,直到最近才有一个patch破坏了这个规范。有一个用户就感到很困惑所以在twitter上提了这个问题。

而Storchaka指出,其实calendar早就已经有一些non-public的函数,自从Python 3.6时开始就没有使用下划线来做标记了。有些人会用dir()这个内置函数来查看module里的name,而dir()module)其实是会给出module里的所有name的完整列表,包括public和private的,不会根据__all__或者下划线前缀来决定是否要隐藏一些。Storchaka认为dir()不是这种场景下该用的方法,而是应该使用help()内置函数,例如help(module)这样。


Hettinger邮件里面第一行提出“只有我们包含在doc里的才是public API,其他的都不是”。Brett Cannon觉得Hettinger说的对,应该让每个module都保持自己的规范,尽管calendar其实做的不是很好。甚至核心开发者应该对新的module都鼓励使用下划线开头方式。


Kyle Stanley提出对所有standard library都做一次大规模改名,不过这个建议没能得到大家的赞同。因为这里会有很多操作上的问题,尤其是deprecation cycle(前面提到过的,两个release cycle之前提出警示)。并且这真的能解决什么真实问题吗?Steven D'Aprano指出他基本上没见过用户错用module的private API,并且他认为哪怕都改名到_private开头了,大家也还是有可能会用这些private API的。而且,还需要花费很大精力来完成这种大规模的改动。


他还提到一个可以应用的规则:“除非是文档里面特别说明是public的,所有import进来的都是private name,哪怕它没有用下划线开头”。Stanley回复也说他改变了对所有模块的大规模改动的想法,但是他想不到什么好地方来明确规定这个规则。Steve Dower觉得D'Aprano的说法是个比较合理的规则,不过确实没有任何地方有明确文档规定这一点。而Stanley建议了另一个清理方法,就是增加 @public decorator(装饰器)。


atpublic


Barry Warsaw在这个讨论刚开始的时候就提出让大家看看他的@public decorator project,根据Zen of Python(Python之禅)的基本规则“Explicit is better than implicit”(尽量做明确指定)。他的这个module在PyPI上的名字是atpublic,可以使用pip来获取。它提供了一个简单的decorator可以用来明确指定module里的各个public name:


@public
    def foo():
        pass

    def bar():
        pass

    @public
    class Baz:
        pass

    public(QUX=42)



这里foo()函数了Baz类会被列在__all__属性里面,而bar()则不会。因为常量无法被decorate(使用装饰器),那么可以利用public()来指定常量名,并且加入__all__,上面的QUX就是相应的例子。这样__all__就总能反映出public API的列表了。


Dower觉得不应该使用@public这种方式。首先他觉得这样做在改变public API name的时候更加复杂了,不够简洁。此外他也觉得@public会有运行时的额外开销,不过Warsaw等人指出可以有多种方法来消除这个开销(甚至缺省的实现里面这个开销也非常非常小)。Dower不再提性能影响了,转而提出这个改动需要对整个代码库都要做秀爱。他也还担心在规则定义清楚之前如何做这个修改:“我们一直在源代码库之外维护独立的文档,文档才是查询public与否的唯一依据。除非我们能跟大家说要改成以__all__作为唯一依据了,否则不应该急于修改那么多文件(甚至还要加一个builtin函数)。我收回刚才所说的import性能问题的话,咱们先忽略这个,倒是应该先担心如何让所有人都能达成一致到底public函数列表的唯一查询依据究竟是哪个。”


Stanley和Warsaw都希望能先找个地方来放这个public API表示方式的规定,Cannon指出PEP 8(“Style Guide for Python Code”)是个合适的地方。Warsaw补充说他觉得不应该一下子把@public加入所有各个module的代码,而是应该逐步的加入。他也比较担心过去常见的代码和文档不一致的问题:“这个问题总是终结于到底是源代码说了算还是文档说了算,对各个具体情况来说,我们经常做出不同选择。”


对大多数人来说,都一直认为是根据下划线来判断哪些是public API。D'Aprano的规则(除非是文档里面特别说明是public的,所有import进来的都是private name,哪怕它没有用下划线开头)看起来是个比较简洁的做法,不过最好能规定得更加详细一些。API都得很小心的定义,通常来说,正在开发中的项目不太会花很多时间做设计、审查、测试,也就不会保证这些API能长久可用。如果我们连对如何定义public API的规则都还没有定义清楚,这样就没法搞了。因此第一步还是要把这里的模糊定义改得更加清晰。