Skip to content

Latest commit

 

History

History
1337 lines (963 loc) · 80.9 KB

File metadata and controls

1337 lines (963 loc) · 80.9 KB

六、开发工具和最佳做法

在开始实际开发hms_sys之前,需要做出几个决定。在现实场景中,这些决策中的一些(可能全部)可能是在策略级别做出的,要么是由开发团队做出的,要么是由团队之上的管理层做出的。有些,例如 IDE/代码编辑器程序,可能是每个团队成员的个人决定;只要不同开发人员的选择之间没有冲突,或者没有因此而引起的任何问题,这没有什么错。另一方面,有一些一致性也不是一件坏事;这样,每个团队成员都知道在编写团队中其他人接触过的代码时会发生什么。

这些选择分为两大类:开发工具的选择以及将采用的最佳实践(和标准),具体如下:

  • 综合发展环境选择
  • 源代码管理选项
  • 代码和开发过程标准,包括将 Python 代码组织到包中
  • Python 虚拟环境的设置和使用

开发工具

毫不奇怪,需要考虑的两个最重要的面向工具的决策围绕着在整个开发生命周期中创建、编辑和管理代码。

集成开发环境(IDE)选项

当然,无需使用成熟的集成开发环境IDE就可以编写和编辑代码。最终,任何可以读写任意类型或具有任意文件扩展名的文本文件的东西在技术上都是可用的。然而,许多 IDE 提供了额外的、以开发为中心的功能,可以节省时间和精力,有时甚至可以节省大量的时间和精力。一般来说,折衷的办法是,任何给定的 IDE 提供的特性和功能越多,它的轻量级就越低,并且可能变得越复杂。找到一个开发团队中每个成员都能同意的方法可能会很困难,甚至是痛苦的——大多数方法都有缺点,而且可能没有一个明显的正确选择。这很主观。

在查看代码编辑和管理工具时,将只检查真正的 IDE。如前所述,文本编辑器可用于编写代码,其中有相当多的编辑器可识别各种语言格式,包括 Python。无论它们有多好(也有一些非常好),如果它们不提供以下功能中的至少一种,就不会被考虑。这只是一个时间问题,直到这个列表中的东西是需要的,而不是可用的,至少,这种可能性会分散注意力,最坏的情况下,它可能是一个关键问题(尽管这似乎不太可能)。特征集标准如下所示:

  • 大型项目支持:为了讨论的目的,大型项目涉及开发两个或多个不同的、可安装的、具有不同环境要求的 Python 软件包。一个例子可能包括一个business_objects类库,该类库由两个单独的包使用,例如online_storeback_office,它们为不同的用户提供不同的功能。这方面的最佳情况包括:

    • 在不同的包项目中支持不同的 Python 解释器(可能作为单独的虚拟环境)
    • 拥有和管理项目间引用的能力(在本例中,online_storeback_office包能够拥有对business_objects库的有用引用)
    • 不太重要但仍然非常有用的是能够同时打开和编辑多个项目,这样,当一个包项目中的更改需要另一个包项目中的相应更改时,进行这些更改的开发人员几乎不需要或根本不需要上下文更改
  • 重构支持:在足够长的时间内,不可避免地需要在不改变系统外部行为的情况下更改系统代码。这是重构的教科书定义。重构工作至少需要能够跨多个文件(可能跨多个库)查找和替换代码中的实体名称。在更复杂的范围内,重构可以包括创建新的类或类的成员,以将功能移动到代码中的不同位置,同时维护代码的接口。

  • L****语言探索:检查项目使用但不是项目一部分的代码的能力是有帮助的,至少偶尔是这样。这比听起来更有用,除非你足够幸运拥有一个易识别的内存,因此永远不必查找函数签名、模块成员等等。

  • C****ode 执行:实际运行正在处理的代码的能力在开发过程中非常有用。为了运行代码、测试对代码的更改而不得不从编辑器中退出到终端中,这是一种上下文更改,而这些更改至少是乏味的,并且在正确的情况下实际上可能会中断流程。

这些项目将按以下等级进行评级,从最好到最差:

  • 棒 极 了
  • 伟大的
  • 好的
  • 公平的
  • 平庸的
  • 贫穷的
  • 可怕的

These are the author's opinion, obviously, so take these with an appropriately sized grain of salt. Your personal views on any or all of these, or your needs for any or all of them, may be substantially different.

许多 IDE 都有各种各样的功能,这些功能可能在很大程度上有助于编写或管理代码,但并不是真正关键的。这些例子包括:

  • 能够从使用代码实体的某个位置导航到定义代码实体的位置
  • 代码完成和自动建议,允许开发人员根据开始键入的实体名称的前几个字符,快速、轻松地从实体列表中选择
  • 代码着色和表示,它提供了一个易于理解的可视化指示,指示给定代码块是什么——注释、类、函数和变量名,诸如此类

这些也将在相同的等级上进行评级,但由于它们不是关键功能,因此它们仅作为附加信息项呈现。

以下所有 IDE 都可以在所有主要的操作系统中使用——Windows、Macintosh 和 Linux(可能还有大多数 UNIX 系统),因此,评估开发工具包 IDE 部分的一个重要标准在所讨论的三个系统中都是没有意义的。

闲置的

IDLE 是一个简单的 IDE,用 Python 编写并使用TkinterGUI,这意味着它应该运行在 Python 可以运行的任何东西上。它通常(但并非总是)是默认 Python 安装的一部分,但即使在默认情况下没有包括它,它也很容易安装,并且不需要太多外部依赖项或其他语言运行时环境。

  • 大型项目支持:差
  • 重构支持:差
  • 语言探索:好
  • 代码执行良好
  • 钟声和口哨:公平

开箱即用,IDLE 不提供任何项目管理工具,尽管可能有一些插件提供了这种功能。即便如此,除非也有插件允许一次打开多个文件而不要求每个文件都位于单独的窗口中,否则跨多个文件使用代码最终将是乏味的,最多也可能是不切实际的,甚至根本不可能。

尽管 IDLE 的搜索和替换功能包括一个很好的特性——基于正则表达式的搜索,但对于重构目的来说,这就是有意义或有用的功能。任何重要的重构工作,甚至是广泛但范围较小的更改,都需要相对较高程度的手动工作。

IDLE 真正的亮点在于它能够深入挖掘系统上可用的包和模块。它提供了一个类浏览器,允许直接探索 Python 路径中的任何可导入命名空间,还提供了一个路径浏览器,允许探索所有可用命名空间。唯一的缺点是缺乏搜索功能,每个类浏览器必须位于单独的窗口中。如果这些都不是问题,那么一个好的评级也不会显得不合时宜。

IDLE 允许任何打开的文件通过一次按键执行,运行的结果/输出显示在一个公共 Python shell 窗口中。没有将参数传递给这些执行的工具,但是如果项目涉及某种接受参数的命令行程序,那么这可能只是一个问题。IDLE 还提供了一个语法检查,用于识别代码中检测到的第一个语法问题,这可能会有一些用处。

IDLE 提供的唯一可靠的功能是代码着色。有些扩展应该提供诸如自动完成和一些代码编写帮助(例如自动生成右括号)之类的功能,但它们似乎都不起作用。

以下是 IDLE 的屏幕截图,显示了控制台、代码编辑窗口、类和路径浏览器窗口以及搜索和替换窗口:

对于小代码工作来说,空闲可能是一个合理的选择——任何不需要打开比用户在各自窗口中显示的文件更多的文件的事情。它是轻量级的,有一个相当稳定(如果偶尔有古怪的话)的 GUI。不过,对于涉及多个可分发包的项目来说,这并不是一种感觉。

杰尼

G****eany是一个轻量级代码编辑器和 IDE,支持多种语言,包括 Python。它可以作为一个可安装的应用程序在所有主要的操作系统上使用,尽管它有一些在 Windows 上不可用的功能。Geany 可从www.Geany.org免费下载:

  • 大型项目支持:公平
  • 重构支持:平庸
  • 语言探索:平庸
  • 代码执行良好
  • 钟声和口哨:很好

这是 Geany 的屏幕截图,显示了几个项目插件的边栏之一、打开的代码文件、项目设置以及搜索和替换窗口:

Geany 的界面使得处理多个并发打开的文件比处理空闲的同一任务容易得多;每个打开的文件都位于 UI 中的一个选项卡中,使多文件编辑变得更容易处理。即使在最基本的安装配置中,它也支持基本的项目结构,并且有一些不同的面向项目的插件,允许更容易/更好地管理和查看项目文件。一般来说,对于大型项目支持来说,它缺少的是同时打开多个项目的能力,尽管支持跨不同项目源代码树打开多个文件。通过仔细的规划和对单个项目设置的明智配置,可以跨一组相关项目管理不同的执行需求,甚至是特定的Python 虚拟环境,尽管这需要一些规则来保持这些良好的隔离和高效。从屏幕截图中可以看出,Geany 还提供了项目级编译和构建/生成命令的设置,这非常方便。

Geany 的重构支持略优于 IDLE,主要是因为其多文件搜索和替换功能。对重构操作没有现成的支持,例如在整个项目或项目集上重命名 Python 模块文件,将其作为一个完全手动的过程,但需要谨慎(以及,再次强调,纪律性),即使这些操作也不难正确管理,尽管它们可能会很繁琐和/或耗时。

Geany 的语言探索能力看起来不应该像平庸的那样获得如此高的评价。除了实际打开与给定项目相关的每个 Python 名称空间(这至少允许在 Symbols 面板中探索这些包)之外,对于挖掘底层语言,显然没有太多可用的支持。Geany 的救赎在这里是一个非常强大的自动完成功能。输入可识别语言元素的前四个字符后(无论该元素是项目中打开的文件的一部分还是导入的模块的一部分),将显示并选择与当前输入文本匹配的所有元素名称,如果所选项是函数或方法,该项出现的代码提示包括该项的参数签名。

Geany 的代码执行能力相当稳定——在几个方面略优于 IDLE,如果还不够的话,或者在足够多的领域,以保证更高的评级。在项目设置的早期对需求和细节进行一些关注,就可以配置给定项目的执行设置,以使用特定的 Python 解释器,例如特定虚拟环境的一部分,并允许从其他项目的虚拟环境安装和代码库导入。缺点是这样做确实需要一定程度的规划,而且在管理相关的虚拟环境时会增加复杂性。

Geany 的开箱即用的铃铛和口哨与 IDLE 提供的铃铛和口哨相当,只有一个显著的改进;大量现成的插件,用于许多常见和有用的任务和需求。

Eclipse 变体+PyDev

Eclipse 平台由 Eclipse 基金会(PurryT05.www. Eclig OrthTy1)管理,旨在为任意数量的语言和开发重点提供健壮、可定制和全功能的 IDE。这是一个开源项目,至少衍生了两个不同的子版本(Aptana Studio,专注于 web 开发)和 LiClipse(专注于 Python 开发)。

这里将使用 LiClipse 安装作为比较的基础,因为它不需要特定于语言的设置来开始编写 Python 代码,但可能值得注意的是,任何 Eclipse 派生的安装都可以访问相同的插件和扩展(PyDev 用于 Python 语言支持,EGit 用于 Git 支持)将提供相同的功能。尽管如此,日食可能并不适合所有人。它可能是一个非常繁重的 IDE,特别是如果它提供对多种语言的支持,并且可能会占用大量的操作内存和 CPU 使用量,即使其支持的语言和功能集受到相当严格的控制:

  • 大型项目支持:很好
  • 重构支持:很好
  • 语言探索:公平
  • 代码执行良好
  • 钟声和口哨:很好

以下是 LiClipse 的屏幕截图,显示了打开的代码文件的代码大纲视图、项目属性以及根据打开的代码文件中的 TODO 注释自动生成的任务列表:

Eclipse 对大型 Python 项目的支持非常好:

  • 可以同时定义并打开多个项目进行修改
  • 每个项目都可以有自己独特的 Python 解释器,它可以是特定于项目的虚拟环境,允许每个项目有不同的包需求,同时还允许执行
  • 可以通过项目引用设置将项目设置为使用其他项目作为依赖项,代码执行将考虑这些依赖项;也就是说,如果代码在另一个项目中运行,而该项目设置为引用/依赖项,则第一个项目仍然可以访问第二个项目的代码和安装的包

跨所有 Eclipse 派生 IDE 的重构支持也相当不错,提供了代码元素重命名的过程,包括模块、变量和方法的提取,以及生成属性和其他代码构造的工具。可能还有其他依赖于上下文的重构功能,因此乍一看显然不可用。

一旦 Python 环境与项目关联,该环境的结构在项目的 UI 中就完全可用。就其本身而言,它允许深入研究通过相关环境可以获得哪些包和功能。不太明显的是,控制点击已安装包的成员(例如,在第 5 章、*hms_sys 系统项目、*或模块提供的urlopen功能的示例代码中的urllib.request上)会将开发人员带到实际成员(方法或属性)项目在其安装中具有的实际模块的。

Eclipse 系列 ide 为 Python 代码提供了相当好的执行能力,不过需要一些时间才能习惯。如果需要,可以执行任何模块或包文件,并显示任何结果,无论是输出结果还是错误结果。特定文件的执行还会生成一个内部运行配置,可以根据需要修改或删除该配置。

Eclipse/PyDev bells 和 whistles 在很大程度上可以与 Geany 和 IDLE 的代码相媲美,并且可以使用和配置结构着色、自动建议和自动完成。LiClipse 特别提供的一个潜在的重要项目是集成的 Git 客户端。在克隆任何存储库之前,LiClipse 的 Git 集成如下所示:

其他

这些不是 Python 开发中唯一可用的 IDE,也不一定是最好的。基于各种专业和半专业团体投票的其他流行选项包括:

  • PyCharm(社区版或专业版):PyCharm 一直是 Python 开发的流行 IDE。它的功能列表包括 Geany 和 Eclipse/PyDev 工具中的大部分功能,还包括与 Git、Subversion 和 Mercurial 版本控制系统的开箱即用集成,以及用于处理各种流行 RDBMS 的 UI 和工具,例如专业版的 MySQL 和 SQL Server。如果它的项目管理功能不会被代码库淹没,那么它可能是 Python 开发 web 应用程序的一个很好的首选。PyCharm 可以在www.jetbrains.com/PyCharm下载。

  • VisualStudio 代码:VS 代码被吹捧为一个闪电般快速的代码编辑器,并通过一系列用于各种语言和用途的扩展提供了大量功能。尽管它是支持 Python 的最新 IDE 之一,但它正在迅速成为脚本任务的流行选择,并且在更大的、以应用程序为中心的工作中也有很大的潜力。Visual Studio 可在**code.visualstudio.com下载。**

*** 忍者 IDE:从其功能列表来看,忍者拥有 Geany 提供的大部分相同的基本功能,并添加了一个听起来有用且吸引人的单一内置项目管理子系统。忍者 IDE 可在**忍者 IDE.org下载****

****# 源代码管理

无论描述为版本或修订控制系统、源代码管理SCM)还是其他名称,更常见和更流行的 SCM 提供了一系列特性和功能,使开发过程的某些方面更容易、更快,或者至少更稳定。这些措施包括:

  • 允许多个开发人员在同一代码库的相同部分上进行协作,而不必担心(同样多)会覆盖彼此的工作
  • 跟踪代码库的所有版本,以及在提交新版本时,谁对代码库进行了哪些更改
  • 提供提交每个新版本时所做更改的可见性
  • 为特定目的维护同一代码库的不同版本,其中最常见的变化可能是为不同的环境提供代码更改处理和升级的版本,这可能包括:
    • 地方发展环境
    • 共享开发环境,所有开发人员的本地代码更改首先混合在一起
    • 用于 QA 和更广泛的集成测试的共享测试服务器
    • 一个用户验收测试服务器,使用真实的、类似于生产的数据,可用于向需要最终批准升级到实时环境或构建的更改的人演示功能
    • 可以完全访问生产数据的完整副本的暂存环境,着眼于能够执行需要访问该数据集的加载和其他测试
    • 实时环境/构建代码库

从开发者的角度来看,虽然这些系统在幕后的运行方式至少有一些主要的变化,但这些功能差异可能并不重要,只要它们按照预期运行并且运行良好。综上所述,这些基本功能,以及它们与各种手动操作的组合,允许以下功能:

  • 开发人员可以回滚到完整代码库的早期版本,对其进行更改,并将其作为新版本重新提交,这对以下方面很有用:

    • 查找、删除或修复在提交甚至升级后意外引发重大问题的更改
    • 创建新的代码分支,用其他方法实现提交的功能
  • 具有不同专业领域的多个开发人员可以处理同一问题和/或代码的部分,从而使他们能够更快地解决该问题或编写代码

  • 具有较强体系结构背景或技能集的开发人员可以定义基本的代码结构(可能是类及其成员),然后将它们提交给其他人完全实现

  • 系统领域专家可以轻松地审查对代码库的更改,在这些更改升级到无情的环境之前识别功能或性能的风险

  • 配置管理器可以访问不同版本的代码库并将其部署到不同的目标环境中

一个好的 SCM 系统,尤其是如果它与开发和代码推广过程有良好的联系,可能还有很多其他更具体的应用程序可以帮助管理。

典型的供应链管理活动

对于任何 SCM,无论哪种 SCM 正在使用,也不管具体的命令变化,最常用的模式可能是以下操作序列:

  • 获取给定代码库的版本:
    • 通常,这将是最新的版本,可能来自特定的开发分支,但是任何需要检索的分支或版本都可以获取。在任何情况下,该过程都会在本地文件系统的某个位置生成所请求代码库的完整副本,以供编辑。
  • 更改代码的本地副本。
  • 在提交更改之前协调任何差异:
    • 此步骤的目标是提取对同一代码库所做的任何更改,并查找和解决本地更改与其他人在同一代码中所做更改之间的任何冲突。当前的几个 SCM 允许在提交到共享存储库之前进行本地提交。在这些 SCM 中,在将代码提交到共享存储库之前,这种协调可能没有那么重要,但在每次本地提交时这样做通常会将冲突的解决分解为更小、更易于管理的块。
  • 提交到共享存储库:
    • 完成后,所做的更改现在可供其他开发人员检索(并在必要时协调冲突)。

这种使用模式可能包含大多数开发工作,包括在已建立的分支上工作,而不需要新分支。创建新的分支也很常见,尤其是当现有代码库的大部分预期会发生重大更改时。在不同的环境中使用嵌套分支也是一种常见的策略,在这些环境中,更深层次的分支在升级到更稳定的分支之前仍有待于一些审查或接受。

分支结构如下所示:

将代码从[dev]分支提升到[test]的过程简化为向上合并,将代码从较低分支复制到较高分支,然后在必要时从较高分支再次向下分支到较低分支。

为特定项目创建单独的分支机构也很常见,特别是如果有两个或两个以上的工作正在进行,可能会产生广泛和/或重大的变化,尤其是如果这些工作预计会相互冲突。项目特定分支通常来自共享开发分支,如下所示:

[project1][project2]分支的代码完成时,它将被提交到自己的分支,然后合并到现有的[dev]分支中,检查并解决过程中的任何冲突。

有几十种可用的 SCM,其中大约有十几种是免费的开源系统。最流行的系统有:

  • 吉特(大幅度)
  • 颠覆
  • 汞的

吉特

Git 在很大程度上是目前使用的最流行的 SCM。它是一个分布式 SCM 系统,以极低的成本保留代码库和其他内容的本地分支,同时仍然能够将本地提交的代码推送到共享的中央存储库中,然后多个用户可以访问和使用该存储库。最重要的是,它能够处理大量并发提交(或补丁)活动,这并不奇怪,因为它是为适应 Linux 内核开发团队的工作而编写的,一次可能有数百个这样的补丁/提交。如果使用命令行是首选方法的话,它速度快、效率高,而且用于满足大多数日常需求的基本功能的命令也可以很容易地提交到内存中

Git 在普通命令和进程之外的功能要比这些进程本身的功能多,也就是说,有八到九个命令可能包含前面提到的获取/编辑/协调/提交步骤,但 Git 总共有 21 个命令,其他 12 到 13 个命令提供的功能不太常用。传闻证据表明,大多数开发人员,除非他们从事的项目规模或复杂程度超过一定程度,否则可能更接近这些人所处的领域的尽头:

Git 也不缺少 GUI 工具,尽管许多 IDE,无论是为了尽量减少上下文切换,还是出于其他原因,都为 Git 提供了某种类型的接口,即使它是通过可选的插件实现的。其中最好的还将在某些进程(例如提交或推送)出现问题时进行检测,并提供有关如何解决这些问题的指导。还有一些独立的 Git GUI 应用程序,甚至与内置系统工具(如 TortoiseGit)()集成 https://tortoisegit.org/ ),它将 Git 功能添加到 Windows 文件资源管理器中。

颠覆

Subversion(或 SVN)是一种较旧的 SCM,从 2004 年初开始使用。它是目前仍在使用的最流行的非分布式 SCM 之一。与之前的大多数 SCM 一样,SVN 为其跟踪的每个签出分支存储代码和内容的完整本地副本,并在提交过程中上载这些副本(可能全部)。它也是一个集中式系统,而不是分布式系统,这意味着所有分支和合并都必须发生在代码库的主副本上,无论它位于何处。

尽管 Git 有各种各样的幕后差异和流行性,但是 SVN 对于跨团队管理源代码来说是一个完全可行的选择,即使它的效率和流行性不如 Git。它完全支持典型的 get-edit-commit 工作周期,只是没有 Git 提供的灵活性。

比较 Git 和 SVN 的基本工作流

尽管所有主流的 SCM 都支持基本的签出、工作、合并和提交工作流,但值得一看 Git 需要的一些附加流程步骤。很明显,每个附加步骤都是开发人员在完全提交代码之前必须执行的附加任务,尽管它们都不一定是长时间运行的任务,因此影响很少很大。另一方面,所涉及的每个附加步骤都提供了一个附加点,在附加到代码的主版本之前,可以对代码进行附加修改。

比较Git 工作流(左)和SVN 工作流(右):

  • 获取代码的当前版本和编辑代码的过程基本相同。

  • Git 允许开发者阶段性变更。然而,也许五分之三的文件中对代码的修改已经完成,并且已经准备好提交,至少在本地提交,而在其他两个文件上仍然需要做大量的工作。由于在提交之前必须在 Git 中暂存更改,因此可以暂存完成的文件,然后单独提交,而其他文件仍在进行中。未提交的暂存文件仍然可以根据需要进行编辑和重新暂存(或不进行);在实际提交更改集之前,所有内容仍处于进行中状态。

  • Git 的提交更改是对本地存储库的更改,这再次意味着可以继续进行编辑,以及对本地提交的操作,直到最终主存储库提交所需的一切正常为止。

  • 两者都提供在最终的推送提交给主控操作之前从主控执行合并的能力。实际上,这可能发生在最终提交之前的任何时候,但是 Git 的 stage-then-commit 方法的粒度非常适合在更小、更易于管理的块中进行,这通常意味着主源代码中的任何合并也将更小、更易于管理。在 SVN 方面,没有理由不执行类似的定期合并,只是更容易记住在开发过程中的本地提交例程中这样做。

其他 SCM 选项

无论如何,Git 和 SVN 不是唯一可用的选项。下一个最流行的选项如下:

  • Mercurial一种免费的开源 SCM,用 Python 编写,使用 Git 这样的分布式结构,但不需要 Git 那样的变更暂存操作。Mercurial 已被谷歌和 Facebook 内部采用。

*** Performance****Helix Core一种专有的分布式 SCM,至少在某种程度上与 Git 命令兼容,面向企业客户和使用。**

****# 最佳做法

围绕开发的标准和最佳实践有很多,至少当涉及的代码库超过一定的复杂程度时是这样。它们之所以被认为是这样,是因为它们解决(或防止)了如果不遵循它们可能会出现的各种困难。他们中的相当一部分人还关注(如果是间接的话)未来验证代码的某些方面,至少是从试图让新开发人员(或同一开发人员,可能几年后)更容易理解代码的功能、如何找到特定的代码块,或者,可能是扩展或重构代码的角度来看。

不管编程语言如何,这些指导原则大致分为两类:

  • **代码标准:**关注代码结构和组织的指南和概念,但不一定关注代码的功能——更多的是保持代码易于理解和导航

  • **流程标准:**围绕确保代码行为良好以及以最少的麻烦和干扰对其进行更改的指导方针和概念

Python 在这个组合中又添加了两个项目,这两个项目都不太符合语言不可知论的范畴;它们是 Python 环境中的功能和功能需求的结果,具体而言:

  • **包组织:**如何最好地在文件系统级别构造代码;何时何地生成新模块文件和包目录

  • **何时以及如何使用 Python 虚拟环境:**它们的用途是什么,以及如何在给定的代码集合中最好地利用它们

规范标准

归根结底,代码级标准与其他标准一样,都是为了确保代码本身以可预测且易于理解的方式编写和构造。当遵循这些标准时,当使用代码库的开发人员合理地理解这些标准时,期望任何开发人员,即使是可能从未见过给定代码块的开发人员,仍然能够做到以下几点也不是不合理的:

  • 阅读并更容易理解代码及其作用
  • 快速方便地查找只能通过名称或名称空间标识的代码元素(类、函数、常量或其他项)
  • 在现有结构中创建也符合这些标准的新代码元素
  • 修改现有的代码元素,并了解哪些与标准相关的项目需要根据这些更改进行修改(如果有)

Python 社区有一套指导方针(PEP-8),但也可能有其他的内部标准。

PEP-8

至少 Python 的一些 DNA 与这样一个观察结果有关:代码通常在编写时被读取的频率更高。这是其语法的重要功能方面的基础,特别是那些与 Python 代码结构相关的方面,例如使用缩进来指示功能块。或许,最早的Python 增强方案PEPs)之一就是关注如何在风格变化没有功能意义的情况下保持代码的可读性,这也就不足为奇了。PEP-8 是一个很长的规范,如果直接从当前的 Python 页面(www.Python.org/dev/peps/PEP-0008打印出来,大约有 29 页),但是重要的方面值得在此总结。

第一个,也许也是最重要的一个,是认识到如果所有 Python 代码都遵循相同的标准是理想的,但是有很多理由不这样做(参见愚蠢的一致性是 PEP-8 中的小头脑的妖怪。包括但不限于以下内容:

  • 当应用 PEP-8 风格的指南时,代码的可读性会降低,即使对于习惯于阅读遵循标准的代码的人来说也是如此
  • 与周围不遵守的法规保持一致(可能出于历史原因)
  • 因为除了样式指南之外,没有其他理由对代码进行更改
  • 如果遵守这些准则会破坏向后兼容性(更不用说功能了,尽管这似乎不太可能)

PEP-8 特别指出,它是一个样式指南,如 Solidity v0.3.0 的样式指南介绍中所述:

"A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important".

这意味着可能有很好的(或至少是有理由的)理由不遵守部分或全部准则,即使是新准则。示例可能包括以下内容:

  • 使用另一种语言的命名约定,因为功能是等效的,例如在提供相同的文档对象模型DOM的 Python 类库中使用 JavaScript 命名约定)跨服务器端类库的操作功能,用于创建和使用 DOM 对象
  • 使用非常特定的文档字符串结构或格式,以符合适用于整个业务中所有代码(Python 或其他)的文档管理系统的要求
  • 符合与 PEP-8 建议的标准相矛盾的其他内部标准

但最终,由于 PEP-8 是一套风格指南,而不是功能指南,最糟糕的情况是有人会抱怨代码不符合公众接受的标准。如果您的代码永远不会在您的组织之外共享,那么这可能永远不会成为一个问题。

PEP-8 指南中有三个松散的分组,其成员可简要总结:

代码布局

  • 每层缩进应为四个空格:
    • 不要使用标签
    • 悬挂缩进应尽可能使用相同的规则集,详情和建议见 PEP-8 页
  • 功能行的长度不应超过 79 个字符,长文本字符串的长度应限制为每行 72 个字符,包括缩进空格
  • 如果一行必须在运算符(+、-、*、或等)周围打断,请在运算符之前打断它
  • 用两个空行环绕顶级函数和类定义

注释

  • 与代码冲突的注释比没有注释更糟糕。当代码更改时,始终优先保持注释最新!
  • 评论应该是完整的句子。第一个单词应该大写,除非它是以小写字母开头的标识符(不要改变标识符的大小写!)。
  • 块注释通常由一个或多个由完整句子组成的段落组成,每个句子以句号结尾。

命名约定

  • 包和模块应具有短名称,并使用lowercase或(如有必要)lowercase_words命名约定
  • 类名应使用CapWords命名约定
  • 函数和方法应使用lowercase_words命名约定
  • 常量应使用CAP_WORDS命名约定

PEP-8 中提到的其他项目太长,无法在此进行有效总结,包括:

  • 源文件编码(感觉它可能很快就不再是一个问题)
  • 进口
  • 表达式和语句中的空格
  • 文档字符串(有自己的 PEP:www.python.org/dev/peps/PEP-0257
  • 为继承而设计

hms_sys项目的开发过程中,这些建议以及 PEP-8 的实质性编程建议部分将在代码中予以遵循,而不会与其他标准发生冲突。

内部标准

任何给定的开发工作、团队甚至公司都可能对代码的编写或结构有特定的标准和期望。也可能存在功能标准,例如定义将使用何种类型的外部系统来提供系统使用的各种功能、支持哪些 RDBMS 引擎、将使用哪些 web 服务器等的策略。在本书中,功能标准将在开发过程中确定,但一些代码结构和格式标准将在这里和现在定义。作为起点,PEP-8 代码布局、注释和命名约定标准将适用。除此之外,还有一些代码组织和类结构标准也将发挥作用。

模块中的代码组织

将遵循 PEP-8 结构和顺序指南,使用模块级文档字符串,从__future__导入,各种 dunder 名称(支持from [module] import [member]使用模块成员的__all__列表,以及一些关于模块的标准__author____copyright____status__元数据),然后从标准库导入,然后是第三方库,最后是内部库。

之后,代码将按成员类型按此顺序组织和分组,每个元素按字母顺序排列(除非有功能上的原因说明该顺序不可行,例如类依赖于或继承尚未定义的其他类(如果它们是严格顺序的话):

  • 模块级常数
  • 在模块中定义的自定义异常
  • 功能
  • 用作正式接口的抽象基类
  • 用作标准抽象类或混合类的抽象基类
  • 具体类别

所有这些结构约束的目标都是在整个代码库中提供一些可预测性,以便轻松定位给定的模块成员,而不必每次都搜索它。现代 IDE 能够控制在代码中单击成员名称并直接跳转到该成员的定义,这可以说是不必要的,但如果代码将被没有访问此类 IDE 权限的人查看或阅读,那么以这种方式组织代码仍然有一定的价值。

因此,模块和包头文件遵循一个非常特定的结构,该结构在一组模板文件中设置,一个用于通用模块,另一个用于包头(__init__.py模块)。从结构上看,它们是相同的,只是在起始文本/内容上有一些细微的变化。module.py模板如下:

#!/usr/bin/env python
"""
TODO: Document the module.
Provides classes and functionality for SOME_PURPOSE
"""

#######################################
# Any needed from __future__ imports  #
# Create an "__all__" list to support #
#   "from module import member" use   #
#######################################

__all__ = [
    # Constants
    # Exceptions
    # Functions
    # ABC "interface" classes
    # ABC abstract classes
    # Concrete classes
]

#######################################
# Module metadata/dunder-names        #
#######################################

__author__ = 'Brian D. Allbee'
__copyright__ = 'Copyright 2018, all rights reserved'
__status__ = 'Development'

#######################################
# Standard library imports needed     #
#######################################

# Uncomment this if there are abstract classes or "interfaces" 
#   defined in the module...
# import abc

#######################################
# Third-party imports needed          #
#######################################

#######################################
# Local imports needed                #
#######################################

#######################################
# Initialization needed before member #
#   definition can take place         #
#######################################

#######################################
# Module-level Constants              #
#######################################

#######################################
# Custom Exceptions                   #
#######################################

#######################################
# Module functions                    #
#######################################

#######################################
# ABC "interface" classes             #
#######################################

#######################################
# Abstract classes                    #
#######################################

#######################################
# Concrete classes                    #
#######################################

#######################################
# Initialization needed after member  #
#   definition is complete            #
#######################################

#######################################
# Imports needed after member         #
#   definition (to resolve circular   #
#   dependencies - avoid if at all    #
#   possible                          #
#######################################

#######################################
# Code to execute if the module is    #
#   called directly                   #
#######################################

if __name__ == '__main__':
    pass

模块模板和包头模板之间唯一的真正区别是初始文档,并且在__all__列表中包含子包和模块名称空间成员时有一个特定的标注:

#!/usr/bin/env python
"""
TODO: Document the package.
Package-header for the PACKAGE_NAMESPACE namespace. 
Provides classes and functionality for SOME_PURPOSE """

#######################################
# Any needed from __future__ imports  #
# Create an "__all__" list to support #
#   "from module import member" use   #
#######################################

__all__ = [
    # Constants
    # Exceptions
    # Functions
    # ABC "interface" classes
    # ABC abstract classes
    # Concrete classes
 # Child packages and modules ]

#######################################
# Module metadata/dunder-names        #
#######################################

# ...the balance of the template-file is as shown above...

将这些文件作为模板文件提供给开发人员使用也可以使启动新模块或包更快、更容易。将文件或其内容复制到一个新文件比创建一个新的空白文件需要几秒钟的时间,但是让结构准备好开始编码使维护相关标准变得更加容易。

课程结构和标准

类定义,无论是针对具体/实例类还是任何 ABC 变量,都定义了类似的结构,并将按如下顺序分组排列:

  • 类属性和常量
  • 属性吸气剂方法
  • 属性设置器方法
  • 属性删除器方法
  • 实例属性定义
  • 对象初始化(__init__
  • 对象删除(__del__
  • 实例方法(具体或抽象)
  • 标准内置方法的覆盖(__str__
  • 类方法
  • 静态方法

选择了属性 getter、setter 和 deleter 方法,而不是使用方法修饰,以便更容易将属性文档保存在类定义中的单个位置。属性的使用(从技术上讲,它们是托管属性,但属性是一个较短的名称,在多种语言中具有相同的含义)与一般属性不同,这是对单元测试需求的让步,也是对提出尽可能接近其原因的错误的策略的让步。在过程标准部分的单元测试部分,将很快讨论这两个问题。

然后,混凝土类模板包含以下内容:

# Blank line in the template, helps with PEP-8's space-before-and-after rule
class ClassName:
    """TODO: Document the class.
Represents a WHATEVER
"""
    ###################################
    # Class attributes/constants      #
    ###################################

    ###################################
    # Property-getter methods         #
    ###################################

#     def _get_property_name(self) -> str:
#         return self._property_name

    ###################################
    # Property-setter methods         #
    ###################################

#     def _set_property_name(self, value:str) -> None:
#         # TODO: Type- and/or value-check the value argument of the 
#         #       setter-method, unless it's deemed unnecessary.
#         self._property_name = value

    ###################################
    # Property-deleter methods        #
    ###################################

#     def _del_property_name(self) -> None:
#         self._property_name = None

    ###################################
    # Instance property definitions   #
    ###################################

#     property_name = property(
#         # TODO: Remove setter and deleter if access is not needed
#         _get_property_name, _set_property_name, _del_property_name, 
#         'Gets, sets or deletes the property_name (str) of the instance'
#     )

    ###################################
    # Object initialization           #
    ###################################

    # TODO: Add and document arguments if/as needed
    def __init__(self):
        """
Object initialization.

self .............. (ClassName instance, required) The instance to 
                    execute against
"""
        # - Call parent initializers if needed
        # - Set default instance property-values using _del_... methods
        # - Set instance property-values from arguments using 
        #   _set_... methods
        # - Perform any other initialization needed
        pass # Remove this line 

    ###################################
    # Object deletion                 #
    ###################################

    ###################################
    # Instance methods                #
    ###################################

#     def instance_method(self, arg:str, *args, **kwargs):
#         """TODO: Document method
# DOES_WHATEVER
# 
# self .............. (ClassName instance, required) The instance to 
#                     execute against
# arg ............... (str, required) The string argument
# *args ............. (object*, optional) The arglist
# **kwargs .......... (dict, optional) keyword-args, accepts:
#  - kwd_arg ........ (type, optional, defaults to SOMETHING) The SOMETHING 
#                     to apply
# """
#         pass

    ###################################
    # Overrides of built-in methods   #
    ###################################

    ###################################
    # Class methods                   #
    ###################################

    ###################################
    # Static methods                  #
    ###################################
# Blank line in the template, helps with PEP-8's space-before-and-after rule

除了几乎总是要实现的__init__方法之外,实际的功能元素、属性和方法都被注释掉了。这允许模板中出现预期的标准,如果开发人员愿意,他们可以简单地复制和粘贴他们需要的任何代码存根,取消对整个粘贴块的注释,重命名需要重命名的内容,然后开始编写代码。

抽象类的模板文件与具体类模板非常相似,添加了一些项以适应具体类中不存在的代码元素:

# Remember to import abc!
# Blank line in the template, helps with PEP-8's space-before-and-after rule
class AbstractClassName(metaclass=abc.ABCMeta):
    """TODO: Document the class.
Provides baseline functionality, interface requirements, and 
type-identity for objects that can REPRESENT_SOMETHING
"""
    ###################################
    # Class attributes/constants      #
    ###################################

    # ... Identical to above ...

    ###################################
    # Instance property definitions   #
    ###################################

#     abstract_property = abc.abstractproperty()

#     property_name = property(

    # ... Identical to above ...

    ###################################
    # Abstract methods                #
    ###################################

#     @abc.abstractmethod
#     def instance_method(self, arg:str, *args, **kwargs):
#         """TODO: Document method
# DOES_WHATEVER
# 
# self .............. (AbstractClassName instance, required) The 
#                     instance to execute against
# arg ............... (str, required) The string argument
# *args ............. (object*, optional) The arglist
# **kwargs .......... (dict, optional) keyword-args, accepts:
#  - kwd_arg ........ (type, optional, defaults to SOMETHING) The SOMETHING 
#                     to apply
# """
#         pass

    ###################################
    # Instance methods                #
    ###################################

    # ... Identical to above ...

    ###################################
    # Static methods                  #
    ###################################
# Blank line in the template, helps with PEP-8's space-before-and-after rule

类似的模板也可用于用作正式接口的类定义;定义类实例的功能需求,但不提供这些需求的任何实现的类。它看起来非常像抽象类模板,除非进行一些名称更改和删除任何属于或暗示具体实现的内容:

# Remember to import abc!
# Blank line in the template, helps with PEP-8's space-before-and-after rule
class InterfaceName(metaclass=abc.ABCMeta):
    """TODO: Document the class.
Provides interface requirements, and type-identity for objects that 
can REPRESENT_SOMETHING
"""
    ###################################
    # Class attributes/constants      #
    ###################################

    ###################################
    # Instance property definitions   #
    ###################################

#     abstract_property = abc.abstractproperty()

    ###################################
    # Object initialization           #
    ###################################

    # TODO: Add and document arguments if/as needed
    def __init__(self):
        """
Object initialization.

self .............. (InterfaceName instance, required) The instance to 
                    execute against
"""
        # - Call parent initializers if needed
        # - Perform any other initialization needed
        pass # Remove this line 

    ###################################
    # Object deletion                 #
    ###################################

    ###################################
    # Abstract methods                #
    ###################################

#     @abc.abstractmethod
#     def instance_method(self, arg:str, *args, **kwargs):
#         """TODO: Document method
# DOES_WHATEVER
# 
# self .............. (InterfaceName instance, required) The 
#                     instance to execute against
# arg ............... (str, required) The string argument
# *args ............. (object*, optional) The arglist
# **kwargs .......... (dict, optional) keyword-args, accepts:
#  - kwd_arg ........ (type, optional, defaults to SOMETHING) The SOMETHING 
#                     to apply
# """
#         pass

    ###################################
    # Class methods                   #
    ###################################

    ###################################
    # Static methods                  #
    ###################################
# Blank line in the template, helps with PEP-8's space-before-and-after rule

综上所述,这五个模板应该为编写大多数项目中更常见的元素类型的代码提供坚实的起点。

函数和方法注释(提示)

如果您以前使用过 Python 函数和方法,那么您可能已经注意到并想知道前面模板文件中某些方法中的一些意外语法,特别是此处粗体显示的项目:

def _get_property_name(self) -> str:

def _set_property_name(self, value:str) -> None:

def _del_property_name(self) -> None:

def instance_method(self, arg:str, *args, **kwargs):

这些是 Python 3 中支持的类型提示示例。hms_sys代码也将遵循的标准之一是,所有方法和函数都应该是类型提示的。最终产生的注释可能会被用于使用装饰器强制执行参数的类型检查,甚至在以后可能会有助于简化单元测试。在短期内,人们期望自动文档生成系统会关注这些问题,因此它们现在是内部标准的一部分。

类型暗示可能已经足够新了,它还没有被广泛使用,因此,对它的功能和工作原理的深入了解可能值得一试。在执行时考虑以下未注释函数及其结果:

def my_function(name, price, description=None):
    """
A fairly standard Python function that accepts name, description and 
price values, formats them, and returns that value.
"""
    result = """
name .......... %s
description ... %s
price ......... %0.2f
""" % (name, description, price)
    return result

if __name__ == '__main__':
    print(
        my_function(
            'Product #1', 12.95, 'Description of the product'
        )
    )
    print(
        my_function(
            'Product #2', 10
        )
    )

执行该代码的结果看起来不错:

正如 Python 函数一样,这非常简单。my_function函数需要nameprice,还允许description参数,但这是可选的,默认为None。函数本身只是将所有这些值收集到一个格式化的字符串值中并返回它。price参数应该是某种类型的数值,其他参数应该是字符串(如果存在)。在本例中,根据参数名称,这些参数值的预期类型可能是显而易见的。

不过,price 参数可以是几种不同的数值类型中的任何一种,并且仍然可以使用函数-intfloat值,因为代码运行时没有错误。一个decimal.Decimal值,甚至一个complex类型,也会如此荒谬。因此,类型提示注释语法的存在是为了提供一种方法来指示所期望或返回的值的类型,而不需要这些类型。

这是相同的函数,暗示:

def my_function(name:str, price:(float,int), description:(str,None)=None) -> str:
    """
A fairly standard Python function that accepts name, description and 
price values, formats them, and returns that value.
"""
    result = """
name .......... %s
description ... %s
price ......... %0.2f
""" % (name, description, price)
    return result

if __name__ == '__main__':
    print(
        my_function(
            'Product #1', 12.95, 'Description of the product'
        )
    )
    print(
        my_function(
            'Product #2', 10
        )
    )

 # - Print the __annotations__ of my_function
    print(my_function.__annotations__)

这里唯一的区别是每个参数后的类型提示注释和函数第一行末尾的返回类型提示,它们指示每个参数的预期类型以及调用函数的结果:

my_function(name:str, price:(float,int), description:(str,None)=None) -> str:

函数调用的输出是相同的,但函数的__annotations__属性显示在输出的末尾:

所有类型提示注释真正做的是填充my_function__annotations__属性,如前一次执行结束时所示。本质上,它们提供了有关函数本身的元数据,并附加到函数本身,以便以后使用。

因此,所有这些标准合在一起旨在实现以下目标:

  • 帮助保持代码尽可能可读(PEP-8 基准约定)

  • 保持文件中代码的结构和组织可预测(模块和类元素组织标准)

  • 使创建符合这些标准(各种模板)的新元素(模块、类等)变得容易

  • 针对自动化文档生成、方法和功能的类型检查以及稍后可能探索的一些单元测试效率(类型提示注释),提供某种程度的未来证明

工艺标准

流程标准涉及针对某个代码库执行哪些流程,以实现多种目的。最常见的两个独立实体如下:

  • **单元测试:**确保代码经过测试,并且可以根据需要重新测试,从而确保代码按预期工作

  • **可重复的构建过程:**设计的目的是,无论您使用什么构建过程,也可能是作为结果的安装过程,都是自动化的、无错误的、可按需重复的,同时需要尽可能少的开发人员时间来执行

综上所述,这两个因素也导致了集成单元测试和构建过程的想法,因此,如果需要或需要,构建过程可以确保其结果输出已经过测试。

单元测试

对于人们,甚至开发人员来说,将单元测试视为确保代码库中不存在 bug 的过程并不罕见。尽管至少在较小的代码库中有相当多的事实,但这实际上更多地是单元测试背后真正目的的结果:单元测试是确保代码在所有合理可能的执行情况下以可预测的方式运行。差别可能很微妙,但仍然很重要。

让我们从单元测试的角度再看一看前面的my_function。它有三个参数,一个是必需的字符串值,一个是必需的数字值,还有一个是可选的字符串值。它不会根据这些值或它们的类型做出任何决定,它只是将它们转储到一个字符串中并返回该字符串。让我们假设提供的参数是产品的属性(这是输出所暗示的,即使事实并非如此)。即使不涉及任何决策,功能的某些方面也会产生错误,或者在这种情况下可能会出现错误:

  • 传递非数值price 值将引发TypeError,因为字符串格式不会使用指定的%0.2f格式格式化非数值
  • 传递一个负的price值可能会引起错误,除非一个产品实际上有可能有一个负的价格,这是没有意义的
  • 传递一个数值,但不是实数的price值(如complex号)可能会引发错误
  • 传递一个空的name值可能会引发一个错误。让我们假定的产品名称接受一个空值是没有意义的
  • 传递多行name值可能会引发错误
  • 传递非字符串name值可能也会引发错误,原因与传递非字符串description值类似

除了列表中的第一项之外,这些都是函数本身的潜在缺陷,目前没有一项会引起任何错误,但所有这些都很可能导致不良行为。

漏洞。

The following basic test code is collected in the test-my_function.py module.

即使没有正式的单元测试结构,也不难编写代码来测试所有良好参数值的代表性集合。首先,必须定义这些值:

# - Generate a list of good values that should all pass for:
#   * name
good_names = [
    'Product', 
    'A Very Long Product Name That is Not Realistic, '
        'But Is Still Allowable',
    'None',  # NOT the actual None value, a string that says "None"
]
#   * price
good_prices = [
    0, 0.0, # Free is legal, if unusual.
    1, 1.0, 
    12.95, 13, 
]
#   * description
good_descriptions = [
    None, # Allowed, since it's the default value
    '', # We'll assume empty is OK, since None is OK.
    'Description',
    'A long description. '*20,
    'A multi-line\n\n description.'
]

然后,简单地迭代所有好的组合,并跟踪结果中出现的任何错误:

# - Test all possible good combinations:
test_count = 0
tests_passed = 0
for name in good_names:
    for price in good_prices:
        for description in good_descriptions:
            test_count += 1
            try:
                ignore_me = my_function(name, price, description)
                tests_passed += 1
            except Exception as error:
                print(
                    '%s raised calling my_function(%s, %s, %s)' % 
                    (error.__class__.__name__, name, price, description)
                )
if tests_passed == test_count:
    print('All %d tests passed' % (test_count))

执行该代码的结果看起来不错:

接下来,采用类似的方法为每个参数定义坏值,并用已知的好值检查每个可能的坏值:

# - Generate a list of bad values that should all raise errors for:
#   * name
bad_names = [
   None, -1, -1.0, True, False, object()
]
#   * price
bad_prices = [
    'string value', '', 
    None, 
    -1, -1.0, 
    -12.95, -13, 
]
#   * description
bad_description = [
   -1, -1.0, True, False, object()
]

# ...

for name in bad_names:
    try:
        test_count += 1
        ignore_me = my_function(name, good_price, good_description)
        # Since these SHOULD fail, if we get here and it doesn't, 
        # we raise an error to be caught later...
        raise RuntimeError()
    except (TypeError, ValueError) as error:
        # If we encounter either of these error-types, that's what 
        # we'd expect: The type is wrong, or the value is invalid...
        tests_passed += 1
    except Exception as error:
        # Any OTHER error-type is a problem, so report it
        print(
            '%s raised calling my_function(%s, %s, %s)' % 
            (error.__class__.__name__, name, good_price, good_description)
        )

即使只进行了名称参数测试,我们也已经开始看到问题:

在加入价格和描述值的类似测试后:

for price in bad_prices:
    try:
        test_count += 1
        ignore_me = my_function(good_name, price, good_description)
        # Since these SHOULD fail, if we get here and it doesn't, 
        # we raise an error to be caught later...
        raise RuntimeError()
    except (TypeError, ValueError) as error:
        # If we encounter either of these error-types, that's what 
        # we'd expect: The type is wrong, or the value is invalid...
        tests_passed += 1
    except Exception as error:
        # Any OTHER error-type is a problem, so report it
        print(
            '%s raised calling my_function(%s, %s, %s)' % 
            (error.__class__.__name__, good_name, price, good_description)
        )

for description in bad_descriptions:
    try:
        test_count += 1
        ignore_me = my_function(good_name, good_price, description)
        # Since these SHOULD fail, if we get here and it doesn't, 
        # we raise an error to be caught later...
        raise RuntimeError()
    except (TypeError, ValueError) as error:
        # If we encounter either of these error-types, that's what 
        # we'd expect: The type is wrong, or the value is invalid...
        tests_passed += 1
    except Exception as error:
        # Any OTHER error-type is a problem, so report it
        print(
            '%s raised calling my_function(%s, %s, %s)' % 
            (error.__class__.__name__, good_name, good_price, description)
        )

由此产生的问题列表更大,总共有 15 项,如果不解决这些问题,其中任何一项都可能导致生产代码错误:

仅仅说单元测试是开发过程中的一项要求是不够的;必须考虑这些测试的实际功能、相关测试策略的外观以及它们需要考虑的内容。良好的基本起点测试策略可能至少包括以下内容:

  • 测试特定类型的参数或属性时使用哪些值:
    • 数值可能包括偶数和奇数变化、正值和负值以及最小值为零
    • 字符串值应包括预期值、空字符串值和仅为空格(“”)的字符串
  • 对于测试的每个元素,了解这些值何时有效,何时无效
  • 必须为合格和不合格案例编写测试
  • 必须编写测试,以便它们执行被测试元素中的每个分支

最后一项有一些解释。到目前为止,被测试的代码没有以完全相同的方式执行任何决策,无论参数的值是什么。针对确实根据参数值做出决策的代码执行的完整单元测试必须确保通过调用代码可以做出的所有决策的参数的测试值。通过简单地确保好的测试值和坏的测试值有足够的变化,这种需求很难得到充分的解释,但是当复杂的类实例出现时,要确保这一点会变得更加困难,这些情况需要更密切、更深入的关注。

前面在关于类模板的讨论中提到,将使用形式属性(托管属性),其背后的原因与单元测试策略有关。我们已经看到,生成能够在函数或方法执行期间检查特定错误类型的测试相对容易。由于属性是方法的集合,get、set 和 delete 操作各一个,由property关键字打包,因此对传递给 setter 方法的值执行检查并在传递的值或类型无效时引发错误(因此可能会在其他地方引发错误)将使单元测试实现遵循前面所示的结构/模式,至少在一定程度上更快、更容易。使用class-concrete.py模板中的property_name属性的基本结构表明,实现这样一个属性非常简单:

###################################
# Property-getter methods         #
###################################

def _get_property_name(self) -> str:
    return self._property_name

###################################
# Property-setter methods         #
###################################

def _set_property_name(self, value:(str, None)) -> None:
    if value is not None and type(value) is not str:
        raise TypeError(
            '%s.property_name expects a string or None '
            'value, but was passed "%s" (%s)' % (
                self.__class__.__name__, value, 
                type(value).__name__
            )
        )
    self._property_name = value

###################################
# Property-deleter methods        #
###################################

def _del_property_name(self) -> None:
    self._property_name = None

###################################
# Instance property definitions   #
###################################

property_name = property(
    _get_property_name, _set_property_name, _del_property_name, 
    'Gets, sets or deletes the property_name (str|None) of the instance'
)

涉及到 18 行代码,这比如果property_name是一个简单的非托管属性所需的代码至少多 17 行,并且如果在创建实例期间设置了property_name,则在使用此属性的类的__init__方法中可能会有至少两行代码。不过,折衷的办法是,托管属性属性将是自我调节的,因此不必在任何其他可能使用它的地方检查它的类型或值。它是可访问的,它所属的实例在访问属性之前没有抛出错误,这意味着它处于已知(有效)状态。

可重复的构建过程

构建过程的思想可能起源于需要编译才能执行代码的语言,但即使对于 Python 等不需要编译的语言,建立这样的过程也有好处。具体地说,在 Python 的例子中,这样的过程可以从多个项目代码库中收集代码,定义需求,而无需将它们实际附加到最终的包中,并以一致的方式打包代码,为安装做好准备。由于构建过程本身是另一个程序(或至少是类似脚本的过程),它还允许执行其他代码到所需的任何目的地,这意味着构建过程也可以执行自动测试,甚至可能将代码部署到本地或远程的指定目的地。

Python 的默认安装包括两个打包工具,distutils,它是一个基本功能的集合,以及setuptools,它构建在这两个工具之上,以提供更强大的打包解决方案。如果提供了打包参数,setuptools运行的输出是一个准备安装的包(一个 egg)。创建包的常规做法是通过setup.py文件调用setuptools提供的设置函数,该函数可能如下所示:

#!/usr/bin/env python
"""
example_setup.py

A bare-bones setup.py example, showing all the arguments that are 
likely to be needed for most build-/packaging-processes
"""

from setuptools import setup

# The actual setup function call:
setup(
    name='',
    version='',
    author='',
    description='',
    long_description='',
    author_email='',
    url='',
    install_requires=[
        'package~=version',
        # ...
    ],
    package_dir={
        'package_name':'project_root_directory',
        # ...
    },
    # Can also be automatically generated using 
    #     setuptools.find_packages...
    packages=[
        'package_name',
        # ...
    ],
    package_data={
        'package_name':[
            'file_name.ext',
            # ...
        ]
    },
    entry_points={
        'console_scripts':[
            'script_name = package.module:function',
            # ...
        ],
    },
)

显示的所有参数都与最终包的特定方面有关:

  • name:定义最终包文件的基本名称(例如MyPackageName

  • version:定义包的版本,该字符串也是最终包文件名的一部分

  • author:包的主要作者姓名

  • description:包装的简要说明

  • long_description:包装的详细说明;这通常是通过打开和读取包含长描述数据的文件来实现的,如果要将包上载到 Python 网站的包存储库,则通常采用降价格式

  • author_email:包的主要作者的电子邮件地址

  • url:包的主 URL

  • install_requires:为使用软件包中的代码而需要安装的软件包名称和版本要求列表–一组依赖项

  • package_dir:将包名映射到源目录的字典;显示的'package_name':'project_root_directory'值通常用于将源代码组织在srclib目录下的项目,通常与setup.py文件在文件系统中处于同一级别

  • packages:将添加到最终输出包的包列表;setuptools模块还提供了一个功能find_packages,该功能将搜索并返回该列表,并规定使用模式列表明确排除包目录和文件,以定义应忽略的内容

  • package_data:需要包含在映射到的包目录中的非 Python 文件的集合;也就是说,在所示的示例中,setup.py运行将查找package_name包(从包列表中),并将file_name.ext文件包含在该包中,因为它已被列出以供包含

  • entry_points:允许安装程序为代码库中的特定功能创建命令行可执行别名;它实际上要做的是创建一个小型的标准 Python 脚本,它知道如何从包中查找和加载指定的函数,然后执行它

在为hms_sys创建的第一个包中,将对实际setup.py的创建、执行和结果进行更详细的观察。还有一些选项用于指定、要求和执行自动化单元测试,我们将对此进行探讨。如果他们提供了所需的测试执行和故障停止功能,那么setuptools.setup可能足以满足hms_sys的所有需求。

如果发现标准 Python 安装过程由于任何原因无法管理的其他需求,那么将需要一个回退构建过程,尽管它几乎肯定会将setup.py运行的结果作为其过程的一部分。为了使回退尽可能(相对)简单,并确保解决方案在尽可能多的不同平台上可用,回退将使用 GNU Make。

Make 通过为Makefile中指定的每个目标执行命令行脚本进行操作。一个简单的Makefile,带有测试和执行setup.py文件的目标,非常简单:

# An example Makefile

main: test setup
        # Doesn't (yet) do anything other than running the test and 
        # setup targets

setup:
        # Calls the main setup.py to build a source-distribution
        # python setup.py sdist

test:
        # Executes the unit-tests for the package, allowing the build-
        # process to die and stop the build if a test fails

从命令行运行 Make 进程与执行make一样简单,可能有一个目标规范:

第一次运行(make未指定任何目标)执行Makefilemain中的第一个目标。而main目标则将testsetup目标指定为先决目标,以在继续其自身流程之前执行。如果执行了make main,将返回相同的结果。第二次和第三次运行make testmake setup分别执行这些特定目标。

因此,Make 是一个非常灵活和强大的工具。只要给定的构建过程步骤可以在命令行中执行,就可以将其合并到基于 Make 的构建中。如果不同的环境需要不同的过程(devteststagelive),则可以设置对应于这些环境的 Make 目标,允许一个构建过程处理这些变化,而不需要比执行make devmake live更复杂,虽然在本例中,需要注意目标命名,以避免两个不同但逻辑上合理的test目标之间的名称冲突。

集成单元测试和构建过程

如前所述,构建过程应该允许合并和执行为项目创建的所有可用自动化测试(至少是单元测试)。该集成的目标是防止测试失败的代码无法构建,从而无法部署,从而确保只有明显良好的代码可用于安装,至少在实时或生产代码级别。

但是,可能有必要允许在本地或共享开发构建级别上构建中断的代码,即测试失败的代码,即使只是因为开发人员可能希望或需要安装中断的构建以解决问题。这将是非常间接的,取决于为处理类似情况而制定的任何政策和程序。基于五种环境的可能策略集可以归结为以下内容:

  • **本地开发:**完全不需要测试

  • **共享开发:**需要测试,但失败的测试不会终止构建过程,因此可以将损坏的构建提升到公共开发服务器上;但是,坏的构建会被记录下来,并且这些日志很容易获得,以防需要快速升级代码

  • **QA/测试:**作为共享开发环境

  • 暂存(和**用户验收测试****环境:**测试必须执行并通过才能安装或升级代码

  • **现场/生产:**作为舞台

如果基于标准setuptools的打包过程允许运行测试,导致失败的测试中止打包工作,并且在安装过程中不需要执行测试,那么通过使用包装器(如 Make),这就为此类策略集提供了充分的功能覆盖为了提供特定于环境的目标和构建过程,可能需要处理策略一致性/覆盖率。

如果单元测试和构建过程标准到位并得到遵守,那么最终的结果将是代码易于构建和部署,无论它可能处于何种状态,并且在所有已知情况下都以已知(且可证明)的方式运行。但这并不意味着它将没有 bug;它不太可能有任何明显的 bug,只要测试套件是彻底和完整的,但这不是保证。

在建立相关的过程中会有一些开销,尤其是在单元测试方面,在维护过程中会有更多的开销,但是对系统稳定性的影响和影响是惊人的。

The author once wrote an asset catalog system for an advertising firm that was in daily use by as many as 300 people every business day following these process guidelines. Over the course of four years, runtime, including an update to a new and significantly changed version of the system, the total number of errors reported that weren't user error, data entry errors, or enterprise-level access permissions was four. These process standards make a difference.

定义 Python 代码的包结构

Python 中的包结构规则很重要,因为它们将确定在尝试从该包导入成员时可以访问哪些代码。包结构也是整个项目结构的一个子集,它可以对自动化构建过程产生重大影响,也可能对单元测试的设置和执行产生影响。让我们首先检查一个可能的顶级项目结构,如图所示,然后检查 Python 包的需求是什么,并查看它如何适合整个项目:

此项目结构假定最终版本将安装在 POSIX 系统上—大多数 Linux 安装、macOS、UNIX 等。比如说,Windows 安装可能会有不同的需求,这将在hms_sys开发周期中探索,我们将开始为它开发远程桌面应用程序。即使如此,该结构仍可能保持:

  • bin目录旨在收集最终用户可以执行的代码和程序,无论是从命令行还是通过操作系统的 GUI 执行。这些项目可能使用主程序包的代码,也可能不使用,但如果它们是 Python 可执行文件,则很有可能使用主程序包的代码。

  • etc目录是存储配置文件的地方,该目录下的example_project目录将用于特定于项目最终安装实例的配置。在顶层目录中删除特定于项目的配置可能是可行的,甚至是更好的方法,以此类推,该决策将需要逐个项目进行评估,这可能取决于安装项目的最终用户是否具有安装到全局目录的权限。

  • scratch-space目录只是一个收集开发过程中可能有用的任意文件的地方——概念验证代码、注释文件等等。它不打算成为构建的一部分,也不可部署。

  • src目录是项目代码所在的目录。我们将很快深入研究。

  • var目录是 POSIX 系统存储需要作为文件持久化的程序数据的地方。其中的cache目录是用于缓存文件的标准 POSIX 位置,因此其中的example_project目录将是专门用于缓存文件的项目代码的位置。在var中有一个cache中没有的专门的、项目特定的目录可能会很有用,而且也提供了该目录。

项目上下文中的包

src目录中是项目的包树。example_project目录下的每个目录级别(包含__init__.py文件)都是一个正式的 Python 包,可以通过 Python 代码中的 import 语句进行访问。一旦构建并安装了该项目,并且假设其中的代码是为了适应相关的导入结构而编写的,那么以下所有内容都是从该项目代码中合法导入的:

| import example_project | 导入整个example_project命名空间 | | import example_project.package | 进口example_project.package及其所有成员 | | from example_project import package | | from example_project.package import member | 假设member存在,则从example_project.package导入 | | import example_project.package.subpackage | 进口example_project.package.subpackage及其所有成员 | | from example_project.package import subpackage | | from example_project.package.subpackage import member | 假设member存在,则从example_project.package.subpackage导入 |

Python 中包的典型模式是围绕公共功能领域对代码元素进行分组。例如,在非常高的级别上,一个专注于 DOM 操作(HTML 页面结构)并支持 XML、XHTML 和 HTML5 的包可能会这样分组:

  • dom (__init__.py)
    • generic (__init__.py)
      • [使用元件的通用类]
    • html (__init__.py)
      • generic (generic.py)
        • [用于处理 HTML 元素的通用类]
        • forms (forms.py)
    • html5 (__init__.py)
      • [用于处理 HTML-5 特定元素的类]
      • forms (forms.py)
    • xhtml (__init__.py)
      • [用于处理 XHTML 特定元素的类]
      • forms (forms.py)
    • xml (__init__.py)

因此,该结构的完整实现可能允许开发人员通过创建位于dom.html5.forms.EmailField命名空间的类的实例来访问 HTML5 电子邮件字段对象,该类的代码作为名为EmailField的类存在于.../dom/html5/forms.py中。

Deciding where specific classes, functions, constants, and so on should exist in the structure of a code base is a complex topic, and will be explored in greater depth as part of the early architecture and design of hms_sys.

使用 Python 虚拟环境

Python 允许开发人员创建虚拟环境,将所有基线语言设施和功能收集到一个位置。设置后,这些虚拟环境中安装或删除了包,这使得在环境上下文中执行的项目能够访问基本系统中可能不需要的包和功能。虚拟环境还提供了一种跟踪这些安装的机制,从而允许开发人员只跟踪那些与项目本身相关的依赖项和需求。

还可以使用虚拟环境,经过一定的谨慎和思考,允许根据特定版本的 Python 语言开发项目——例如,该语言不再受支持,或者仍然太新,无法在开发机器的操作系统中作为标准安装使用。最后一个方面在开发 Python 应用程序以在各种公共云(如 Amazon 的 AWS)中运行时非常有用,在 AWS 中,Python 版本可能比一般可用的版本更新,并且可能与早期版本的语言有显著的语法差异。

Breaking changes at the language level aren't very common, but they have happened in the past. Virtual environments won't solve those, but they will, at least, allow different versions of code to be maintained with more ease.

如果已经安装了相应的 Python 模块(Python 3 中的venv),那么在命令行级别创建虚拟环境、激活和停用它是非常简单的:

python3 -m venv ~/py_envs/example_ve

在指定位置创建新的最小虚拟环境(在本例中,在用户主目录中名为example_ve的目录中,在名为py_envs的目录中):

source ~/py_envs/example_ve/bin/activate

这将激活新创建的虚拟环境。此时,启动python表示使用的是 3.5.2 版本,命令行界面在每行前面加上(example_ve)表示虚拟环境处于活动状态:

deactivate

这将停用活动虚拟环境。从命令行启动python现在显示系统的默认 Python 版本 2.7.12。

安装、更新和删除软件包以及显示安装的软件包同样简单:

这将再次激活虚拟环境:

source ~/py_envs/example_ve/bin/activate

这将显示当前安装的软件包的列表。它不显示作为核心 Python 发行版一部分的任何包,只显示已经添加的包

pip freeze

在本例中,第一次运行还注意到环境中的当前版本pip是旧的,可以更新,这是通过以下命令完成的:

pip installupgrade pip

pip包本身是基本 Python 安装的一部分,尽管它刚刚被更新,但这对再次调用pip freeze返回的包列表没有影响。

为了说明pip如何处理新软件包的安装,安装了用于处理图形文件的 Python APIpillow库,其中包括:

pip install pillow

由于pillow不是一个标准库,它确实出现在另一个pip freeze调用的结果中。pip freeze的结果可以作为项目结构的一部分转储到需求文件(requirements.txt,用于说明),并与项目一起存储,这样包依赖关系实际上不必存在于项目的源树中,也不必与之一起存储在 SCM 中。这将允许项目中的新开发人员简单地创建自己的虚拟环境,然后通过另一个pip调用安装依赖项:

pip install -r requirements.txt

然后卸载pillow库以显示其外观,如下所示:

pip uninstall pillow

The pip program does a good job of keeping track of dependencies, but it may not be foolproof. Even if uninstalling a package removes something that it lists as a dependency, but that's still in use, it's easy enough to re-install it with another pip call.

因此,虚拟环境允许对与项目关联的第三方软件包进行大量控制。但是,它们的代价很小:它们必须得到维护(如果很少的话),而且由于这些外部包的更改是由一个开发人员进行的,因此需要使用一些规则来确保这些更改可供在相同代码基础上工作的其他开发人员使用。

总结

有相当多的因素可以影响代码的编写和管理方式,甚至在编写第一行代码之前。它们中的每一个都会对开发工作的进展有一定的影响,或者对开发工作的成功程度有一定的影响。幸运的是,有很多选择,在做出决定时也有相当大的灵活性,这些决定决定决定了哪一个在发挥作用,以及如何发挥作用,即使假设一些团队或管理层的政策没有规定它们。

关于hms_sys项目中这些项目的几个决定已经被注意到,但由于下一章将真正开始该开发,它们可能值得再次提及:

  • 代码将使用 Geany 或 LiClipse 作为 IDE 编写。它们都提供了代码项目管理工具,可以处理预期的多项目结构,并将提供足够的提示,使跨项目导航相对轻松。最初,这项工作将使用 Geany,如果 Geany 变得太麻烦而无法合作,或者在开发完成后无法处理项目的某些方面,则 LiClipse 将保留。
  • 源代码管理将使用 Git 处理,指向外部存储库服务,如 GitHub 或 Bitbucket。
  • 准则将遵循 PEP-8 建议,直到或除非有令人信服的理由不这样做,或者它们与所述的任何内部标准相冲突。
  • 代码将按照所示的各种模板文件中列出的结构编写。
  • 可调用函数(函数和类方法)将使用类型暗示注释,除非有令人信服的理由不这样做。
  • 所有代码都将进行单元测试,尽管测试策略细节尚未定义,只是要确保所有公共成员都经过测试。
  • 系统中的每个代码项目都将有自己的构建过程,使用标准的setup.py机制,如果需要,将基于Makefile的过程包裹在它们周围。
  • 每个构建过程都将集成单元测试结果,以便在任何单元测试失败时防止构建完成。
  • 项目中的包结构尚未定义,但将随着开发的进行而展开。
  • 每个项目都将拥有并使用自己独特的虚拟环境,以便将与每个项目相关联的需求和依赖项分开。这可能需要对构建过程进行一些调整,但这还有待观察。********