观点 · 我的Python风格
我的Python风格
我怎样组织和编写科研代码,让合作者不必问我,就能读懂、重新运行并继续扩展它。
Python哲学讲的是怎样思考代码;本页讲的是我自己的代码是什么样子。这里写的是习惯,不是规定:有用的就拿去,在别人的代码仓库里则沿用它原有的风格。例子来自我编写或参与编写的代码仓库:“无为”(Effortless,面向罗曼望远镜的快速线性图像重建)、PyIMCOM(它的前身,我主导了它的重构)、mesa_apokasc(红巨星的恒星模型)和ContEvol(一种求解微分方程的数值方法)。
项目怎样组织
- 一个程序包,加上驱动脚本。可以复用的逻辑放进一个以方法命名、可以导入的程序包;只属于某一次运行的选择,比如路径和选项,放进简短的驱动脚本,每个脚本开头都写明怎样运行。
- 模块分层。计算内核在最底层,物理模型在其上,输入输出和分析在最上层。下层模块从不导入上层模块。
- 类的结构与数据一致。在“无为”中,一幅输出图像包含它的各幅输入图像,以及每次处理的一小块区域。每一部分都通过上一层获取设置,因此同一份配置是向下传递的,而不是到处复制。
- 设置也是数据;数据不进git。PyIMCOM把每次运行的设置存在一个JSON文件里,并把包括默认值在内的完整配置写进输出,因为默认值可能会变。
读得懂的代码
- 处处有文档字符串。每个模块开头列出它包含的内容;每个类和公开函数都有NumPy格式的文档字符串,写明每个数组的形状、类型和单位。
- 每个可调的数都有名字。名字用大写字母,注释里写明单位和来源;由它推出的数用公式算出来,而不是再敲一遍。这样,驱动脚本只需一行显眼的代码就能改变程序的行为。
- 命名跟着论文走。
psf_in、psf_out和sigma的含义与公式中相同;循环变量的名字说明它数的是什么。 - 注释解释原因。每段代码前有一句简短的注释,其余注释解释所作的选择,例如:“In principle we should flip the input PSF, but in practice it is easier to flip the weight field.”(原则上应该翻转输入的PSF,但实际上翻转权重场更方便。)
- 报错时说明出错的是什么,比如哪一项设置的值不合法。
先求正确,再求速度
- 先用答案已知的情形检验。ContEvol在匀速圆周运动上、以越来越小的步长测试每一种积分方法,并输出误差;这是任何新方法的第一道检验。
- 每次重构都做回归测试。“无为”会重新运行一个参考算例,检查输出是否保持不变。
- 然后才提速。耗时的循环移进编译后的函数(Numba),每次提速单独提交一次,之后重新检查结果;大数组在一个阶段结束后立即释放。
- 每个输出都能追溯。随机数种子是固定的,配置写进输出文件;耗时的批量任务会跳过已经完成的部分,因此重新提交的任务只做剩下的工作。
图都由代码生成
- 论文中的每一幅图,都由脚本从保存的数据生成,存为矢量PDF,从不手工修改;因此回应审稿意见时,改脚本就够了。
- 统一的样式(字体、刻度,以及用于每个子图的一个小函数),让各篇论文中的图保持一致。
- 每个视觉通道只表示一个变量:颜色表示方法,线型表示另一种选择,标记则让曲线在灰度打印时仍能分辨。每条坐标轴都有带单位的标签。
- 当问题是“这个选择影响有多大”时,我画的是与基准的比值,并在1处画一条参考线。
- 每个作图脚本都注明它生成的是论文中的哪一幅图。
说清改动的提交
- 每次提交只做一件事,标题简短,开头说明改动的类型,例如“Fix calculation of distortion matrices”“Accelerate
SubSlice.__call__with Numba”。 - 改变科学结果的修复,标题以“IMPORTANT:”开头,让手里有旧输出的人知道需要检查。
- 改变代码用法或配置的提交,会附上正文,说明改了什么、为什么改。
- 在共享的代码仓库中,新功能通过分支和拉取请求(pull request)合并。
有AI助手参与时
代码可以由助手来写,但必须有人能读懂、检查并解释其中的每一行(Python哲学)。
- 生成的代码沿用仓库自己的风格,而不是助手的风格。
- 每次改动都保持很小,以便逐一审阅;我解释不了的代码行,提交之前先简化或重写。
- 每条提交信息都说明程序的行为有什么变化、为什么变,并注明助手的贡献。
- 仓库里有一份写给助手的说明:怎样运行和测试代码,数据放在哪里,哪些事情必须先问过人才能做。MapHub是我借助AI(Claude)搭建的,这些做法在其中都能看到。
我保留的习惯
我有几个习惯和最常见的Python风格不同:几条简短的语句如果表达的是同一个意思,就写在同一行(if not mask_out[i]: continue);类通过__call__依次运行各个阶段;探索性笔记本中的开关注明设置的日期。给别人的代码做贡献时,我沿用它的风格,也从不把风格上的改动混进改变程序行为的提交。
有些习惯更适合原型,而不是多人共用的代码。代码的用户多了,它也会添上用户需要的东西:可以安装的程序包,每次改动都会运行的测试,以及取代打印输出的日志信息。