Python实践提升-开发大型项目
1991 年,在发布 Python 的第一个版本 0.9.0 时,Guido 肯定想不到,这门在当时看来有些怪异、依靠缩进来区分代码块的编程语言,会在之后一路高歌猛进,三十年后一跃成为全世界最为流行的编程语言之一 。
在 2021 年 7 月发布的 TIOBE 编程语言流行榜单上,Python 名列第三,仅次于 C 语言和 Java。
但 Python 的流行并非偶然,简洁的语法、强大的标准库以及极低的上手成本,都是 Python 赢得众人喜爱的重要原因。以我自己为例,我最初就是被 Python 的简洁语法所吸引,而后成为了一名忠实的 Python 爱好者。
但除了那些显而易见的优点外,我喜欢 Python 还有另一个原因:“自由感”。
对我而言,Python 的“自由感”体现在,我既可以用它来写一些快糙猛的小脚本,同时也能用它来做一些真正的“大项目”,解决一些更为复杂的问题。
在任何时候,当遇到某个小问题时,我都可以随手打开一个文本编辑器,马上开始编写 Python 代码。代码写好后直接保存成 .py 文件,然后调用解释器执行,一杯茶的工夫就能解决问题。
而在面对更复杂的需求时,Python 仍然是一个不错的选择。在经历了多年发展后,如今的 Python 有着成熟的打包机制、强大的工具链以及繁荣的第三方生态,无数企业乐于用 Python 来开发重要项目。
在国外,许多大企业在或多或少地使用 Python,YouTube、Instagram 以及 Dropbox 的后台代码几乎完全使用 Python 编写 。而在国内,豆瓣、搜狐邮箱、知乎等许多产品,也大量用到了 Python。
但是,写个几百行代码的 Python 脚本是一码事,参与一个有数万行代码的项目,用它来服务成千上万的用户则完全是另一码事。当项目规模变大,参与人数变多后,许多在写小脚本时完全不用考虑的问题会跳出来:
缩进用 Tab 还是空格?如何让所有人的代码格式保持统一?
为什么每次发布新版本都心惊胆战?如何在代码上线前发现错误?
如何在快速开发新功能的同时,对代码做安全重构?
虽然 Python 有官方的 PEP 8 规范,但在实际项目里,区区纸面规范远远不够。在 13.1 节中,我会介绍一些常用的代码格式化工具,利用这些工具,你可以在大型项目里轻松统一代码风格,提升代码质量。
在开发大型项目时,自动化测试是必不可少的一环。它能让我们可以更容易发现代码里的问题,更好地保证程序的正确性。在 13.2 节中,我会对常用的测试工具 pytest 做简单介绍,同时分享一些实用的单元测试技巧。
希望本章内容能在你参与大型项目开发时提供一些帮助。
13.1 常用工具介绍
在很多事情上,百花齐放是件好事,但在开发大型项目时,百花齐放的代码风格却会毁灭整个项目。
试想一下,在合作开发项目时,如果每个人都坚持自己的一套代码风格,最后的项目代码肯定会破碎不堪、难以入目。因此,在多人参与的大型项目里,最基本的一件事就是让所有人的代码风格保持一致,整洁得就像是出自同一人之手。
下面介绍 4 个与代码风格有关的工具。如果能让所有开发者都使用这些工具,你就可以轻松统一项目的代码风格。
如无特殊说明,本节提到的所有工具都可以通过 pip 直接安装。
13.1.1 flake8
在 1.1.4 节中,我提到 Python 有一份官方代码风格指南:PEP 8。PEP 8 对代码风格提出了许多具体要求,比如每行代码不能超过 79 个字符、运算符两侧一定要添加空格,等等。
但正如章首所说,在开发项目时,光有一套纸面上的规范是不够的。纸面规范只适合阅读,无法用来快速检验真实代码是否符合规范。只有通过自动化代码检查工具(常被称为 Linter)才能最大地发挥 PEP 8 的作用。
Linter 指一类特殊的代码静态分析工具,专门用来找出代码里的格式问题、语法问题等,帮助提升代码质量。
flake8 就是这么一个工具。利用 flake8,你可以轻松检查代码是否遵循了 PEP 8 规范。
比如,下面这段代码:
class Duck:
"""鸭子类
:param color: 鸭子颜色
"""
def __init__(self,color):
self.color= color
虽然语法正确,但如果用 flake8 扫描它,会报出下面的错误:
flake8_demo.py:3:3: E111 indentation is not a multiple of four ➊
flake8_demo.py:8:3: E111 indentation is not a multiple of four
flake8_demo.py:8:20: E231 missing whitespace after ',' ➋
flake8_demo.py:9:15: E225 missing whitespace around operator ➌
❶ PEP 8 规定必须缩进必须使用 4 个空格,但上面的代码只用了 2 个
❷ PEP 8 规定逗号 , 后必须有空格,应改为 def init(self, color):
❸ PEP 8 规定操作符两边必须有空格,应改为 self.color = color
值得一提的是,flake8 的 PEP 8 检查功能,并非由 flake8 自己实现,而是主要由集成在 flake8 里的另一个 Linter 工具 pycodestyle 提供。
除了 PEP 8 检查工具 pycodestyle 以外,flake8 还集成了另一个重要的 Linter,它同时也是 flake8 名字里单词“flake”的由来,这个 Linter 就是 pyflakes。同 pycodestyle 相比,pyflakes 更专注于检查代码的正确性,比如语法错误、变量名未定义等。
以下面这个文件为例:
import os
import re
def find_number(input_string):
"""找到字符串里的第一个整数"""
matched_obj = re.search(r'\d+', input_sstring)
if matched_obj:
return int(matched_obj.group())
return None
假如用 flake8 扫描它,会得到下面的结果:
flake8_error.py:1:1: F401 'os' imported but unused ➊
flake8_error.py:7:37: F821 undefined name 'input_sstring' ➋
❶ os 模块被导入了,但没有使用
❷ input_sstring 变量未被定义(名字里多了一个 s)
这两个错误就是由 pyflakes 扫描出来的。
flake8 为每类错误定义了不同的错误代码,比如 F401、E111 等。这些代码的首字母代表了不同的错误来源,比如以 E 和 W 开头的都违反了 PEP 8 规范,以 F 开头的则来自于 pyflakes。
除了 PEP 8 与错误检查以外,flake8 还可以用来扫描代码的圈复杂度(见 7.3.1 小节),这部分功能由集成在工具里的 mccabe 模块提供。当 flake8 发现某个函数的圈复杂度过高时,会打印下面这种错误:
$ flake8 --max-complexity 8 flake8_error.py ➊
flake8_error.py:5:1: C901 'complex_func' is too complex (12)
❶ --max-complexity 参数可以修改允许的最大圈复杂度,建议该值不要超过 10
如之前所说,flake8 的主要检查能力是由它所集成的其他工具所提供的。而更有趣的是,flake8 其实把这种集成工具的能力完全通过插件机制开放给了我们。这意味着,当我们想定制自己的代码规范检查时,完全可以通过编写一个 flake8 插件来实现。
在 flake8 的官方文档中,你可以找到详细的插件开发教程。一个极为严格的流行代码规范检查工具:wemake-python-styleguide,就是完全基于 flake8 的插件机制开发的。
扫描结果示例:wemake-python-styleguide 对代码的要求极为严格。安装它以后,如果再用 flake8 扫描之前的 find_number() 函数,你会发现许多新错误冒了出来,其中大部分和函数文档有关:
$ flake8 flake8_error.py
flake8_error.py:1:1: D100 Missing docstring in public module
flake8_error.py:1:1: F401 'os' imported but unused
flake8_error.py:6:1: D400 First line should end with a period
flake8_error.py:6:1: DAR101 Missing parameter(s) in Docstring: - input_string
flake8_error.py:6:1: DAR201 Missing "Returns" in Docstring: - return
flake8_error.py:7:37: F821 undefined name 'input_sstring'
由此可见,flake8 是一个非常全能的工具,它不光可以检查代码是否符合 PEP 8 规范,还能帮你找出代码里的错误,揪出圈复杂度过高的函数。此外,flake8 还通过插件机制提供了强大的定制能力,可谓 Python 代码检查领域的一把“瑞士军刀”,非常值得在项目中使用。
13.1.2 isort
在编写模块时,我们会用 import 语句来导入其他依赖模块。假如依赖模块很多,这些 import 语句也会随之变多。此时如果缺少规范,这许许多多的 import 就会变得杂乱无章,难以阅读。
为了解决这个问题,PEP 8 规范提出了一些建议。PEP 8 认为,一个源码文件内的所有 import 语句,都应该依照以下规则分为三组:
(1) 导入 Python 标准库包的 import 语句;
(2) 导入相关联的第三方包的 import 语句;
(3) 与当前应用(或当前库)相关的 import 语句。
不同的 import 语句组之间应该用空格分开。
如果用上面的规则来组织代码,import 语句会变得更整齐、更有规律,阅读代码的人也能更轻松地获知每个依赖模块的来源。
但问题是,虽然上面的分组规则很有用,但要遵守它,比你想的要麻烦许多。试想一下,在编写代码时,每当你新增一个外部依赖,都得先扫一遍文件头部的所有 import 分组,找准新依赖属于哪个分组,然后才能继续编码,整个过程非常烦琐。
幸运的是,isort 工具可以帮你简化这个过程。借助 isort,我们不用手动进行任何分组,它会帮我们自动做好这些事。
举个例子,某个文件头部的 import 语句如下所示:
源码文件:isort_demo.py
import os
import requests
import myweb.models ➊
from myweb.views import menu
from urllib import parse
import django
❶ 其中 myweb 是本地应用的模块名
执行 isort isort_demo.py 命令后,这些 import 语句都会被排列整齐:
import os ➊
from urllib import parse
import django ➋
import requests
import myweb.models ➌
from myweb.views import menu
❶ 第一部分:标准库包
❷ 第二部分:第三方包
❸ 第三部分:本地包
除了能自动分组以外,isort 还有许多其他功能。比如,某个 import 语句特别长,超出了 PEP 8 规范所规定的最大长度限制,isort 就会将它自动折行,免去了手动换行的麻烦。
总之,有了 isort 以后,你在调整 import 语句时可以变得随心所欲,只需负责一些简单的编辑工作,isort 会帮你搞定剩下的所有事情——只要执行 isort,整段 import 代码就会自动变得整齐且漂亮。
13.1.3 black
在 13.1.1 节中,我介绍了 Linter 工具:flake8。使用 flake8,我们可以检验代码是否遵循 PEP 8 规范,保持项目风格统一。
不过,虽然 PEP 8 规范为许多代码风格问题提供了标准答案,但这份答案其实非常宏观,在许多细节要求上并不严格。在许多场景中,同一段代码在符合 PEP 8 规范的前提下,可以写成好几种风格。
以下面的代码为例,同一个方法调用语句可以写成三种风格。
第一种风格:在不超过单行长度限制时,把所有方法参数写在同一行。
User.objects.create(name='andy', gender='M', lang='Python', status='active')
第二种风格:在第二个参数时折行,并让后面的参数与之对齐。
User.objects.create(name='andy',
gender='M',
language='Python',
status='active')
第三种风格:统一使用一层缩进,每个参数单独占用一行。
User.objects.create(
name='andy',
gender='M',