Python 最佳实践高级教程(四)
十五、分解编程任务
程序员,就像诗人一样,只是稍微偏离了纯粹的思想。——弗雷德·布鲁克斯——神话中的人月
编程的一个关键部分是将一个问题分成更小的部分。这种问题分解是每个程序员的必备技能。不管你是想从头写一个程序,还是想给一个已存在的问题增加功能,你都需要先分解编程问题。问题分解,也称为需求分析,是一种被没有经验和有经验的程序员低估的技能。前者低估了它,因为他们正忙于掌握编程语言本身,后者因为他们认为这是微不足道的,他们已经弄清楚了(尽管他们往往是正确的)。因此,许多编程项目因为缺乏功能或者功能分解不良而遇到问题就不足为奇了。结果,产品是脆弱的,并且深受软件熵之苦,它们花费的时间比预期的要长,或者它们完全失败。个人和大型开发团队、使用传统软件工程方法的团队以及动态、敏捷的团队都报告过这样的问题。本章致力于分解编程任务,帮助你编写不容易犯这个错误的软件。
分解编程任务是困难的
但是为什么分解编程问题很难呢?让我们考虑一个典型的编程任务:我们想给我们的游戏添加一个幽灵。幽灵将随机穿过迷宫,试图吃掉玩家。怎么才能分解这个特征呢?直观地说,把一个问题分成更小的块,可以分层次地考虑(见图 15-1 )。我们首先把它分成两个更小的子问题。然后我们将每个子问题分解成更小的问题。最后,我们实现所有较小的部分,我们就完成了。
图 15-1。
Hierarchical decomposition of a programming problem. First, the problem is divided into two separate subproblems, which are divided further. Unfortunately, this approach rarely works in practice.
这是一个不错的模型。不幸的是,除了最简单的程序之外,它并不适用于所有程序。原因是大多数程序中较小的组件是高度相互依赖的。大多数编程问题都是多维的。我们需要记住多个问题维度:
- 编程任务本身——我们试图编程的到底是什么?
- 数据——我们需要在程序中存储什么信息,它是如何构成的?
- 控制流——程序执行任务的顺序是什么?
- 技术——软件由哪些组件(python 式和非 python 式)组成?
- 成本—哪些功能价格合理,哪些不合理?
所有这些维度(以及这里没有提到的其他维度)都相互依赖。多年来,已经写了很多关于分解的书。他们涉足非常多样的领域,如需求分析、通用建模语言(UML)和 Scrum。所有这些都是非常强大的技术。在一个(主要是公司)环境中,这些方法之一被应用到大型项目中,它们当然值得学习。但是对于普通的 Python 程序员来说,它们并没有太大的帮助。
Python 中的分解问题有什么特别之处吗?有:Python 的一个关键优势是开发速度快。不花任何时间在计划上并马上开始编程是非常诱人的。一个好的分解方法也需要很快,这样我们才不会危及 Python 带给我们的优势。这就是为什么本章描述了一个在几分钟或几小时内适用的轻量级分解过程。我们将详细分解重影特征。最后,我们将在本章开始实现它。
分解编程任务的过程
分解编程任务是困难的,因为我们必须在多个维度(功能、数据、控制流、技术、成本)做出设计决策。为了避免过早的设计决策,我们需要或多或少地并行处理这些维度。这是什么意思?我们对程序特性的最初看法就像一朵云(见图 15-2 )。我们对程序应该做什么有一个粗略的想法,但是功能太不精确而无法实现。在设计过程中,组件开始从云中出现并成形。我们不是在开始下一个维度之前完全固定一个维度,而是将每个维度与其他维度并行平衡,从而解决大的设计问题。最后,组件获得了足够的清晰度,实现是可行的。如果从一个特性到它的实现的路径是清晰的,我们可以说这个特性是完全分解的。
图 15-2。
A program feature is like a blurry cloud in the beginning. Only by decomposing it will details of the functionality and program components become apparent. When we finally arrive at sharply defined, implementable components, the decomposition is complete.
在下文中,我们将使用七个步骤将重影特征分解成更小的部分:
- 写一个用户故事,对编程任务的简单描述
- 向描述中添加细节
- 检查非功能需求
- 识别可能的问题
- 决定解决主要问题的架构
- 识别程序组件
- 实施
这个过程反映了许多程序员在实现一个简单的功能时的直觉。系统地完成这七个步骤,我们也可以将同样的过程应用到更困难的任务中。在这个过程中,我们有意识地做出设计决策,在云中寻找可以用 Python 实现的组件。这是我们接下来要做的。
我们可以用这个过程来扩展现有的程序吗?没错。很少有软件是从零开始开发的。分解的过程在从零开始或扩展现有程序之间没有根本的区别。当然,当添加到现有软件中时,我们需要考虑我们已经拥有的(在第 3 步及以后),因为我们不应该在每次添加功能时颠倒我们的设计。
写一个用户故事
第一步是写下我们想要编程的内容。此描述必须足够短,以便能写在一张小纸上(A6 纸卡)。描述还必须足够简单,非程序员也能明白其中的价值。这样的描述也被称为用户故事。对于鬼魂,相应的用户故事可能是(也见图 15-3 ):作为一个玩家,我想要一个鬼魂,这样我就可以通过迷宫逃离它。这个用户故事的句子结构是标准的最佳实践。为了更清晰,许多开发人员已经同意使用该结构“作为(一种类型的用户),我想要(一些目标),以便(一些原因)。”这种用户故事的结构不仅记录了要实现什么样的程序特性,还记录了为什么要引入这个特性。又见 https://www.mountaingoatsoftware.com/agile/user-stories .我们用户故事的价值当然是有鬼的游戏比没鬼的有趣。尽管写下这个特性是一件简单的事情,但它在许多方面改进了程序员的工作:
- 任务变得切实可行。无论使用什么项目管理方法,用户故事都很容易管理。我们可以对它们进行优先排序,估计或跟踪完成的时间,当然,也可以检查它们是否完成。在编写用户故事时,一个常见的最佳实践是将它们放在一个显眼的地方。令人惊讶的是,纸质卡片或纸板往往比电子系统更好。
- 描述有助于我们集中注意力。在描述中包含程序用户将会看到或做的事情有助于我们开发一个比我们写“用 Python 实现一个 ghost 模块”更有用的程序
- 这个描述忽略了技术细节。此时,决定如何实现该功能还为时过早。
- 最重要的是,用户故事表明我们对功能的理解可能是不完整的。我们将需要进一步思考或沟通,以澄清我们想要实施的细节。因此,用户故事也被称为“交流的承诺”
图 15-3。
User Story for adding a ghost to the game. A User Story is a short description of a program feature with an emphasis on the value the feature has for users.
每当你开始开发一个新的程序时,写下所有计划功能的用户故事是组织起来的第一步。
向描述中添加细节
从前面的简单描述中,我们已经可以看出,ghost 将大大改变我们的程序。以前,我们只需要对用户按下的键做出反应。现在,幽灵和玩家将同时移动:存在并发性。对于一个程序员来说,这是一个非常危险的时刻。当遇到“并发”这样的概念时,有趣的事情就发生了。经验不足但聪明的程序员举手说“等等!我不知道该怎么做。”更有经验、热情的程序员倾向于立即提出他们喜欢的解决方案(例如,“Python 线程是理想的解决方案”或“不惜一切代价避免使用 Python 线程”。GIL 会把你逼疯的!”).有经验的程序员之间的讨论可能会让其他人感到有点害怕。此外,现在决定技术解决方案可能还为时过早。无论你看到许多可能的解决方案还是根本没有,收集更多的信息都是值得的。
验收准则
处理新情况的一个建设性方法是首先收集问题的细节。很多时候,简单地记下要点列表是我们能做的最好的事情。ghost 功能的要点如下所示:
- 玩家和幽灵必须平行移动。
- 幽灵以规则的间隔移动(每平方不到一秒)。
- 鬼魂朝一个方向直线移动,直到碰到一堵墙。然后它随机改变方向。
- 鬼魂不吃小点。
- 当幽灵和玩家在同一方格时,游戏终止。
根据项目方法的不同,这种特性的细节有时被称为验收标准或要求。在一个敏捷项目中,验收标准适用于另一张 A6 卡。在大型的传统软件项目中,需求有时会增长到数百或数千页的文档。后者绝对不是我们希望在一般的 Python 项目中发生的事情。对我们来说,将需求保存在一个小的文本文件中或者用户故事卡的背面就足够了。如果一个简单的要点列表还不够,我们可以添加文字描述和图表来更详细地描述这个特性。如果这样的信息已经存在(带有说明、文章、例子、会议记录的电子邮件),最好将这些信息放在靠近其他描述的地方。
用例描述
除非我们正在编写一个巨大的软件,否则我不会从编写一个关于我们将要实现的特性的冗长的文本描述开始:它们可能无论如何都会改变。比写文本更快的实用计划技术是写下一系列事件或用例描述。用例描述是以一定顺序发生的事件的列表。例如,我们可以像图 15-4 那样描述幽灵的运动。用例描述已经比用户故事更类似于 Python 代码。但是仍然,里面没有技术术语;我们仍然不需要决定幽灵的移动是由单个函数处理(也许),由十个不同的类处理(也许),还是用 COBOL 语言实现会更好(非常不可能)。
图 15-4。
Use Case for moving the ghost. The movement feature has been broken down into a chronological order of steps that make it easier to reason about it. This Use Case description is not exhaustively accurate, nor does it suggest how to implement the feature. The description simply helps us to gradually shift to higher precision while postponing technical decisions.
图 15-4 中的用例包含一个玩家被吃掉时的分支(在 3a 中)。一个用例可以,但不是必须,包含分支。如果有许多不同的情况,通常挑选几个有代表性的就足够了。用例不一定要完整。它仍然需要适合幻灯片或一张纸。也不要担心是否有不止一个“正确”的序列。我们将在接下来的步骤中找到答案。用例描述是一种帮助分解困难用例的工具。大多数情况下,一系列要点将为实施添加足够的细节。
检查非功能需求
在前两步中,我们已经足够详细地描述了我们想要实现的功能。在这一步,我们将关注开发中更微妙的部分:非功能性需求。什么是非功能需求?简而言之,非功能需求是对项目的所有约束,与我们正在实现的功能没有直接关系。例如,这些包括技术、开发方法、平台、性能和其他技术参数,以及道德和法律问题。安全和保障也是非功能需求的一部分,但是正如第一章所述,我们不会在本书中讨论它们。MazeRun 游戏的一些非功能性需求如下:
- 游戏因为书名(技术)需要用 Python 写。
- 游戏需要有自动化测试(开发方法)。
- 游戏需要可以安装在 Linux、Windows 和 Mac 上,以便许多开发者可以对它进行实验(平台)。
- 源代码的长度需要少于 1000 行,以适合一本书(技术参数)。
- 有 0 到 10 个幽灵以 0.1-1.0 秒的间隔移动(技术参数)。
- 游戏需要适合六岁儿童(伦理)。
- 该游戏需要在开源许可(合法)下在 GitHub 上发布。
为什么非功能需求很重要?想象一下,我们增加了一个非功能性的要求,除了前面的要求,MazeRun 应该可以在 Android 和 iOS 手机上运行,并且能够处理 10,000 个同时追逐玩家的幽灵(以确保即使是最活跃的手机用户也不会感到无聊)。结果将是一个完全不同的程序,我们将不得不使用不同于 Python 和 SDL 的技术来运行它。对非功能性需求的错误理解有可能使程序完全无用,因此跳过这一步是不可选择的。
在更深的层次上,非功能需求帮助我们确定我们正在编写什么样的程序。MazeRun 的第一个工作版本(没有第七章中的幽灵)可以开发成许多不同的游戏:一个具有高效人工智能的类似象棋的策略游戏(不在本书中),一个有许多花哨图形的快节奏射击游戏(也不在本书中),或者一个为了推理 Python 最佳实践而构建的类似小精灵的游戏(准确地说!).用几个技术参数或其他非功能性需求来表达明确的方向,会使以后的技术决策变得容易得多。
就像功能性描述一样,写下非功能性需求是个好主意。幸运的是,非功能性需求在项目中通常不会改变(但是,如果它们改变了,您需要非常小心)。
识别问题
有了功能性和非功能性的描述,我们就有希望清楚地了解 ghost 特性是什么,以及它需要适应什么样的边界条件。这是寻找潜在问题的好时机。还有什么看起来很难的呢?有没有与非功能需求相冲突的用户故事?这些描述中有互相矛盾的吗?有什么事情是听起来不可能的吗?在这一节中,我们休息一下,思考一下我们需要处理的问题。
Tip
休息一下是字面意思。在花了一些时间(10 分钟到几个小时之间)编写用户故事、用例描述和非功能需求之后,现在是后退一步,从一个新的角度来看待任务的好时机。新鲜空气确实有帮助!
对于 ghost 特性,有一个主要的挑战:并发性。玩家和幽灵必须同时移动。目前,我们还没有一个如何实施的计划。这里我们将详细分析并发问题,看看如何从不同的角度处理问题。一般来说,我们可以预料到至少来自四个不同方向的麻烦:不完整的信息、领域专业知识、更改现有代码和预测未来的变化。
不完全信息
当你试图自己应用这个过程时,你很可能会意识到你没有足够的信息来继续。对你的特征的描述包含了一些假设,或者仅仅是缺乏你的主管/客户真正需要的细节。这是完全正常的。面对不完全信息时,你有两种选择。要么创建一个(快速的)原型实现,看看它是否符合预期,要么尝试先获取更多信息。当询问更多信息时,你的主管/客户可能也不知道,因为他们在你分解之前从来没有想过这个问题。或者他们想出了一些一开始听起来不错的好主意,但一旦付诸实施,却发现毫无用处。因此,应对信息缺乏的一个好策略是循序渐进。
对于 ghost 特性,我们没有关于如何处理并发性的信息。另一方面,我们对这个特性有很多自由,因为它的主要目的是提供学习体验。任何工作鬼都可以,所以信息不足在这里不是问题。
领域专业知识
相反的问题是拥有过多的信息,知道我们正在解决的问题充满了例外和特殊情况。一个常见的症状是,我们反复推理某个特征的非常特殊的方面,比如“是的,但在 Anhk-Morpork 的税法中,如果雇员是一个幽灵,税率是 13%而不是 21%,因为它是一个非物质实体。”如果你不熟悉问题领域(在这种情况下是 Ankh-Morporkian 税法),这种特定领域的细节很快就会让人不知所措。这类问题的解决方法是先简化。找到一个简洁地代表问题域的模型,但是不要过于简化。在问题域的一个良好构建的模型中,在实现过程中可以包含特殊情况。关于问题领域的背景知识是关键。取决于你是否是领域专家,这种问题可以在一杯咖啡或去图书馆做大量背景研究时解决。幸运的是,我们每个人以前都玩过或至少看过电脑游戏。我们对游戏有足够的了解,可以理解前面提到的 MazeRun 中的并发问题。我们的理解有助于我们列举一些需要注意的情况:
- 如果玩家在幽灵上移动会发生什么?这一点在前面的用例描述中没有明确涵盖!
- 玩家对幽灵移动还是幽灵对玩家移动有区别吗?
- 玩家和鬼魂理论上可以同时移动从而互换位置吗?
- 移动的幽灵会让玩家移动的更慢吗?
收集这些问题将有助于我们评估我们的解决方案。
更改现有代码
向程序中添加新的特性可能很有挑战性,因为它们需要适应已经存在的代码。通常,需要重新组织现有代码,以便为新功能腾出空间。问题是如何做到这一点,同时又不造成一片混乱。简而言之,你需要记住在第十四章中介绍的软件熵的概念。考虑到并发性问题,当考虑 MazeRun 的现有代码时,我们需要更改哪些部分?当然,事件循环必须改变。到目前为止,第四章的实现什么都不做,除非玩家按下一个键。我们将不得不仔细看看这些代码。
预见未来的变化
软件熵的另一个方面是我们知道我们的程序将来会改变。我们现在做出的设计决策将会产生长期的影响。因此,我们不仅需要考虑程序的当前需求,还要预测程序在未来可能如何发展。为了创建一个稳定的设计,我们需要知道程序的哪些部分最不可能改变,哪些部分肯定会改变。这些部分需要分开。在所有可能的问题中,这是最困难的一个。要解决这个问题,领域专业知识、经验和运气的结合是必要的。回想过去几十年写作和玩电脑游戏的经历,有些事情很可能会改变
- 额外的游戏元素(幽灵的种类,特殊的地砖)
- 参数(重影速度、屏幕尺寸)
- 图形和动画
每个玩游戏的人都立即意识到的一个方面是移动玩家时缺少动画。玩家人物只是从一个方块跳到下一个方块。让动作流畅确实是一个不错的改进。乍一看,这似乎是一个无害的功能,我们可以用几行代码实现为一个定格动画:
import time
for offset in range(32):
draw_map_without_player()
draw_player(100 + offset, 100) # starting point (x=100, y=100)
pygame.display.update()
time.sleep(0.05) # seconds
但是等等!如果我们加上幽灵,不知何故,幽灵和玩家的流畅动作需要协调。又是并发问题。不用进一步考虑这个问题,我们可以预计并发问题在未来会变得更加重要。总而言之,我们在本节中收集了三个问题,都与并发性有关:
- 目前,我们还没有解决如何处理平行移动的球员和幽灵。
- 为了允许同时运动,我们需要改变事件循环。
- 在未来,将会有更多的事情同时发生。
这样一个可能的问题列表可能会很长。我们需要做的是对它们进行优先排序,然后专注于最糟糕的问题。在我们的例子中,并发性是最大的挑战。在本章的剩余部分,我们将重点解决这个问题。
决定架构
我们认为并发是最困难的问题。为了给游戏加个鬼,任何实现都需要妥善解决这个问题。现在,如果这听起来已经足够简单,我们可以跳过接下来的两节,开始编写代码。但是假设我们是第一次做这种事情,最好先考虑一下我们程序的架构。老实说,给电脑游戏添加一个幽灵,建筑这个词有点浮夸。术语软件架构也用于描述由数百台连接的服务器组成的事物。但是我更喜欢架构,而不是更温和的术语软件设计,因为设计经常与事物的外观不正确地联系在一起。我们真正感兴趣的是找到一个能帮助我们解决问题的程序结构。让我们看几个潜在的架构。
在图 15-5 中,我们发现了软件中使用的六种常见架构模式。其中四个是有用的。管道描述了一系列相互依赖的步骤。我们在执行计算、数据处理的程序中和 Unix 命令行上的工具中找到了管道结构。层模型有助于组织以两种方式相互通信的组件。它是构建 web 服务器的经典模型。反馈回路是各种调节过程的良好结构,例如,可以在监控工具和感觉装置中找到。最后,中介模型组织不直接相互对话的组件之间的通信。例如,一个绘图程序被构造成一个中介;画布是所有不同绘图工具交互的中心媒介。
图 15-5。
Six frequent architectural pattern s in software. The four on the left are useful patterns; the two on the right are antipatterns that should be avoided.
该图还包含两个反模式,应该避免的结构。第一个是 Blob,它通过将所有内容放在一个组件中来避免分解。结果是一个巨大的混乱的非结构。第二个是意大利面条模型,它有许多组件,但是所有组件都可以自由地相互交流。结果又是一个混乱的非结构。
图中还没有包括其他基本的架构模式。没有太多,因为有有限数量的不同拓扑,既不是斑点也不是意大利面条。我们不能混合两种架构,因为结果将会是一个类似意大利面条的结构。但是,体系结构可以嵌套,也就是说,可以相互包含。例如,分层架构的第二层可以在内部包含一个管道。
我们如何使用这些信息来解决并发问题呢?我们需要让玩家和幽灵都可以并行移动,并负责中间的一些后台任务(例如,绘制迷宫)。让我们考虑图 15-6 中的两种可选结构。
- 一个简单的反馈回路。我们在一个循环中处理所有事情。首先我们移动玩家,接下来我们移动幽灵,最后我们绘制图形。
- 我们引入了一个中介,这个组件的唯一职责是决定该轮到谁做什么。播放器、ghost 和绘图部分通过相同的协议与中介进行通信。
图 15-6。
Two alternatives for taking care of concurrency : a feedback loop architecture (left) and a mediator-based architecture (right)
可能有更多的可能性来解决这个问题,但是我们将坚持这两个。在我们的案例中,这两种架构中哪一种是最好的?反馈循环可能是更容易实现的。我们可以很快地从头开始写,而且会成功。不过,循环结构有几个明显的缺点。首先,它不容易扩展:每次我们添加一个新的游戏元素,我们都必须将它添加到循环中。第二,循环中最慢的一步会减慢所有其他步骤。在游戏中,增加一些元素很容易导致明显的延迟。我们可以通过最终跳过事件循环中的一个或多个步骤来避免这种情况,但这样架构就不再那么简单了。
另一方面,中介结构易于扩展。只要组件使用相同的协议与中介通信,插入多少个元素并不重要。中介只需要有某种队列或其他规则集来决定轮到哪个组件。后一种属性正是我们所需要的。我们知道玩家和鬼魂的移动会遵循非常不同的节奏。中介架构能够适应这种差异。这就是为什么我们将继续使用中介架构来实现。
识别程序组件
我们已经决定使用一个中介架构来实现 ghost。但到目前为止,我们还没有决定介体和它们周围的组件到底是什么。我们仍然需要决定是否使用函数,类,模块,或者其他的东西作为图的一部分。为了先解决高阶问题,我们推迟了这个(显然很重要的)决定。在这一节,最后的实施之前,是时候做出这样的决定了。一般来说,识别组件意味着跨越开头提到的编程问题的维度(功能、数据、控制流、技术)来划分界限。我们现在需要看到一个清晰的结构从最初的云中浮现出来,并且易于实现。
在 Python 中,决定是否在给定的地方使用函数、类或模块本身并不是一个非常困难的决定。因为所有这些都是 Python 对象,所以通常可以很容易地将一个对象换成另一个对象。例如,我们可以从实现一个函数开始,但是如果程序增长了,就用一个类来代替它。在其他语言中,这样的决定有点困难。这一步的目的是使上一步的架构变得生动,而不是创建一个函数、类和模块的详尽列表。我们将需要在实施过程中自由添加或更改一些细节。有一个我们的程序将包含的组件的概要就足够了。
我们的调解人需要哪些组件?同样,我们面临一个设计决策:我们将使用什么作为中介,以及它将如何与其他组件通信?同样,我们将考虑两个选项。首先,我们可以使用多线程。我们并行运行三个子进程,一个用于玩家,一个用于幽灵,一个用于绘图。三者共享相同的数据。在这种情况下,中介是 Python 内置的线程引擎。多线程是游戏中常见的一种中介模型。对于更复杂的并发模型(例如,asyncio、twisted,或gevent),有大量关于在 Python 和库中实现线程的文档。多线程模型很容易扩展。我们可以简单地为更多的幽灵添加更多的线程。另一方面,众所周知,线程很难调试,不仅仅是在 Python 中。此外,Python 中线程的性能优化并不容易(因为所谓的全局解释器锁或 GIL)。
第二,我们可以使用基于 Pygame 事件的交流。作为中介,我们将有一个事件循环,它收集事件并根据事件类型将它们分配给不同的功能。事件模型的好处是 Pygame 负责将事件排队,并且有预定义的事件类型。我们基本上需要使事件循环更加通用,这样我们就可以插入自定义事件(玩家和幽灵移动和绘图)。像多线程一样,调试事件队列并不容易,我也不敢对它的性能做任何假设。决定是使用线程还是事件队列比之前关于架构的决定要小得多,因为我们现在是在架构的约束下决定的。如果没有太多支持这个或那个模型的话,程序员的经验和偏好决定。就我个人而言,我更喜欢事件循环模型,主要是因为我以前做过,对我来说,线程调试起来更痛苦。但是我坚信这个游戏可以通过多种不同的方式成功实现。你可以自由尝试自己喜欢的方法。
这个问题解决后,我们可以打开文本编辑器,写一个程序大纲。首先,我们将对所有事情使用函数。为了让事件循环作为中介工作,我们将需要以下组件(见图 15-7 以获得概述):
图 15-7。
A clear structure has emerged. We have decided to solve the problem of concurrency by using a mediator structure—the event loop. Other game components communicate with the mediator using pygame.event.Event objects, each of which is associated with a callback function. The decomposition is now ready for an implementation.
def move_player(event):
"""moves when keys are pressed"""
pass
def move_ghost(event):
"""Ghost moves randomly"""
pass
def update_graphics(event):
"""Re-draws the game window"""
pass
def event_loop():
"""a mediator using the Pygame event queue"""
pass
创建这样的框架结构是实现的一个很好的准备,不管你想实现小的还是大的组件。这里介绍的方法是相同的。实际上有无限多种可能的成分。为了避免迷失在细节中,表 15-1 列出了一些经常出现的错误。
表 15-1。
Some Frequently Occuring Components in Python programs
| 名字 | 目的 | Python 关键字或模块 | | --- | --- | --- | | 数据结构 | 将数据与代码分离 | 任何(正确) | | 班 | 模块化数据+代码 | `class` | | 命令行界面 | 解析命令行选项 | `argparse` | | 记录 | 将信息写入日志文件 | `logging` | | 配置 | 设置参数或从文件中读取参数 | `configparser` | | 文件输入输出 | 读取或写入数据文件 | 许多 | | 数据库接口 | 访问外部数据 | 模块,取决于数据库 | | c 扩展 | 加速计算 | 外部模块 | | HTML 模板 | 将代码与显示分开 | `jinja2 or django` |有了这些,我们已经做了足够的练习。是时候将我们的计划工作转化为工作代码了。
实施
让我们开始实施的幽灵。中心组件将是事件循环。事件循环的主要职责是充当并发事件的中介。我们从上一节中的框架函数开始实现。事件循环将通过pygame.event.Event对象与图 15-7 中的其他功能进行通信。Pygame 有内置的整数USEREVENT用于定义自定义事件类型,这很方便。我们首先为幽灵移动定义事件,更新屏幕,并退出游戏(对于玩家移动,我们将使用已经存在的键盘事件KEYDOWN):
import pygame
from pygame.locals import USEREVENT, KEYDOWN
EXIT = USEREVENT
UPDATE_DISPLAY = USEREVENT + 1
MOVE_GHOST = USEREVENT + 2
接下来,我们为游戏中的主要事件创建虚拟程序(在接下来的两章中,我们将把它们发展成功能齐全的组件):
def move_player(event):
"""moves when keys are pressed"""
print(’player moves’)
def move_ghost(event):
"""Ghost moves randomly"""
print(’ghost moves’)
def update_graphics(event):
"""Re-draws the game window"""
print(’graphics updated’)
我们希望以一种允许我们灵活地插入不同组件的方式来编写事件循环。为此,我们将事件类型与我们的函数相关联。在程序员的行话中,这样的函数通常被称为回调。Python 字典是存储事件类型和回调函数的理想结构:
callbacks = {
KEYDOWN: move_player,
MOVE_GHOST: move_ghost,
UPDATE_DISPLAY: update_graphics,
}
但是所有的事件从何而来呢?对于键盘事件,这一点非常清楚:Pygame 自动生成这些事件,除非我们省略初始化显示(就像我们在第四章中所做的)。其余的事件需要显式生成。例如,我们可以将一个EXIT事件发送到 Pygame 的事件队列中
# not part of the final program
exit = pygame.event.Event(EXIT)
pygame.event.post(exit)
另一种可能性是启动一个每N毫秒产生一个事件的定时器。例如,用于每 0.3 秒移动重影的计时器可以从
pygame.time.set_timer(MOVE_GHOST, 300)
现在我们可以编写事件循环本身了。与我们之前的实现一样,它从队列中收集事件。但是它没有解释它们,而是将它们重定向到一个回调函数。唯一的例外是EXIT事件,它终止了循环:
def event_loop(callbacks, delay=10):
"""Processes events and updates callbacks."""
running = True
while running:
pygame.event.pump()
event = pygame.event.poll()
action = callbacks.get(event.type)
if action:
action(event)
pygame.time.delay(delay)
if event.type == EXIT:
running = False
最后,我们可以开始事件循环。我们需要设置几个定时器来定时移动幽灵和更新屏幕。我们还设置了一个定时器,在五秒钟后触发一个EXIT事件:
if __name__ == ’__main__’:
pygame.init()
pygame.display.set_mode((10, 10))
pygame.time.set_timer(UPDATE_DISPLAY, 1000)
pygame.time.set_timer(MOVE_GHOST, 300)
pygame.time.set_timer(EXIT, 5000)
event_loop(callbacks)
如果我们启动程序,按几个键,输出如下:
player moves
player moves
ghost moves
ghost moves
player moves
player moves
ghost moves
graphics updated
ghost moves
..
到目前为止,我们取得了什么成绩?我们创建了一个通用结构,比幽灵问题通用得多。我们可以对许多并发的游戏元素使用相同的机制。注意,事件循环并不关心我们连接到它的是什么。我们可以插入一个或两个玩家,一个到多个幽灵,或者其他我们以后可能会发明的东西。当然,我们可以插入的组件数量有一个实际上限。但是,我们还远没有到必须考虑优化我们的架构的地步。在接下来的章节中,我们将讨论如何实现 ghost 的其余部分。
七步计划过程是一个如何分解问题的例子。尽管拥有这样一个过程是一种最佳实践,但我很少完整地经历所有的步骤,有时步骤会有很大的不同。大多数时候,解决方案在配方中途指向自己(哔哔声“实现我!”).但是每当有一个有问题的编程任务要添加时,一个缓慢而系统的过程会给出最好的长期结果。关于幽灵的完整实现,参见 http://github.com/krother/maze_run .
其他规划工具
规划是编程中的一项基本技能。尽管这会分散我们编写代码的注意力,并且通过严格应用常识可以实现很多目标,但是当事情变得更加复杂时,了解一些规划工具可以节省您的时间。这不是一本关于软件项目管理的书,就不赘述了。然而,我有几个最喜欢的规划工具值得一提:
一页纸的项目计划
在没有正式计划阶段(有时甚至没有正式的项目经理)的项目中,编写一个最小计划通常足以让所有人达成一致。我一直使用的计划总结在一张 A4 纸上:
- 这个项目是关于什么的?
- 团队中有哪些人(以及如何联系他们)?
- 你为什么要写这个程序?
- 主要目标是什么?
- 最重要的子目标是什么?
- 有截止日期吗?
- 有预算吗?
在一个小团队中,有这样一个小计划可以避免很多误解。
问题跟踪器
当你确定了编程任务或者把它们分解成更小的时候,你把它们放在哪里?一种可能性是使用问题跟踪器。问题跟踪器是一个管理编程任务、描述以及谁在负责它们的软件。使用它对防止遗忘有很大的帮助。流行的问题跟踪器有 JIRA、Trac 和 GitHub 上的问题系统。但是基本上任何项目管理系统都提供了跟踪问题的功能。
看板法
看板最初由丰田公司发明,是精益生产中管理库存的一种方法。这个概念被大卫·安德森应用于软件开发。看板流程限制了要同时处理的事情的数量,并将它们显示在一个清晰可见的板上。我的桌子上、浏览器上和厨房里都有看板——它们都工作得很好。看板是一种注重改进和完成工作的实用方法,可以很容易地与工作环境中的大多数现有实践相结合。
最佳实践
- 分解编程任务类似于从云中慢慢浮现的形状,而不是一系列的突然切割。
- 用户故事是对编程任务的简短、非技术性的描述,使其易于管理。
- 细节可以作为项目符号、文本文档、图表和用例描述添加到用户故事中。
- 非功能需求是描述程序使用环境的边界条件。尽早考虑它们可以大大降低程序变得难以维护或无用的风险。
- 基于一个用户故事,用例描述和非功能需求,矛盾和其他问题可以被识别。
- 当分解一个编程问题时,领域知识是必不可少的。
- 问题的常见来源包括缺乏信息、信息过多、现有代码和未来的变化。
- 选择解决主要问题的架构。
- 确定程序组件(函数、类、模块等)后,创建一个框架程序。)来实现该架构。
- 实现是完全分解后的最后一步。
- 分解编程任务的七步过程应该被理解为一个指南。
十六、Python 中的静态类型
像死亡和税收这样确定无疑的事情,可以更坚定地相信。—丹尼尔·笛福,《魔鬼的政治史》
动态类型是 Python 最受称赞的特性之一。受到称赞是因为它允许快速开发。但是动态类型也有其黑暗的一面。动态类型化也意味着在程序的任何地方都不能确定接收到某种类型的对象。在这一章中,我们将研究为什么动态类型是一种风险,以及如何降低这种风险。作为一个例子,让我们考虑我们想要添加一个高分列表到我们的游戏中。一个简单的高分列表包含五个得分最高的球员的得分和他们的名字(见表 16-1 )。
表 16-1。
High Score List with Five Top-Scoring Players
| 运动员 | 得分 | | --- | --- | | 语言 | Five thousand five hundred | | 上下移动 | Four thousand four hundred | | 傻瓜 | Three thousand three hundred | | 爸爸 | Two thousand two hundred | | 她 | One thousand one hundred |为简单起见,让我们将每个条目定义为一个namedtuple:
from collections import namedtuple
Highscore = namedtuple('Highscore', ['score', 'name'])
现在我们可以生成条目作为Highscore的实例:
entry = Highscore(5500, 'Ada')
在 Python 中,没有什么能阻止我们意外地交换顺序或参数:
entry = Highscore('Ada', 5500)
虽然这个命令明显是错误的,但是它被执行并通过,没有任何错误。它产生了一个Highscore的实例,缺陷传播开来,可能直到我们试图将分数与另一个玩家的分数进行比较(除非顺序也被交换)。如果我们运气不好,缺陷根本不会产生错误,并且会一直传播,直到它感染了我们数据的其他部分。主要原因是 Python 中允许的动态类型。使用早期的类型控制,缺陷不会传播。在这一点上,我们已经可以得出结论,动态类型化使得缺陷更容易传播。如果我们想因为动态类型而限制错误传播,我们必须自己添加它。
动态类型的缺点
动态类型的弱点在许多不同的情况下都有所体现。在这一章中,我们将更仔细地研究四种常见的与类型相关的问题:
功能签名
和前面的高分例子一样,Python 在给函数赋值参数时不考虑类型。调用与设计不同类型的函数很容易。因此,我们最终可能会让相同的功能做完全不同的事情,正如这个经典的例子所示:
>>> def add(a, b): return a + b
...
>>> add(7, 7)
14
>>> add('7', '7')
'77'
价值边界
通常,数据包含硬边界(最小值和最大值、列表大小、可能值等)。).Python 并没有阻止我们违反这些界限,结果是语法上正确的废话:
>>> year = -2016
>>> month = 13
>>> days = [day for day in range(1, 33)]
>>> weekday = "Cupcakeday"
(当然,在日期的情况下,使用datetime模块会发现一些这样的问题。但是我们仍然可以编写像date.day + 42这样的表达式,将一个值推到预期范围之外。)
类型的语义
相同的数据可能有不同的含义。假设我们用不同的单位存储长度:
>>> cm = 183.0
>>> inches = 72.05
>>> cm - inches 110.95
由于两个值都有类型float,Python 永远不会抱怨计算明显错误。同样的错误也会发生在货币而不是长度单位上。如果汇率彼此接近,这种缺陷可能很难发现。大多数人不会觉得一个财务计算有百分之几误差的程序有什么好玩的。
复合类型
在 Python 中,通常很难说“我想要一个除了整数之外不能包含任何其他内容的列表。”对于字典、集合、元组等等也是如此。因此,很容易将不兼容的类型混合在一起,就像前面的例子一样。
Python 中更强的类型化可能吗?
这四个问题的共同点是都和打字有关。它们是 Python 特有的问题。综上所述,尽管动态类型有很多优点,但它仍然是 Python 语言的软肋。让我们看看是否可以做些什么来防止这些问题,并使我们的代码输入更加严格。在理想情况下,类似于以下的构造是可能的:
>>> Highscore = typedtuple(['int score', 'str name'],)
>>> entry = Highscore(5500, 'Ada')
当我们不小心弄乱了类型时,Python 会立即抱怨:
>>> entry = Highscore('Charlie', 3300)
Traceback (most recent call last):
Python expected type 'int' but got type 'str'
in line ...
Hey wait. This is not real Python code!
您可能会得出与前面虚构的解释器相同的结论:“但是 Python 不是一种静态类型的语言。Python 里没有静态类型。”我同意。这一章的标题有点让人迷惑。Python 不是一种静态类型的语言,这一点不太可能改变。相反,我们正在寻找变通办法,即在 Python 程序中加强类型控制的策略。幸运的是,有几种类型的控制策略。在这一章中,我们将考察几种这类控制策略的利弊,看看它们是否有助于我们排除最坏的错误。
断言
第一个想法可能是简单地显式检查变量的类型。例如,我们可以在给列表添加高分时创建一个断言:
from collections import namedtuple
Highscore = namedtuple('Highscore', ['score', 'name'])
highscores = []
def add(name, score):
assert type(name) is str
assert type(score) is int
hs = Highscore(score, name)
highscores.append(hs)
这种策略被称为防御性编程。防御性编程背后的思想是,我们“永远不要相信调用我们函数的代码。”这两个断言明确地陈述了函数对其参数的期望。如果违反了这些期望或前提条件,该函数将不会做任何事情。这样,我们可以在类型相关的缺陷传播之前捕捉到它:
add('Ada', 5500)
add('Bob', 4400)
add(3300, 'Charlie')
程序干净地退出,带有一个AssertionError:
Traceback (most recent call last):
File "highscores_assert.py", line 19, in <module>
add(3300, 'Charlie')
File "highscores_assert.py", line 9, in add
assert type(name) is str
AssertionError
使用断言,我们还可以检查数据上更复杂的条件。例如,我们可以确保高分列表中的元素不超过五个:
def add(name, score):
assert type(name) is str
assert type(score) is int
hs = Highscore(score, name)
highscores.append(hs)
highscores = highscores[:5]
assert len(scores) < 5
第二种断言也称为后置条件。我们故意引入另一个失败点来缩短错误传播。因为我们可以用任何有效的 Python 表达式和assert一起使用,所以我们基本上可以检查我们想要的任何东西。前置条件和后置条件(以及一般的防御性编程)的好处在于,它们会在早期以特定的方式失败。一个受欢迎的副作用是断言明确地阐明了需求,因此比文档更可靠。这个断言就像一个来自过去的幽灵,警告你“如果 score 不是一个整数,就会发生不好的事情!”
Hint
在某些语言中,前置条件和后置条件是强大的正式结构,可以在运行代码之前通过外部工具进行检查。在 Python 中并非如此。我们本质上是用一个错误替换另一个错误。
但是防御性编程也有一些严重的缺点。首先,断言膨胀了我们的程序。在add函数的六行代码中,有三行是断言。更多的代码意味着更多的空间来隐藏 bug。第二,一些断言引入了冗余。函数add的最后两条指令负责相同的事情,确保列表不会超过五条。第三,断言需要计算时间,并且会使代码变慢。如果我们要调用前面的函数一百万次,断言将花费我们大约一秒钟的时间。如果我们代码中的计时很重要,或者我们想在 Raspberry Pi 上运行它,断言会很快成为障碍。总而言之,防御性编程倾向于使代码更庞大,更难以快速更改。
然而,当处理一组复杂的依赖关系时,防御性编程可以合理地增强我们的代码。例如,在由许多步骤组成的计算管道中,中间的断言是有意义的。根据计算结果,我们可以确保数字确实是浮点数,三角形确实有三条边,并且开始和结束时的样本数是相同的。在一长串事件中,通过一个assert语句缩短错误传播可能会挽救我们的一天。也有人说,防御性编程可以用来防止老化的程序崩溃(一段时间)。但是我不会推荐防御性编程作为帮助控制一般类型的最佳实践。例如,在前面的高分示例代码中,这肯定是多余的。
NumPy
作为最突出的 Python 库之一,NumPy 在这里值得一提。顾名思义,NumPy 是作为一个库设计的,用来处理大型数组和数值矩阵。在内部,NumPy 将数据映射到用 c 编写的数据结构中,这样做的好处是 NumPy 数组的所有元素都具有相同的类型。用 NumPy 编写,我们的高分列表可能是这样的:
import numpy as np
scores = np.array([5500, 4400, 3300, 2200, 1100], dtype=np.int64)
players = np.array(['Ada', 'Bob', 'Charlie', 'Dad', 'Elle'], dtype=np.str)
严格保留数组的类型。我们可以检查,如果我们试图打破一个不匹配类型的数组。在需要数字的地方使用字符串会立即失败:
>>> scores[2] = "Charlie"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for long() with base 10: 'Charlie'
但是,如果我们将一个数字放入一个字符串数组,它会自动转换:
>>> players[3] = 100
>>> players
array(['Ada', 'Bob', 'Charlie', '100', 'Elle'], dtype='|S6')
这意味着我们实际上不能很好地输入控制字符串,因为每个 Python 对象都有一个字符串表示。在scores中插入一个浮点数也会导致转换。因此,这些类型不是在所有方向上都严格强制的。dtype参数为整个数组设置一个类型。对于 Python 中的许多基本数据类型,dtype参数有多种选项(不同精度的整数和浮点数等)。).更复杂的数据类型由dtype=np.object表示。这意味着 NumPy 不能对字典、集合甚至自定义类型施加任何类型控制。'object'型对我们的目的没有帮助。
另一个缺点是 NumPy 接口迫使我们将玩家的名字和分数保存在两个独立的数组中。在实践中,这限制了在 NumPy 中输入用于构建类型控制的数据结构的有用性——它根本不是为此而构建的(除非我们使用 NumPy . re array 来稍微缓解这种情况)。NumPy 在最初构建的方面要好得多:数字数组。NumPy 中数值计算的速度是出了名的,这也是大多数人使用该库的原因。
总之,NumPy 的类型系统为相同类型的对象(最好是数字)的列表式集合引入了一些严格性。使用错误的类型仍然会失败,但会更早。这很好,但是它并没有给我们一个足够强的理由仅仅为了类型控制而使用 NumPy 数组。
What about the pandas library?
pandas library 是一个构建在 NumPy 之上的豪华界面,用于帮助分析表格数据。pandas 推断数据列的类型类似于 NumPy。然而,类型检查更加宽松:您可以将所有内容分配给 pandas 中的一个列,而不管它是什么类型。如有必要,该列将自动更改其类型。这导致了讨厌的类型相关的错误;例如,读取一个用逗号(,)而不是点(.)将产生一列字符串而不是浮点数。
数据库 s
因为我们主要感兴趣的是控制数据的类型,所以我们可以将数据委托给外部工具(而不是像 C 扩展那样将所有事情都委托给外部工具)。有一个很大的工具家族就是为了这个目的而建立的:数据库。主要思想是:如果我们使用数据库来存储数据,我们可以强加一个刚性的结构,包括数据类型。因此,每次我们访问我们的数据时,我们都确信我们获得的数据正是我们想要的类型。这种方法的好处是 Python 可以与几乎所有可用的数据库引擎很好地协作。这里我们将使用 PostgreSQL 作为数据库引擎,使用 psycopg2 作为 Python 模块,创建数据库连接来实现高分列表。根据您的系统,建立数据库的过程可能有点复杂。在 Ubuntu 上,以下命令就足够了:
sudo apt-get install postgresql
sudo /etc/init.d/postgresql start
createdb highscores
sudo pip install psycopg2
如果您想跳过安装,而是想专注于代码,下面是如何将 highscore 表存储到数据库和从数据库中读取:
import psycopg2
DB_SETUP = "'
CREATE TABLE IF NOT EXISTS scores (
player VARCHAR(25),
score INTEGER);
"'
db = psycopg2.connect(host="127.0.0.1",
user="krother",
dbname="highscores")
cur = db.cursor()
# create the database
cur.execute(DB_SETUP)
# fill the database with entries
insert = "INSERT INTO scores VALUES (%s,%s);"
cur.execute(insert, 'Ada', 5500)
cur.execute(insert, 'Bob', 4400)
# retrieve the top five entries in descending order
query = 'SELECT player, score FROM scores ORDER BY score DESC LIMIT 5;' cur.execute(query)
for result in cur.fetchall():
print(result)
db.close()
执行代码会导致:
('Ada', 5500L)
('Bob', 4400L)
数字后的'L'表示这些是长整数。如果我们添加另一个带有交换参数的调用(3300, 'Charlie'),我们会得到想要的类型错误:
Traceback (most recent call last):
File "highscores_postgres.py", line 20, in <module>
cur.execute(insert, (3300, 'Charlie'))
psycopg2.DataError: invalid input syntax for integer: "Charlie"
LINE 1: INSERT INTO scores VALUES (3300,'Charlie');
用于其他 SQL 数据库的 Python 模块几乎以同样的方式工作。更方便的方法是使用对象关系映射器(ORM),这是一组创建 SQL 查询并将结果转换为 Python 对象的类。广泛使用的 ORM 是 SQLAlchemy 和 Django 中的数据模型。大多数 NoSQL 数据库(MongoDB、Redis 等)也有 Python 接口。),但是它们之间的差别太大了,因此无法在这里讨论它们的输入方法。
Warning
SQLite 数据库是随 Python 一起安装的全功能 SQL 数据库接口。然而,SQLite 是动态类型的,因此对类型控制没有任何帮助。
使用数据库有几个额外的好处:持久性(即使我们的程序崩溃,数据也是安全的)、互操作性(非 Python 程序员也可以使用它们)和可伸缩性(如果 PostgreSQL 数据库变得太小,我们可以用专用的数据库服务器(或 Oracle 集群)来代替它,而不用从根本上改变我们的代码)。当然,数据库也不是完美的解决方案。首先,与 SQL 数据库交互会在代码大小和速度方面产生大量开销。为了管理单个表,编写一个助手模块来构建和解释 SQL 查询是一个好主意,即使只是为了让 SQL 代码看不见。使用 ORM,代码会变得更短,但是在助手模块中创建数据模型仍然是一个好主意。其次,我们只能使用数据库提供的类型(例如,SQL 数据库中没有字典)。第三,我们仍然没有在程序中得到类型控制;我们只是将数据管理移出 it。
考虑到这些限制,数据库对类型控制问题有什么影响?简而言之,我们控制了违规最严重的数据类型:我们为我们的“业务数据”获得了一个清晰、持久、强类型的结构在我们的 Python 代码中,所有其他具有更多本地上下文的变量都像以前一样保持动态,数据库为类型控制问题提供了一个实用的局部解决方案。我们并没有非常接近“真正的”静态类型,但是它是有效的。
集成 C 代码
Python 非常擅长与用 C/C++编写的代码进行交互。通常,这样做是为了加速 Python 代码。然而,如果没有静态类型,C 代码是不可想象的,这就是为什么我们要仔细研究这个选项。用 Python 编写 C 扩展允许我们像调用 Python 函数一样调用 C 函数。C 头文件Python.h提供了将标准 Python 数据类型转换成 C 类型的函数。这意味着,我们在程序的 C 部分得到了完全的类型控制。另一个实现相同目的的方法是 scipy.weave 包,它允许直接在 Python 模块中以字符串形式执行 C 指令。
然而,在这两种情况下,我们负责在 Python-C 接口中捕捉所有类型的错误。这种类型检查可以在 C 端(使用Python.h)或 Python 端(作为断言,使用scipy.weave)完成。在 C 中使用错误的类型会带来整个程序因分段错误而崩溃的风险。一般来说,除了静态类型之外,C 代码给了我们灵活性和速度。然而,也有一些缺点:
- 有一些开销:我们需要编写 Python-C 适配器,设置 C 编译器,并将所有东西集成到构建程序的任何东西中。
- 在 Python 方面,没有什么变化。一切都一如既往地充满活力。
- 编译 C 代码意味着我们编译的代码不再是独立于平台的。
- 严格来说,使用 C 代码已经不是 Python 了。如果我们有充分的理由用静态类型语言编写代码,也许将整个程序转换成这种语言是值得考虑的(用更严格的实现替换 Python 原型是许多开发团队追求的最佳实践)。
当优化程序速度或使用外部 C 库时,类型控制是一个受欢迎的副作用。不过,我还没见过有人为了类型控制而编写 C 扩展。
西通
Cython 项目受到了很多关注(参见 http://docs.cython.org/en/latest/index.html ). Cython 是一个将 Python 源代码转换成 C 源代码的工具。然后,我们可以使用 C 编译器来构建一个可执行程序。可执行程序可以在没有 Python 解释器的情况下使用,并且通常比常规 Python 程序更快。除了极少数例外,Cython 解释 Python 代码的方式与标准 Python 解释器相同。我们感兴趣的是,我们可以添加 Cython 理解的类型注释。highscore 实现的 Cython 版本应该是
cdef struct Highscore:
char *name
int score
cdef Highscore scores[5]
scores[0] = Highscore('Ada', 5500)
scores[1] = Highscore('Bob', 4400)
for i in range():
print(scores[i].name, scores[i].score)
该结构是一个类似于namedtuple的 C 数据结构,但是具有静态类型。
假设安装了 Cython,我们可以编译一个可执行文件并在 Unix shell 中运行它:
cython --embed highscores_cython.pyx
gcc -Os -I /usr/include/python3.4m -o highscores.out highscores_cython.c -lpython3.4m -lpthread
./highscores.out
这导致了
Bob 4400
Ada 5500
如果我们尝试在 Python 代码中使用无效的类型会发生什么?
scores[2] = Highscore(3300, 'Charlie')
重新编译程序时,第一步(用 C ython编译)会出现一堆错误:
highscores_cython.pyx:11:28: Only single-character string literals can be coerced into int
Error compiling Cython file:
------------------------------------------------------------
...
cdef Highscore scores[5]
scores[0] = Highscore('Ada', 5500)
scores[1] = Highscore('Bob', 4400)
scores[2] = Highscore(3300, 'Charlie')
------------------------------------------------------------
highscores_cython.pyx:11:22: Cannot assign type 'long' to 'char *'
这里我们终于有了真正的静态类型:在执行任何代码之前,类型被检查并生成错误消息!我们付出的代价是代码本身只是部分类似于一个 Python 程序(后半部分是,但前半部分看起来更像 C)。幸运的是,C 类型是可选的。我们可以通过 Cython 运行普通的 Python 代码,或者在一些关键的地方添加静态类型。这种方法的缺点是 Cython 注释代码的构建过程完全改变了。标准 Python 解释器不再理解 Cython 代码。此外,如果我们在 Cython 中导入 Python 模块,它们也需要编译。
使用 Cython 编译可执行程序并不是使用 Cython 最正统的方式。它的主要应用领域是生成库。与 C 扩展和 NumPy 一样,使用 Cython 的一个常见动机是速度。然而,通过额外的类型注释,使用 Cython 进行类型控制是一个可行的选择。如果您不担心额外的编译步骤(并且知道如何配置 C 编译器),Cython 将为您提供两个世界的最佳选择。
类型提示
从 Python 3.5 开始,可以注释变量和参数的类型。这些类型提示是该语言的一个快速发展的特性,这里给出的例子需要 Python 3.5 或更高版本。类型提示背后的主要思想是,我们将类型写入 Python 代码,以便其他程序员知道我们希望变量具有什么类型。注意,类型提示是提示,不是检查——目前,Python 解释器忽略它们。带类型注释的高分列表如下所示:
from typing import NamedTuple, List
import csv
Highscore = NamedTuple('Highscore', [('score', int), ('name', str)])
class HighscoreList:
"""A sorted list of top scores"""
def __init__(self, places: int=10) -> None:
self.scores = [] # type: List[Highscore]
self.places = places
def add(self, highscore: Highscore) -> None:
self.scores.append(highscore)
self.scores.sort()
self.scores = self.scores[:self.places]
def __repr__(self) -> str:
return str(self.scores)
hs = HighscoreList()
hs.add(Highscore(5500, 'Ada'))
hs.add(Highscore(4400, 'Bob'))
在代码中,类型提示出现在几个地方:
- 函数签名-函数参数具有类型(例如:整数的:
int或高分对象的:Highscore)。返回值也有类型,例如,-> None或-> str。 - 变量定义——第一次定义变量时,以
# type:开头的注释指定了它的类型。 - 复合类型——我们使用
typing模块将scores定义为一列Highscore对象。 - 预定义的类型——我们定义自己的
NamedTuple,以及每个字段的类型。请注意,这是一个与常见的collections.namedtuple不同的元组。
typing提供了更加详细和灵活的方式来注释类型,例如,Any(类型无关紧要)、Union(从类型列表中选择一个),以及NewType(现有类型的不同克隆)。您可以在 PEP484 文档和typing模块的文档中找到完整的描述(参见 https://docs.python.org/3.5/library/typing.html#module-typing )。就其本身而言,类型提示作为文档是有帮助的:其他程序员知道我们的意思。这种方法的最大缺点是类型提示什么都不做。Python 解释器会忽略它们,由错误类型导致的运行时失败根本不会改变。
但是我们可以使用额外的工具来解释类型提示。目前,在这些工具中,我们找到了用于生成文档的 pydoc、在编写代码时警告类型冲突的 PyCharm IDE(见图 16-1 )和 mypy。在这里,我们将重点关注 mypy。
图 16-1。
Type checking in PyCharm for function calls. The checker needs to be activated by setting the ‘Python – editor – Type checker’ preferences to ‘Error’. Also see https://www.jetbrains.com/help/pycharm/5.0/type-hinting-in-pycharm.html .
我是麦比
mypy 是一个静态类型检查器(参见 http://mypy-lang.org /)。它检查 Python 代码,并检查带注释的类型是否被一致地使用。如果没有,mypy 会在执行任何代码之前创建一条错误消息。mypy 可以与一起安装
> pip install mypy
接下来,我们通过键入mypy <filename.py>从命令行调用 mypy:
> mypy highscores.py
如果一切都一致,mypy 的输出是空的。让我们试着给 Python 脚本添加一个错误的类型。
add(3300, 'Charlie')
重新运行 mypy 会产生一条错误消息:
> mypy highscores.py
highscores.py, line 37: Argument 1 to "Highscore" has incompatible type "str";
expected "int"
highscores.py, line 37: Argument 2 to "Highscore" has incompatible type "int";
expected "str"
这最终是我们希望看到的应用严格类型的错误,我们不需要过多地扭曲我们的 Python 程序。耶!为了对第三方模块创建的对象应用类型检查,mypy 使用存根文件。这些包含 mypy 解释的.pyi文件中函数和变量的带类型注释的签名。Python 标准库和许多其他模块的 Stubfiles 可以在 https://github.com/python/typeshed/ 的 typeshed 项目中找到。
请注意,mypy 并没有找到每一个问题。例如,如果我们使用collections.namedtuple而不是typing.NamedTuple,元组中的类型将被忽略。像 pylint 一样,mypy 增加了额外的质量检查,我们可以打开和关闭它。因此,它可以集成到像 Jenkins 这样的持续集成平台中,这样每次代码添加到存储库时都会执行类型检查。目前,mypy 的 0.4.6 版本已经发布(2016 年 11 月)。该工具正在非常积极地开发中,可能会发生变化,就像 Python 中的类型提示一样。鉴于它们短暂的历史,我不会把类型提示和 mypy 的组合称为最佳实践,但它是一个非常有前途的候选,我希望将来能看到更多的 mypy。
使用哪种类型控制方法?
我们已经看到了六种可能的策略来改进 Python 中的类型控制。它们是否有助于我们限制程序中的类型,使大多数的TypeErrors和/或NameErrors永远不会出现?让我们回顾一下每种方法的优缺点。为此,让我们考虑一下我们在开始时提出的四个问题:
- 函数签名:我们能强制一个函数被调用或者返回一个特定的类型吗?(
9 + 9 vs. '9'+ '9') - 语义类型:我们可以定义多个不同名的类型吗?(保持厘米和英寸分开)
- 价值边界:我们可以将价值限制在某个范围或选择中吗?(选手必须有正分)。
- 类型组合:我们可以限制复杂类型的组合吗?(例如,高分列表中的所有项目必须是
Highscore对象)。
表 16-2 包含这些问题的快速答案。让我们详细总结一下这六种策略。
表 16-2。
Strategies to Improve Type Control in Python
| 方法 | 断言 | NumPy | c 代码 | 数据库 | 西通 | 我是麦比 | | --- | --- | --- | --- | --- | --- | --- | | 功能签名 | 是 | 不 | 是 | 不 | 是 | 是 | | 价值边界 | 是 | 不 | 不 | 是 | 不 | 不 | | 类型语义 | 是 | 不 | 是 | 不 | 不 | 是 | | 复合类型 | 是 | 不 | 是 | 桌子 | 是 | 是 | | 失败时间 | 执行 | 执行 | 执行 | 执行 | 编辑 | 分析 | | 开销 | 高的 | 低的 | 高的 | 媒介 | 高的 | 低的 |断言和防御性编程给 Python 代码本身增加了更严格的类型控制。断言允许我们引入任何类型的约束,提前失败,并带有定义好的错误消息。这听起来是一个完美的解决方案。实际上,维护断言的成本很高:它们增加了代码的大小,使程序不那么灵活,不那么 pythonic 化。
但是在长时间的计算或者跨越几十个模块的过程中,单个断言可以节省我们许多调试时间。节约使用,是一个有价值的工具。
NumPy 为我们提供了整数和浮点数的类型控制。NumPy 数字数组是一个数字数组,所以我们在实现计算时不必担心输入问题。对于其他类型,它对我们没有任何帮助。这是一个非常棒、非常有用的库,但是类型控制不是 NumPy 的初衷。
乍一看,为控件类型编写 C 扩展似乎是个好主意,但事实证明不值得这么麻烦。我们有很好的方法来控制 C 部分中的类型,但是 Python 部分完全不受影响。开销很高,因为我们需要编写防御性代码来避免 C 代码中的打字问题。如果你依赖于强类型语言的好处,也许 Python 不是解决这个问题的正确选择。但是,如果我们计划用 C 扩展 Python(例如,为了速度),知道打字更容易是件好事。
将数据存储在强类型数据库中通常是个好主意。我们将核心数据委托给一个专用的、严格类型化的引擎。类型控制只是使用数据库的众多好处之一。事实上,这是一个副作用,数据库仍然不能修复 Python 程序内部的类型问题。不管我们是使用 ORM 还是将 SQL 代码写入 Python 模块,数据库在构建、管理和与它交互时都会产生一些开销。然而,我们得到了我们想要的:对我们数据的更多控制。
Cython 是这里唯一一个将真正的静态类型引入 Python 代码的策略。它也是唯一拒绝执行错误类型代码的方法。这是有代价的:结果是一个有点奇怪的 Python-C 混合体,结合了两者的优点。一个好的方面是,我们可以决定程序的哪些部分需要静态类型化。最大的缺点是构建 Cython 程序非常不同。需要做一些工作来配置基于 Cython 的库和可执行文件的编译。你的生活越接近 C(或者你越愿意学习 C),Cython 就越有用。
我们最后的竞争对手,类型提示和 mypy 的结合提供了一个强大的类型系统,它是 Python 本身的一部分(至少在最新版本中)。类型提示文档代码,mypy 在最早的时候警告许多类型冲突。让 mypy 作为一个独立的工具来执行检查与许多软件开发团队使用的工具链集成得很好。使用 mypy 的一个好方法是让代码自动检查类型违规(例如,在提交到代码库之后)。Python 中的类型提示仍然是一个新特性,还不是一个标准过程,但我们可以期待在未来看到更多关于类型提示的内容。
综上所述,在某种程度上用 Python 控制类型是可能的。前面描述的所有方法都有副作用(好的和坏的)。最值得注意的是,除了 Cython 和 mypy 之外的所有控件类型都只能在运行时使用。使用任何类型的控制策略,我们的程序仍然会失败。但是它们会更早地失败,有更清晰的错误信息,这样缺陷可以更快地被发现。在一个增长到 1000 行甚至更多的程序中,这是一种使开发不那么痛苦的健壮性。
最佳实践
- 动态类型是 Python 缺陷的主要来源。
- 与类型相关的问题包括函数签名、值边界和语义以及复合类型。
- 有几种策略可以改善 Python 程序中的类型控制。
- 断言可以用于检查类型和其他限制,但是会增加相当大的开销。
- 防御性编程是系统地使用断言来保护函数免受不一致参数的影响。
- NumPy 数组有一个 set 类型,只对数字严格执行。
- SQL 数据库有助于将核心数据从 Python 程序转移到严格控制类型的表中。
- C 扩展使用静态类型,但是程序员负责在 Python-C 接口捕捉错误。
- Cython 是一个从 Python 代码生成 C 代码的工具。Cython 使用类型注释来加速程序。
- 类型提示注释 Python 代码,但不改变代码的执行方式。
- mypy 是一个独立的工具,它在不执行代码的情况下检查类型提示的一致性。
十七、文件
“但是即使是单独工作的黑客,”无名师说,“也会和其他人合作,并且必须不断地和他们清楚地交流,以免他的工作变得混乱和丢失。”“你说的其他人?”神童问道。无名师说:“你们所有未来的自己。”—Eric S. Raymond,“Foo 大师和编程神童”
20 世纪 80 年代,当我父母翻新房子的木板时,我母亲在墙和新木板之间放了一张报纸。当我想知道妈妈在做什么时,她解释说:“这是我们给未来的信息。无论谁是下一个更换面板的人,都会找到报纸,看到我们是如何看待当今世界的。”也许几十年后,其他人会在镶板建成时瞥见我们的生活(例如,报纸被认为是历史的见证)。回顾过去是困难的。当然,这比展望未来容易。有一些信息:我们可以看到旧的,木制面板,在他们可能枯萎的状态。我们可以看到已经建成的东西,但看不到我们的精神状态、我们的希望、我们的理由、我们的意图。记录软件是相似的。
当我们编写软件时,文档是我们未来的信息。它有助于未来的开发人员理解我们是如何构建某个东西的,以及它是如何构建的。考虑如下陈述:“该程序包含 7 个类和 23 个函数。”在最好的情况下,这将是对您在代码中看到的内容的准确描述。更常见的是,随着程序的不断发展,这种文档在短时间内就会出错。相比之下,像“我们建造迷宫作为游戏的中心数据结构和它周围的一切”这样的陈述有助于你理解在代码中很难看到的想法。但是后一种说法缺乏技术细节:我们需要在描述想法和技术细节之间找到正确的平衡。在这一章中,我们将探索如何为 Python 项目编写有用的、平衡的文档。
我们为谁写文档?
一般来说,我们为三类人写文档:开发人员、其他团队成员和用户。理解这一点是很重要的,在未来的几个月里,作为开发人员,你是你之前写的文档的主要受益者。这就是我们在这里关注以开发人员为中心的文档的原因。在某种程度上,一个好的面向开发人员的文档也可以帮助项目中的其他参与者,例如加入项目的新团队成员或者报告开源项目中的 bug 的人。
这里我们将忽略面向非程序员最终用户的文档。写用户手册或者,例如,一个项目相关的文档当然很重要,但是这是一种完全不同的技术写作。从工程的角度来看,是技术文档挽救了这个项目。
sphinx:Python 的文档工具
很长一段时间以来,我们已经能够在我们的主项目目录中使用一个单独的README .md文件,加上一个用于法律事务的LICENSE.TXT文件,轻松地记录我们的程序。有很多项目的README文件都绰绰有余。但是随着程序的增长,它最终会达到单个文件不再足够的大小。以下是你的程序已经超出了README文件的一些症状:
README文件太长,浏览起来不舒服。- 该文件描述了许多只与某些人相关的特殊情况。
- 有许多代码示例,很难使它们保持最新。
- 您无法自动验证文档中的代码示例是否正确。
当我们发现前面的一个或多个适用时,是时候切换到更大的文档工具了。我们仍将保留README文件,即使只是为了指向更大的文档。在这一章中,我们将使用 Sphinx(Python 可用的标准文档工具)记录我们项目中的迷宫(generate maze模块)。许多大大小小的 Python 项目都是用 Sphinx 编写的,包括 Python 本身。
简而言之,Sphinx 结合了我们编写的文档文件和我们正在记录的 Python 源代码。然后 Sphinx 将这些文件编译成 HTML、PDF 和 EPUB 文档(见图 17-1 )。文档文件以 ReStructuredText ( .rst格式编写,可能包含自动生成的目录、超链接,甚至自动化测试。在这一章中,在回到什么是好的文档的问题之前,我们将浏览所有这些特性。
图 17-1。
Workflow for creating documentation with Sphinx
建立狮身人面像
首先,我们需要安装斯芬克斯。这可以使用pip:来完成
pip install Sphinx
Sphinx 使用了许多其他 Python 包。最值得注意的是,它使用pygments库在文本文档中创建语法突出显示的源代码。
我们将在项目的docs/文件夹中添加文档。如果您使用了pyscaffold来设置您的项目,它应该已经在那里了。如果没有,只需创建一个空的docs/目录:
cd docs
接下来,我们运行sphinx-quickstart来初始化文档:
sphinx-quickstart
斯芬克斯问了你很多问题。在大多数情况下,默认值就可以了,但是我建议更改一些值。对于第一个问题,使用默认设置就可以了:
> Root path for the documentation [.]:
..
> Separate source and build directories (y/n) [n]:
..
> Name prefix for templates and static dir [_]:
对于项目名称、作者和版本号,您可以插入您喜欢的任何内容:
> Project name:
..
> Author name(s):
..
> Project version:
对于接下来的三个问题,默认值是正确的:
> Project release [1]:
> Source file suffix [.rst]:
> Name of your master document (without suffix) [index]:
现在我们已经到了有趣的部分。当程序询问以 epub 格式构建文档时,停止接受默认值。这是一个非常有用的东西,所以我们说“是”。
Sphinx can also add configuration for epub output:
> Do you want to use the epub builder (y/n) [n]: y
接下来是扩展。我强烈建议更改一些非常有用的默认值,比如autodoc, doctest, todo, ifconfig,和viewcode。
Please indicate if you want to use one of the following Sphinx extensions:
> autodoc: automatically insert docstrings from modules (y/n) [n]: y
> doctest: automatically test code snippets in doctest blocks (y/n) [n]: y
> intersphinx: link between Sphinx documentation of different projects (y/n) [n]:
> todo: write "todo" entries that can be shown or hidden on build (y/n) [n]: y
> coverage: checks for documentation coverage (y/n) [n]:
> pngmath: include math, rendered as PNG images (y/n) [n]:
> mathjax: include math, rendered in the browser by MathJax (y/n) [n]:
> ifconfig: conditional inclusion of content based on config values (y/n) [n]: y
> viewcode: include links to the source code of documented Python objects (y/n) [n]: y
最后,斯芬克斯询问剧本创作。您可以按两次 enter 键来完成配置。
> Create Makefile? (y/n) [y]:
> Create Windows command file? (y/n) [y]:
我们将在后面使用这些特性中的大部分。
Sphinx 创建的文件
Sphinx 在doc/文件夹中创建以下文件:
_build conf.py index.rst Makefile _static _templates
文件index.rst是您的文档的主文件。_build/目录将包含编译后的文档。文件conf.py包含我们在sphinx-quickstart配置脚本中选择的设置,这样我们可以在以后编辑它们。我们现在不需要担心其他文件。
构建文档
为了创建文档,我们需要在系统上安装 make 工具和 LaTeX。在 Ubuntu 上,我们可以用
sudo apt-get install make
sudo apt-get install texlive-full
注意,LaTeX 包非常大(1.8 GB)!
构建 HTML 文档
我们现在可以通过键入以下命令将文档编译成 HTML
make html
当我们在网络浏览器中打开文件_build/html/index.html时,我们会看到一个默认页面。该页面还没有包含太多内容,因为我们还没有编写任何文档(您并不期望所有的文档都自己编写,对吗?).也参见图 17-2 。
图 17-2。
Barebones HTML documentation generated by Sphinx
构建 PDF 文档
或者,我们可以编译 PDF 文档。这需要安装 LaTeX 和 pdflatex 包。
make latexpdf
PDF 可在_build/latex目录中获得。
Warning
在撰写本文时,Sphinx 报告说它无法找到文件iftex.sty。我通过手动下载文件并在 Sphinx 需要时多次粘贴文件的完整路径来解决这个问题。我认为这本书出版时这个问题会得到解决。
构建 EPUB 文档
您也可以构建 EPUB 电子书格式:
make epub
使用您最喜欢的电子书阅读器打开来自_build/epub的文档。
按下make [TAB]键,你会看到一个构建文档的其他选项列表。在内部,make 使用了sphinx-build程序。构建 HTML 文档的一种更冗长的方式是
sphinx-build -b html . _build/html
在哪里?是包含所有源文件的目录,而_build/html是编译文档被写入的目录。
编写文档
Sphinx 使用reStructuredText作为标记格式。一般来说,文档是以文本文件的形式编写的,后缀为.rst。记录generate maze模块的剩余文件tile grid.rst可以包含以下文本:
The Maze Generator
------------------
The module "maze_run.generate_maze" is responsible for generating mazes. It creates a **two-dimensional list** with a *fixed size* that can be used to create a graphical representation in Pygame.
迷宫由圆点代表的走廊和散列代表的墙壁组成。该算法在边界附近留下一个圆形路径。
一些特殊字符用于表示格式。例如,给一行加下划线是一个标题,而双撇号为代码、shell 命令等创建固定宽度的字体。同样,您可以制作斜体和粗体文本、图像等等。一些例子见表 17-1 。完整的休止符格式在 www.sphinx-doc.org/en/stable/rest.html 中解释。
表 17-1。
Markup in the ReStructuredText Format
| 功能 | 剩余标记 | | --- | --- | | 大胆的 | *文本* | | 意大利语族的 | **文本* * | | 固定宽度(代码) | “代码” | | 超链接 | `'link name` < [`http://academis.eu/`](http://academis.eu/) > | | 项目符号列表 | `* first bullet` | | 列举 | `1. first item` | | 标题 1 | `underline with '--------', same length as text` | | 标题 2 | `underline with '========', same length as text` | | 标题 3 | `underline with '++++++++', same length as text` |指令
指令是一种特殊的标记。指令是决定 Sphinx 如何生成文档的规则:Sphinx 的大多数有趣特性,如将文档链接在一起、编写 Python 代码和包含 Python 文档字符串,都需要使用指令。所有指令都以..并以::)结尾,因此,例如,在文档中包含图像的指令是
.. image:: example_maze.png
组织文档
中央的一个指示是toctree。它创建了一个树状目录。在 Sphinx 中,所有文档都是按照目录或 toctree 来组织的。
.. toctree::
mazes.rst
tile_grid.rst
sprite.rst
:maxdepth: 2
这里,文档 mazes.rst 和 tile grid.rst 已添加到目录中。指令:maxdepth : 2 告诉 Sphinx 不仅要包含这里列出的文档,还要递归地将 toctree 语句中列出的任何文档包含在各自的文档中。当编写 toctree 指令时,我们可以使用绝对和相对路径,并且在一个文档中有多个toctree语句。Sphinx 将自动创建整个文档中所有目录树的导航和索引,使用文件中的标题作为标题。
Warning
指令中两个点后面的空格是至关重要的!如果你忘记了,写下来了,例如,..toctree:::代替..toctree::,Sphinx 会把那一行当做普通文本,不会抱怨。
代码示例
编写软件文档时最重要的事情之一就是代码本身。要在.rst文件中包含 Python 代码,我们只需设置一个双冒号(::)并引入一个缩进段落:
Using the "generate_maze" module::
>>> from maze_run.generate_maze import create_maze
>>> maze = create_maze(14, 7)
>>> print(maze)
##############
#............#
#.#.#..#.###.#
#.#...##.#...#
#...#....#..##
#.#....#.....#
##############
Python 代码将被呈现为语法高亮显示的文本,无论它是否包含提示符号(> > >)。渲染由 pygments 库完成。这个选项足以编写一本包含许多代码示例的食谱(见图 17-3 )。
图 17-3。
HTML output with source code rendered by Sphinx
从文档字符串生成文档
如果我们在设置 Sphinx 时安装了autodoc扩展,我们可以从 docstrings 为函数、类或整个模块生成文档。在autofunction指令中,我们可以引用程序中的一个函数:
.. autofunction:: maze_run.generate_maze.create_maze
在文档中,该函数的签名与文档一起出现。如果您还包含了 viewcode 扩展,那么您还会自动获得一个到源代码的链接。
Hint
您需要将PYTHONPATH变量设置为带有maze run包的目录,以便 Sphinx 可以导入它。你可能已经在前面的某一章中完成了(或者使用pip和-e选项)。为了使模块可以导入,我偶尔会添加正确的导入路径到conf.py中的sys.path,如果默认情况下在PYTHONPATH变量中找不到的话。
也可以用automodule指令发现整个模块:
.. automodule:: maze_run.generate_maze
:members:
Sphinx 收集所有具有 docstring 和模块级 docstring 的函数和类,并编译相应的文档。如果您没有看到由autodoc生成的函数描述,这并不意味着这样的函数不存在,只是它没有 docstring。
注意,即使导入失败,Sphinx 也会完成文档的构建。除了几个非常严重的错误,Sphinx 总是试图完成构建,而不是因错误而终止。在生成的文档中查找错误信息(参见图 17-4 )。
图 17-4。
Error messages generated by Python appear in the documentation generated by Sphinx Warning
autofunction和automodule都以与 Python 程序相同的方式导入模块。在函数体中执行的代码可能有副作用(例如,如果它创建或删除文件)。
文档测试
文档测试是将文档和自动化测试结合起来的有效方法。strongdoctests 由 Sphinx 文档中的 Python shell 会话组成。当执行测试时,Sphinx 执行这些会话中的命令,并将它们的输出与文档中的输出进行匹配。这样,我们可以有效地提供被证明是正确的文档。
要编写 doctest,我们需要在 Sphinx 中激活doctest扩展(如前面的初始配置中所述)。然后,我们将一个 Python shell 会话复制到一个.rst文件中,并将其放在doctest指令下。从一组走廊位置生成迷宫的 doctest 如下所示:
.. doctest::
>>> from maze_run.generate_maze import create_grid_string
>>> dots = [(1,1), (1, 2), (2,2), (2,3), (3,3)]
>>> maze = create_grid_string(dots, 5, 5)
>>> print(maze.strip())
#####
#.###
#..##
##..#
#####
要执行测试,请编写
make doctest
在输出中,我们获得了每个.rst文档通过和未通过测试的概述,以及最后的总结。每一行 Python 代码都被解释为一个单独的测试(因为每一行都可能单独失败):
Document: tile_grid
-------------------
1 items passed all tests:
4 tests in default
4 tests in 1 items.
4 passed and 0 failed.
Test passed.
Doctest summary
===============
4 tests
0 failures in tests
0 failures in setup code
0 failures in cleanup code
build succeeded.
相反,如果我们写了一个失败的测试:
.. doctest::
>>> 1 + 2
2
在这种情况下,make doctest的输出包含一条故障消息,指示文档文件中的行:
**********************************************************************
File "tile_grid.rst", line 21, in default
Failed example:
1 + 2
Expected:
2
Got:
3
**********************************************************************
Are doctests a replacement for Unit Tests and other kinds of automated testing?
在某种程度上,我们可以用 doctests 代替自动化测试,就像在第八章中用 pytest 创建的那些测试一样。特别是,如果我们正在测试 Python 库的以用户为中心的一面——它的接口——文档测试是非常有价值的。然而,对于测试较小代码单元的详细功能来说,它们并不是那么好。当我们试图用 doctests 覆盖很多边界案例或者一系列测试场景时,代码量会迅速暴涨。此外,doctests 缺少许多选项,如测试参数化或有选择地运行测试。因此,最佳实践是使用 doctests 来创建经过测试的、人类可读的文档,而不是详尽的测试集。
配置 Sphinx
待办事项条目
我们可以将文档中的条目标记为待办事项。例如,我们可以使用todo指令在文档中标记计划的特性。以下两个功能尚未实现:
.. todo::
Adding caching to the function drawing mazes could accelerate graphics a lot.
.. todo::
Describe how to move an element of a maze.
创建待办事项列表
todolist指令向我们显示了到目前为止定义的所有 TODO 项目:
TODOs in the MazeRun project
++++++++++++++++++++++++++++
.. todolist::
使用todo扩展(启动 Sphinx 项目时可配置),我们可以在最终文档中打开和关闭 TODO 项目。conf.py中的配置变量用于切换待办事项(默认设置为True:
todo_include_todos = True
或者,我们可以使用命令行选项覆盖配置设置:
sphinx-build -b html -D todo_include_todos=1 . _build/html
sphinx-build -b html -D todo_include_todos=0 . _build/html
有条件建筑
类似地,我们可以添加自己的变量(例如,为不同的操作系统、处理器架构或简单的长版本和短版本编译文档)。让我们假设我们想要为开发人员构建一个长文档,为网站构建一个短文档作为开胃菜。
这需要打开ifconfig扩展(这应该在启动项目时发生,但是我们可以稍后在conf.py中添加它)。为了创建我们自己的交换机,我们需要在conf.py的末尾定义一个新的配置变量size。这可以用几行代码来完成:
size = "long"
def setup(app):
app.add_config_value('size', ", 'env')
接下来,我们在.rst文件的ifconfig指令中添加可选文本。我们为短版本添加了一小段文本,为长版本添加了一个额外的文档链接。
.. ifconfig:: size == "short"
Magic Methods in MazeRun
++++++++++++++++++++++++
Some classes in MazeRun contain examples for reusable object-oriented design. It uses several magic methods and properties to make it easy to access the data.
.. ifconfig:: size == "long"
.. include(’magic_methods.rst’)
ifconfig评估 Python 表达式size == "short"。如果计算结果为 True,将呈现段落中的额外文本。我们可以用以下代码来构建文档的长版本和短版本
sphinx-build -b html -D size=long . _build/long_html
sphinx-build -b html -D size=short . _build/short_html
为了并行使用多个配置,我们可以编辑Makefile来包含它们,或者创建配置文件conf.py的第二个副本,并在构建文档时使用-c选项在它们之间切换。
改变外观和感觉
为了设计您的文档样式或者给它一个公司的外观和感觉,我们可能想要更改 Sphinx 正在使用的模板。Sphinx 有几个内置主题。我们可以通过改变conf.py文件中的html theme变量来配置它们:
html_theme = 'classic'
可能的名字包括'alabaster', 'classic', 'sphinxdoc', 'sphinx rtd theme', 'scrolls', 'agogo', 'traditional', 'nature', 'haiku', 'pyramid',和'bizstyle'。
对于更详细或定制的更改,我们可以在模板目录中创建自己的模板。在内部,Sphinx 使用 Jinja 作为模板引擎。这意味着我们可以用自己的模板替换部分默认模板,或者只编辑 CSS 文件来改变网站的外观。更多信息见 www.sphinx-doc.org/en/stable/templating.html 。
如何写出好的文档?
写好文档不同于写小说或技术书籍。读者带着许多不同的意图接近他们。有些读者想了解我们节目的基本内容,有些人在寻找非常详细的答案,还有一些人只是想快速查找一些东西。由于这些意图,好的文档需要易于访问、准确且可操作。让每个人一直快乐是非常困难的,甚至可能是不可取的。但是有一些典型的文本部分在许多项目中经常出现。
技术文档中的文本部分
我们将考虑以下典型的文本部分作为记录软件的最佳实践。
摘要
好的文档从回答为什么开始。我们为什么要使用这个软件?我们当初为什么要建造它?它解决的唯一问题是什么?好的总结需要简短。
先决条件和安装
软件的一个必要方面是让它工作。我们的文档需要涵盖这一点,即使它只包含一个pip install命令。在其他情况下,您可能需要包括分步说明。提及可用的和/或经过测试的平台也是值得的。如果我们认为程序应该在一个特定的平台上工作,但是我们还没有测试它,这也是一个写在安装部分的好东西。
入门指南
本节描述软件的预期主要用途。在许多情况下,这可以被理解为一个“Hello world”的例子。根据我们的程序,这个例子可能包括 Python 代码,一步一步的食谱或两者都有。如果我们的程序不止做一件事,我们可以在这一部分包括一个小的功能浏览。
食谱
食谱是一套说明我们程序使用的食谱,包括代码示例。例子必须完整和准确,这也是 doctests 的荣耀所在。菜谱风格的文档适用于基本和高级功能。
个案研究
一个比食谱更实际的方法是记录案例研究,我们在实践中如何实际使用软件的例子。这将有助于用户更好地感受软件的可行性和不可行性。
技术参考资料
技术参考通常是部分的列举:输入格式的描述、参数表和程序中的函数列表(可能是自动生成的)。这部分是给想查资料的读者看的。Sphinx 的默认搜索功能派上了用场。
设计资料
最高级的读者(那些想要使用我们的源代码的人)会对程序设计的细节感兴趣。在这里,为什么又变得重要了。我们为什么要这样构建程序?我们的主要设计考虑是什么?我们能提供我们软件的可视化概述吗?我们是否应该记录我们的程序设计很大程度上取决于软件的种类。在复杂的服务器架构中,这可能是文档中最重要的部分。
法律方面
文档应该说明谁是作者,指向一个许可,并包含一个法律免责声明(可以在LICENSE文件中)。如果你希望被联系,不要忘记你的电子邮件地址。
良好文档的示例
有许多 Python 项目都有很好的文档。这份文件的共同点是,它包含指导性的标题和简短的章节,并避免复杂的词汇。如果特定领域语言的使用不可避免,它们至少会被一致地使用。
一些记录良好的 Python 项目的例子有:
- gizeh by
zulko(http://github.com/Zulko/gizeh):gizeh 是一个基于 cairo 构建的用于创建矢量图形的库。文档可以放在一个网页上,但是这个库非常强大。这是一个文档不需要很长的例子。 - 本书中使用的测试框架文档在记录基本特性和高级特性之间找到了恰当的平衡。在首页,有一个详细但结构清晰的目录。前两节介绍了基础知识,对于基本的使用已经足够了。其余部分(大约三分之二)描述了特殊的用例。通过关注具体的例子,作者避免解释每一个可能的特例。
- sci kit-learn
(http://scikit-learn.org):Python 中机器学习的头号库(Learning s ci kit-learn:Machine Learning in Python,Raúl Garreta 和 Guillermo Moncecchi,JMLR 12,第 2825–2830 页,Packt 出版社,2011 年。)必须平衡两个挑战:
首先,库本身是巨大的,其次,底层概念是复杂的。由此产生的文档非常庞大,其中有许多好东西可以找到。我想指出使用图像来说明不同的方法(如 http://scikit-learn.org/stable/auto_examples/classification/plot_classifier_comparison.html ).
Tip
您可以使用以下浏览器查看 Pygame 文档:
python -m pygame.docs
像编写软件一样,编写文档是一个迭代的过程。不要期望“一劳永逸地”清理文档理想的最佳实践是并行维护文档和代码。这并不意味着我们每次修改代码时都需要更新文档。目标是记录那些不经常改变的东西。对于经常变化的部分,我们的源代码中的注释或源代码本身是一个更好的地方。将 Sphinx 与 doctests 结合使用使我们的生活变得容易多了,因为我们可以检查文档的哪些部分是正确的。
其他文档工具
值得一提的是,还有许多其他工具可以帮助维护 Python 项目的文档。其中一些被认为是 Sphinx 的可能替代品,但大多数是对 Sphinx 提供的功能的补充:
MkDocs
与 Sphinx 相比,MkDocs 是一个轻量级的文档工具。它将 Markdown 格式的文档呈现为 HTML,并提供了许多模板。它具有预览功能;也就是说,我们可以在编写时看到渲染的文档。目前,MkDocs 没有像 Sphinx 那样直接包含 Python 文档字符串的任何功能。 www.mkdocs.org/见。
jupyter 笔记型电脑
Jupyter 笔记本提供了文本和可执行代码的独特组合。它们服务于记录工作的目的,特别是在快速发展的项目中。当我们为数据分析编写代码,并希望与结果、图表和描述我们想法的文本一起分享时,笔记本是一种被广泛接受的工具。http://jupyter.org/见。Jupyter 笔记本可以结合 reveal.js 将笔记本转换成幻灯片演示( http://lab.hakim.se/reveal-js/ ).
Gitbook
Gitbook 是一个以多种电子书格式(PDF、EPUB、MOBI 和 HTML)创建文档的程序。它也是一个门户网站,建立和托管由此产生的电子书。Gitbook 使用 Markdown 格式作为标记语言。对于教程和指南来说,Gitbook 是一个很好的选择,因为它与源代码的集成没有 Sphinx 那么紧密。在更新的版本中,Gitbook 支持多语言文档。 www.gitbook.io见。
阅读文件
read Docs 提供了用 Sphinx 和 MkDocs 构建的文档的免费托管。该服务允许使用 git web-hooks,这样一旦我们将更改推送到包含我们文档的 git 存储库,文档就会自动重建。 http://readthedocs.org/见。
pydoc
pydoc工具显示控制台中任何可导入模块的文档。例如,如果我们在带有maze run.py文件的目录中:
pydoc maze_run
pydoc还允许搜索文档、创建 HTML 文档以及启动本地 web 服务器浏览文档。pydoc默认随 Python 一起安装。详见pydoc --help。
表面抗原-5
由 Eric A. Meyer 建立的 S5 布局是用于幻灯片演示的 HTML5 模板的标准。它们允许以 Markdown 格式编写演示文稿,并将其编译成幻灯片。 http://meyerweb.com/eric/tools/s5/见。
色素细胞
pygments是 Sphinx 用来将 Python 代码呈现为语法突出显示的表示的 Python 库。pygments 可以为这个星球上的大多数编程语言创建 HTML 表示。如果我们想在自己的基于 Python 的网站上显示 Python 代码,pygments是首选工具。 http://pygments.org/见。
doctest(测试)
Python 标准库中的doctest模块可以独立于 Sphinx 使用。我们可以将文档测试直接写入我们的文档字符串,并使用
python -m doctest my program.py
http://docs.python.org/3/library/doctest.html见。
PyPDF2
Python 库PyPDF2允许用几行 Python 代码拆分和合并 PDF 文档。我发现它对 pdf 的后处理非常有用(例如,添加封面)。http://github.com/mstamy2/PyPDF2见。
潘多克
使用 pandoc,您可以快速地将各种标记格式相互转换,并生成 Word 或 PDF 文档。控制各种不同的文件是非常方便的。 http://pandoc.org/见。
最佳实践
- Sphinx 是一个为 Python 项目构建文档的工具。
- Sphinx 将文档构建为 HTML、PDF 或 EPUB 文档。
- 它使用 ReStructuredText 格式作为标记语言。
- 指令链接多个
.rst文档,包括图像,或者触发 Sphinx 的其他特殊功能。 - 文档中的 Python 代码通过 pygments 语法高亮显示。
- Python 函数和模块的文档可以从 docstrings 自动生成。
- 文档测试是写入文档的 shell 会话。它们可以由 Sphinx 执行,有效地验证您的文档是否正确。
- 使用配置变量可以打开和关闭文档的某些部分。
- 对于技术和非技术读者来说,好的文档包括为什么和如何使用一个软件。