第七章: 协作开发
第49条: 为每个函数, 类和模块编写文档字符串
介绍
Python对文档提供了内置的支持, 使得开发者可以把文档与代码块关联起来. Python程序在运行的额时候可以通过 __doc__ 这个特殊属性直接访问源代码中的文档信息.
因为Python代码支持文档字符串和 __doc__ 属性, 带来了以下的好处:
- 交互式开发更方便
- 开发者可以构建一些工具把纯文本转换为HTML等更友好的方式
- 开发者可以在程序中访问格式良好的文档信息
我们在编写文档字符串时应该遵循一些规则, 可以参阅 PEP257. 以下是一些应该遵循的规范:
为模块编写文档
每个模块都应该有顶级的docstring, 这个字符串字面量应该作为源文件的第一条语句, 用三重双引号括起来, 介绍当前的模块以及模块之中的内容.
docstring的第一行文字用以描述本模块的用途. 下面的那段话包含一些细节信息, 把与本模块的操作有关的内容告诉模块的使用者, 强调本模块里面比较重要的类和函数.
为类编写文档
每个类都应该有类级别的docstring, 写法与模块的docstring大致相同. 类中比较重要的public属性及方法应该在这个docstring中加以强调, 还应该告诉子类的实现者如何才能正确的与protected属性集超类方法相交互.
为函数编写文档
每个public函数及方法都应该有docstring, 与模块和类的docstring相似. 应该描述具体的行为和函数的参数, 以及函数的返回值与异常.
为函数编写文档时有一些特例:
- 如果没有参数且仅有一个简单的返回值, 则用一句话描述就可以
- 如果没有返回值就不要描述到docstring, 不要出现return None这种描述
- 如果不会抛出异常则不要描述
- 在文档中用 *args 和 **kwargs 描述数量可变的位置参数和数量可变的关键字参数
- 指出函数的默认值
- 如果函数是一个生成器, 应该描述该生成器在迭代时所产生的内容
- 如果函数是一个协程, 应该描述协程所产生的值, 以及通过yield表达式来接纳的值, 同时还要说明协程会于何时停止迭代
可以通过内置的doctest模块来运行docstring中的范例代码, 以确保代码和文档不会在开发过程中产生偏差.
要点
- 应该通过docstring为每个模块, 类和函数编写文档, 在修改代码的时候应该更新这些文档
- 为模块撰写文档时应该介绍本模块的内容, 并且要把用户应该了解的重要类及重要函数列出来
- 为类撰写文档时, 应该在class语句下面的docstring中介绍类的行为, 重要属性及子类应该实现的行为
- 为函数及方法撰写文档时, 应该在def语句下面的docstring中介绍函数的每个参数, 函数的返回值, 函数在执行过程中可能抛出的异常以及其他行为
第50条: 用包来安排模块, 并提供稳固的API
介绍
包是一种含有其他模块的模块. 大多数情况下我们会在目录中放入名为 __init__.py 的空文件, 并以此来定义包. 只要目录里有 __init__.py 我们就可以采用相对于该目录的路径来引入目录中的其他Python文件.
包提供的能力主要有两大用途:
- 名称空间
包可以把模块划分到不同的名称空间之中, 使得开发者可以编写多个文件名相同的模块, 并把它们放在不同的绝对路径之下.
- 稳固的API
在Python程序中, 我们可以以包或模块编写名为 __all__ 的特殊属性, 以减少其暴露给外围API使用者的信息量. __all__ 属性里面的值是一份列表, 其中的每个名称都将作为本模块的一条公共API, 导出给外部代码.
我们可以修改目录下的 __init__.py 文件, 当外界引入包的时候, 该文件实际上也会称为该包的一部分内容.
要点
- Python包是一种含有其他模块的模块, 可以用包划分成各自独立且互不冲突的名称空间
- 只要把 __init__.py 文件放入含有其他源文件的目录里就可以把目录定义为包
- 外界可见的名称列在文件的 __all__ 的特殊属性里
- 如果想隐藏某个包的内部实现, 可以在包的 __init__.py 文件中, 只把外界可见的名称引入进来, 或是给仅限内部使用的那些名称添加下划线前缀
- 如果软件包只供开发团队或代码库内部使用, 那可能没必要通过 __all__ 来明确的导出API
第51条: 为自编的模块定义根异常, 以便将调用者与API相隔离
介绍
我们可以在模块中定义一个根异常, 然后令该模块抛出的其他异常都继承自这个根异常. 模块有了这个根异常之后, API的使用者就可以轻松的捕获该模块所抛出的各种异常. 这种隔离方式有以下的好处:
- 通过捕获根异常, 调用者可以得知他们在使用你的API时所编写的调用代码是否正确
- 可以帮助模块的开发者找寻API里的BUG
- 便于API的后续演化
要点
- 为模块定义根异常, 可以把API的调用者与模块的API相隔离
- 调用者在使用API时, 可以通过捕获根异常来发现调用代码中的BUG
- 调用者可以通过捕获Exception异常来帮助模块的开发者找寻模块的实现代码中的BUG
- 可以从模块的根异常里面继承一些中间异常, 并令API的调用者捕获这些中间异常, 这样模块的开发者将来就能在不破坏原有调用代码的前提下为这些中间异常分别编写具体的异常子类
第52条: 用适当的方式打破循环依赖关系
介绍
在引入模块的时候, Python会按照深度优先的顺序执行下列操作:
- 在由sys.path所指定的路径中搜寻待引入的模块
- 从模块中加载代码, 并保证这段代码能够正确编译
- 创建与该模块相对应的空对象
- 把这个空的模块对象, 添加到sys.modules里面
- 运行模块对象中的代码, 以定义其内容
循环依赖所产生的问题是, 某些属性必要到等Python系统把对应的代码执行完毕之后(即第5步)才可以具备完整的定义. 但是包含该属性的模块却在第4步之后就可以用import语句添加到sys.modules里面.
通过以下的方法可以避免循环依赖关系:
- 调整引入顺序. 但是这种方式与PEP8风格指南不符, 而且会导致代码的顺序发生少许变动, 这种变动可能会令整个模块都无法运作.
- 先引入, 再配置, 最后运行. 每个模块都定义configure函数, 在模块引入完毕之后调用. 但是有时很难提取出configure步骤, 也会令代码变得不容易理解.
- 动态引入. 即在函数或方法内部使用import语句.
要点
- 如果两个模块必须要相互调用对方才能完成引入操作, 这可能会出现循环依赖而导致程序启动失败
- 打破循环依赖最理想的方案是把互相依赖的部分重构为单独的模块
- 打破循环依赖最简单的方案是动态引入
第53条: 用虚拟环境隔离项目, 并重建其依赖关系
介绍
在同一时刻, Python只能把模块的某一个版本安装为整个系统的全局版本.
Python的pyvenv工具提供了一套虚拟环境, 使得我们可以创建版本互不相同的Python环境.
要点
- 借助虚拟环境我们可以在同一台电脑上安装某软件包的不同版本, 而且相互间不会出现冲突
- pyvenv命令可以创建虚拟环境, source bin/activate 命令可以激活虚拟环境, deactivate命令可以停用虚拟环境
- pip freeze命令可以把环境中的软件包汇总到一份文件, 然后通过 pip install -r 命令安装
- 在Python3.4版本之前没有pyvenv环境, 需要安装功能相似的virtualenv工具