Python进阶笔记:从实验室到生产环境

从笔记本(notebook)到命令行

大多数python初学者都喜欢使用jupyter-notebook写代码。Jupyter-notebook的开发环境非常友好,所见即所得,可以快速测试一些短代码进行概念验证。但是它存在很多问题:不可测试、不可模块化、难以复用、难以进行版本管理……

一个生产可用的python项目应当以命令行为入口,通过执行脚本实现所有设计功能。任何需要传递的参数和选项也应当以标准的命令行传入给程序,然后通过python的sys库读取。实践中更多的是使用click库[1]来处理命令行的数据交互。

代码风格

一致的代码风格对于代码共享和团队协作至关重要。Python官方有PEP8文件[2]提供完整的代码风格指南。一般的项目一般会使用以下任一工具来进行代码风格检查:

以上检查工具都会对代码进行打分,一个企业级高质量python项目应当力争在代码风格上完全遵守PEP8代码规范

此外以上代码风格检查工具都提供了自动格式化功能帮助处理常见的格式问题,但功能并不完善; python代码格式化工具Black(beta公测中)可作为更好的选择:

显式类型声明

尽管python是一个弱类型语言,我们可以任意改变变量的类型而不受任何限制。然而,这种自由度会严重损害代码的可读性。为了促进协作,提升代码可读性,编写代码时应当尽可能的显式声明所有变量类型。Python官方提供了PEP 484 — Type Hints[3]来规范代码中类型标记的使用,同时我们可以使用mypy工具进行类型检查

代码文档

企业级环境下一定会有许多可在项目间复用的代码,这些代码需要有详细的使用文档才能够保证团队的无障碍使用,python官方提供了PEP 257 — Docstring Conventions[4]作为代码注释的标准规范,同时我们也可以使用pydocstyle工具进行注释规范强制检查:

此外我们可以使用文档编译工具根据代码中的注释自动生成文档,常用的工具包括:

Sphinx是python项目使用最为广泛的文档编译工具,但以rst为首选文档编辑语言。doxygen支持python以外的其他语言,mkdocs则有着最好的markdown支持。

除了自动生成的代码文档以外,对于重要的函数和模块,项目文档应当提供足够多的样例和教程保障可重用的代码真正发挥价值。

有些企业会有专门的文档库(例如confluence)存放所有项目文档。尽管如此,代码文档依旧不可缺少,它应当跟随代码一起保存和更新。

一个企业级高质量python项目应当确保完整的文档,否则任何员工的离职都是一场灾难。认真准备文档是对同事和公司负责任的表现。

测试驱动

任何软件项目进入生产环境前都需要足够多的测试,python也不例外。一个可维护的python项目应当包含尽可能多的测试(如单元测试、集成测试、回归测试等),常用的测试工具包括:

同时我们需要关注测试覆盖率,使用pytest-cov[5]等工具检查测试案例是否覆盖了所有的代码,100%测试覆盖率只能作为测试的最低标准,企业级高质量代码需要确保任何代码通过了所有极端案例的测试。

对于开发人员,可以考虑使用测试驱动开发TDD(Test Driven Development)模式工作,不仅能够提升调试(debug)效率,同时能够从一开始就思考如何让代码模块化以便设计单元测试。

虚拟环境

一个python项目往往会依赖许多不同的库,这些依赖库的版本至关重要。一旦使用了不兼容的版本就会导致整个程序的崩溃,一致的依赖库版本管理一直是python调试部署的一大痛点。

传统的解决方案是使用venv[6], conda[7], pipenv[8], poetry[9],pyenv[10]等工具进行不同版本的python虚拟环境管理。但是这些工具都是通过修改环境变量实现在同一个操作系统中的环境切换,无法做到绝对的隔离,往往会造成意料之外的错误和额外的维护成本。特别是一些需要链接库的python包(如lightgbm)很难简单的通过pip安装成功。

实践下来,最优的python虚拟环境解决方案还是Docker,通过Dockerfile定义完全干净隔离的虚拟环境,同时能够很好的解决相关二进制链接库的依赖,并随时可以打包部署到生产环境。

此外docker也可以很方便的启动程序依赖的其他服务(如数据库、内存缓存、消息队列、其他依赖API),实现快速的集成测试。

调试与异常处理

当python代码出现问题时,最直接的测试方案就是使用print把所有相关的变量内容打印出来,它很适合处理一些简单的问题,但其实是一件非常低效的调试手段。更加专业的做法应当是使用pdb工具[11]诊断错误栈。

此外由于python的灵活性,很多函数执行到最后一刻才显现错误,这会耽误很多不必要的时间。因此在撰写代码时,应当尽早进行参数检查,例如在函数执行第一步就进行参数检查,例如长度是否一致、参数是否有效、是否包含必要的属性……这些都是lint工具无法检查出来的问题,应当由开发者根据程序功能撰写规则。

所有运行时检查出来的错误应当根据python的异常机制[12]抛出对应的错误信息,同时应当确保错误原因足够详细到其他开发人员能够据此进行调试。

日志

此外,程序中的print函数会带来大量的屏幕输出,不仅会让重要信息被淹没,也极大影响程序运行效率(print执行的IO开销很大)。

更好的解决方案是使用logging库[13]记录程序运行日志,它与print函数相比能够记录信息的发生时间、严重程度、起源函数,同时支持异步调用避免程序阻塞,并能够输出到文件、屏幕等多个位置,是生产环境下的必备功能。

大部分的python库都用到了logging记录执行日志,使用logging之后可以通过调整全局日志输出等级(例如调整到DEBUG级别),就可以看到所有三方库的执行日志,能够很方便的诊断问题。

简洁架构

一旦python项目超过一个文件,我们就需要开始思考架构问题了:如何设计类与接口,如何组织函数与模块,如何提升应用的可扩展性……这些问题不再是python的语法问题,而需要对软件架构设计有深入的理解和经验。例如我们可以参考最为流行的架构设计SOLID原则:

  • SRP单一职责原则:每一个模块应该只负责一类行为或者职责,避免过度耦合
  • OCP开闭原则:新增功能时应当最小化修改原有代码,实现插件式扩展
  • LSP里氏替换原则:类的继承需要确保子类能够正常实现基类所有接口,否则就不应当继承,或者重新设计基类
  • ISP接口隔离原则:任何模块都不要依赖它不需要的模块功能,带来不必要的运维复杂度
  • DIP 依赖反转选择:代码中抽象接口和具体实现之间需要明确边界,抽象接口被精心设计稳定不变,具体实现只依赖和调用抽象接口,保证代码依赖和控制流反向

发布与版本管理

一个python项目完成后,应当交付什么?简单把所有代码放进一个压缩包是远不够的。

Python项目应当被封装进一个tar或者whl文件中,能够直接被其他人通过pip安装。python官方也提供了文档讲解如何打包python项目[14]

规范打包的代码会有对应的版本号,方便使用者选择需要的版本;会有依赖库版本描述,能让pip安装时自动解析依赖关系。

脚手架 / Scaffold

在一个从零开始的python项目中构建以上内容会非常繁琐,常用的解决方案是使用脚手架(scaffold)自动生成一个项目模版,例如

但这些脚手架并不能完全包含以上所有内容,往往再需要进行额外的修订。每一个企业或者项目组都有必要根据自己的需求,定制并共享属于自己的脚手架


[1] https://click.palletsprojects.com/en/8.0.x/

[2] https://www.python.org/dev/peps/pep-0008/

[3] https://www.python.org/dev/peps/pep-0484/

[4] https://www.python.org/dev/peps/pep-0257/

[5] https://pytest-cov.readthedocs.io/en/latest/

[6] https://docs.python.org/3/library/venv.html

[7] https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html

[8] https://pipenv.pypa.io/en/latest/

[9] https://python-poetry.org/docs/managing-environments/

[10] https://github.com/pyenv/pyenv

[11] https://docs.python.org/3/library/pdb.html

[12] https://docs.python.org/3/tutorial/errors.html

[13] https://docs.python.org/3/library/logging.html

[14] https://packaging.python.org/tutorials/packaging-projects/

发表评论

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

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据