在本章中,我们将介绍以下配方:
- 编写 python 脚本和模块文件
- 编写长代码行
- 包括说明和文件
- DocString 中更好的 RST 标记
- 设计复杂的 if…elif 链
- 设计终止的 while 语句
- 避免中断语句的潜在问题
- 利用异常匹配规则
- 使用 except:子句避免潜在问题
- 使用 raise from 语句链接异常
- 使用 with 语句管理上下文
Python 语法设计得非常简单。有一些规则;我们将通过一些有趣的语句来理解这些规则。仅仅看规则而没有具体的例子可能会令人困惑。
我们将首先介绍一些创建脚本文件的基础知识。然后我们将继续看一些更常用的语句。Python 语言中只有大约二十种不同类型的命令式语句。我们已经看过了第一章中的两种语句数字、字符串和元组:赋值语句和表达式语句。
当我们这样写的时候:
>>> print("hello world")
hello world
实际上,我们正在执行一条语句,该语句只包含函数print()
的求值。我们计算一个对象的函数或方法的这种语句是常见的。
我们已经看到的另一种语句是赋值语句。Python 在这个主题上有很多变体。大多数情况下,我们将单个值赋给单个变量。然而,有时我们可能同时分配两个变量,如下所示:
quotient, remainder = divmod(355, 113)
这些食谱将介绍一些更复杂的语句,包括if
、while
、for
、try
、with
和raise
。在探索不同的食谱时,我们将涉及其他一些食谱。
为了做任何真正有用的事情,我们需要编写 Python 脚本文件。我们可以在交互>>>
提示下进行语言实验。但是,对于实际工作,我们需要创建文件。编写软件的全部目的是为我们的数据创建可重复的处理。
我们如何避免语法错误并确保我们的代码与常用代码匹配?我们需要看看风格的一些常见方面——我们如何使用空白来澄清我们的编程。
我们还将研究一些更技术性的考虑因素。例如,我们需要确保以 UTF-8 编码保存文件。虽然 Python 仍然支持 ASCII 编码,但它对于现代编程来说是一个糟糕的选择。我们还需要确保使用空格而不是制表符。如果我们尽可能多地使用 Unix 换行符,我们还会发现事情会稍微简单一些。
大多数文本编辑工具都可以与 Unix(换行符)行结束符以及 Windows 或 DOS(返回换行符)行结束符一起正常工作。应避免使用不能同时使用两种线端点的任何工具。
要编辑 Python 脚本,我们需要一个好的编程文本编辑器。Python 附带了一个方便的编辑器 IDLE。它工作得很好。它可以让我们在文件和交互提示之间来回跳跃,但它不是一个很好的编程编辑器。
有很多优秀的编程编辑器。几乎不可能只建议一个。因此,我们将提出一些建议。
ActiveState 拥有非常复杂的 Komodo IDE。Komodo 编辑版是免费的,它的一些功能与完整的 Komodo IDE 相同。这在所有普通操作系统上运行;这是一个很好的首选,因为无论我们在哪里编写代码,它都是一致的。
参见http://komodoide.com/komodo-edit/ 。
记事本++对 Windows 开发人员很好。参见https://notepad-plus-plus.org 。
BBEdit 对于 Mac OS X 开发人员来说非常好。参见http://www.barebones.com/products/bbedit/ 。
对于 Linux 开发人员,有几个内置编辑器,包括 VIM、gedit 或 Kate。这些都很好。因为 Linux 倾向于偏向于开发人员,所以可用的编辑器都适合编写 Python。
重要的是,我们在工作时通常会打开两个窗口:
- 我们正在处理的脚本或文件。
- Python 的
>>>
提示符(可能来自 shell,也可能来自 IDLE),在这里我们可以尝试一些东西,看看哪些有效,哪些无效。我们可能正在用 Notepad++创建脚本,但使用 IDLE 来试验数据结构和算法。
我们这里实际上有两个食谱。首先,我们需要为编辑器设置一些默认值。然后,一旦编辑器设置正确,我们就可以为脚本文件创建一个通用模板。
首先,我们将查看需要在所选编辑器中执行的常规设置。我们将使用 Komodo 示例,但基本原则适用于所有编辑器。设置编辑首选项后,我们可以创建脚本文件。
- 打开所选的编辑器。查看首选项页面。
- 查找首选文件编码的设置。使用 Komodo 编辑首选项,它位于国际化选项卡上。将其设置为UTF-8。
- 查找缩进的设置。如果有办法使用空格而不是制表符,请选中此选项。使用 Komodo Edit,我们实际上是向后执行的,我们取消选中首选空格而不是制表符。
规则是这样的:我们需要空间;我们不想要标签。
另外,将每个缩进的空格设置为 4。这是典型的 Python 代码。它允许我们有几个缩进级别,并且仍然保持代码相当狭窄。
一旦我们确定我们的文件将以 UTF-8 编码保存,并且我们也确定我们使用的是空格而不是制表符,我们就可以创建一个示例脚本文件:
-
The first line of most Python script files should look like this:
#!/usr/bin/env python3
这将设置正在编写的文件与 Python 之间的关联。
对于 Windows,文件名与程序关联是通过一个 Windows 控制面板中的设置完成的。在默认程序控制面板中,有一个面板设置关联。此控制面板显示
.py
文件已绑定到 Python 程序。这通常是由安装程序设置的,我们很少需要对其进行更改或手动设置。Windows 开发人员无论如何都可以包含序言行。当 MacOSX 和 Linux 用户从 GitHub 下载该项目时,他们会很高兴。
-
After the preamble, there should be a triple-quoted block of text. This is the documentation string (called a docstring ) for the file we're going to create. It's not technically mandatory, but it's essential for explaining what a file contains.
''' A summary of this script. '''
因为 Python 的三重引号字符串可以无限长,所以可以根据需要自由编写。这应该是描述脚本或库模块的主要工具。这甚至可以包括它如何工作的例子。
-
Now comes the interesting part of the script: the part that really does something. We can write all the statements we need to get the job done. For now, we'll use this as a placeholder:
print('hello world')
有了这个,我们的脚本做了一些事情。在其他食谱中,我们将看到一些其他的关于做事的陈述。创建函数和类定义以及编写语句来使用函数和类来执行操作是很常见的。
在脚本的顶层,所有语句必须从左边距开始,并且必须在一行上完成。有些复杂语句中嵌套了语句块。这些内部语句块必须缩进。通常,因为我们将缩进设置为四个空格,所以可以点击选项卡键缩进。
我们的文件应该如下所示:
#!/usr/bin/env python3
'''
My First Script: Calculate an important value.
'''
print(355/113)
与其他语言不同,Python 中几乎没有样板文件。架空线路只有一条,甚至#!/usr/bin/env python3
线路一般都是可选的。
为什么我们要将编码设置为 UTF-8?整个语言设计为只使用原始的 128 个 ASCII 字符。
我们经常发现 ASCII 是有限的。将编辑器设置为使用 UTF-8 编码更容易。通过此设置,我们可以简单地使用任何有意义的字符。如果我们用 UTF-8 编码保存程序,我们可以使用像µ
这样的字符作为 Python 变量。
如果我们用 UTF-8 保存文件,这就是合法的 Python:
π=355/113
print(π)
在 Python 中选择空格和制表符时,保持一致是很重要的。它们或多或少都是看不见的,把它们混在一起很容易导致混淆。建议使用空格。
当我们将编辑器设置为使用四个空格缩进时,我们可以使用键盘上标记为 Tab 的按钮插入四个空格。我们的代码将正确对齐,缩进将显示我们的语句是如何相互嵌套的。
最初的#!
行是注释:#
和行尾之间的所有内容都被忽略。操作系统外壳程序,如bash和ksh查看文件的第一行,查看文件包含的内容。前几个字节有时被称为魔法,因为外壳正在偷看它们。Shell 程序查找两个字符的序列#!
,以识别负责此数据的程序。我们更愿意使用/usr/bin/env
为我们启动 Python 程序。我们可以利用它通过env
程序进行特定于 Python 的环境设置。
Python 标准库文档部分来自模块文件中的文档字符串。在模块中编写复杂的 docstring 是常见的做法。有像 Pydoc 和 Sphinx 这样的工具可以将模块 docstring 重新格式化为优雅的文档。我们将在单独的食谱中介绍这一点。
此外,单元测试用例可以包含在 docstring 中。像doctest这样的工具可以从文档字符串中提取示例并执行代码,以查看文档中的答案是否与运行代码找到的答案匹配。这本书的大部分内容都经过 doctest 验证。
三重引用的文档字符串优先于#
注释。#
和行尾之间的文本将被忽略,并计为注释。由于这仅限于一条线路,因此使用较少。文档字符串的大小可以不确定;它们被广泛使用。
在 Python 3.5 中,我们有时会在脚本文件中看到这种情况:
color = 355/113 # type: float
# type: float
注释可由类型推断系统使用,以确定在实际执行程序时可能出现各种数据类型。有关这方面的更多信息,请参见Python 增强方案 484:https://www.python.org/dev/peps/pep-0484/ 。
还有一点开销有时包含在文件中。VIM 编辑器允许我们在文件中保留编辑首选项。这被称为模型线。我们通常必须通过在~/.vimrc
文件中包含set modeline
设置来启用 modelines。
一旦我们启用了 modelines,我们就可以在文件末尾添加一个特殊的# vim
注释来配置 VIM。
下面是一个对 Python 有用的典型模型线:
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
这将 Unicodeu+0009
制表符设置为八个空格。当我们点击制表符键时,我们将移动四个空格。该设置在文件中进行;我们不必进行任何 VIM 设置,就可以将这些设置应用到 Python 脚本文件中。
- 我们将了解如何在中编写有用的文档字符串,包括说明和文档以及在配方中编写更好的 RST 标记
- 有关建议样式的更多信息,请参见https://www.python.org/dev/peps/pep-0008/
很多时候,我们需要编写太长以至于很难阅读的代码行。许多人喜欢将一行代码的长度限制在 80 个字符或更少。众所周知的平面设计原则是,线条越窄越容易阅读;观点各异,但 65 个字符通常被认为是理想的。参见http://webtypography.net/2.1.2 。
虽然较短的行更容易让人看到,但我们的代码可能拒绝配合这一原则。长语句是一个常见的问题。我们如何将长 Python 语句分解成更易于管理的部分?
通常,我们会有一份冗长而难以处理的声明。假设我们有这样的东西:
>>> import math
>>> example_value = (63/25) * (17+15*math.sqrt(5)) / (7+15*math.sqrt(5))
>>> mantissa_fraction, exponent = math.frexp(example_value)
>>> mantissa_whole = int(mantissa_fraction*2**53)
>>> message_text = 'the internal representation is {mantissa:d}/2**53*2**{exponent:d}'.format(mantissa=mantissa_whole, exponent=exponent)
>>> print(message_text)
the internal representation is 7074237752514592/2**53*2**2
这段代码包括一个长公式和一个长格式字符串,我们将向其中注入值。这在书上排版时看起来很糟糕。当试图编辑此脚本时,它在屏幕上看起来很糟糕。
我们不能简单地将 Python 语句分成块。语法规则很清楚,语句必须在一个逻辑行上完成。
术语“逻辑线”是一个关于我们如何进行的提示。Python 区分了逻辑线和物理线;我们将利用这些语法规则来分解长语句。
Python 为我们提供了几种包装长语句的方法,因此它们更具可读性。
- 我们可以使用一行末尾的
\
继续下一行。 - 我们可以利用 Python 的规则,即语句可以跨越多个逻辑行,因为
()
、[]
和{}
字符必须平衡。除了使用()
和\
之外,我们还可以利用 Python 自动连接相邻字符串文本的方式来生成单个更长的文本;("a" "b")
与ab
相同。 - 在某些情况下,我们可以通过将中间结果分配给单独的变量来分解语句。
我们将在本食谱的单独部分中逐一介绍。
以下是此技术的上下文:
>>> import math
>>> example_value = (63/25) * (17+15*math.sqrt(5)) / (7+15*math.sqrt(5))
>>> mantissa_fraction, exponent = math.frexp(example_value)
>>> mantissa_whole = int(mantissa_fraction*2**53)
Python 允许我们使用\
并打破界限。
-
将整个陈述写在一行长线上,即使它令人困惑:
>>> message_text = 'the internal representation is {mantissa:d}/2**53*2**{exponent:d}'.format(mantissa=mantissa_whole, exponent=exponent)
-
如果有逻辑中断,请在此处插入
\
。有时候,没有真正好的休息:>>> message_text = 'the internal representation is \ ... {mantissa:d}/2**53*2**{exponent:d}'.\ ... format(mantissa=mantissa_whole, exponent=exponent) >>> message_text 'the internal representation is 7074237752514592/2**53*2**2'
要使其工作,\
必须是行中的最后一个字符。我们甚至不能在\
之后有一个空格。这是很难看到的,;出于这个原因,我们不鼓励这样做。
尽管这有点难看,\
始终可以使用。可以将其视为使一行代码更具可读性的最后手段。
-
将整个陈述写在一行,即使它令人困惑:
>>> import math >>> example_value1 = (63/25) * (17+15*math.sqrt(5)) / (7+15*math.sqrt(5))
-
添加额外的
()
字符,这些字符不改变值,但允许将表达式拆分为多行:>>> example_value2 = (63/25) * ( (17+15*math.sqrt(5)) / (7+15*math.sqrt(5)) ) >>> example_value2 == example_value1 True
-
在
()
字符内换行:>>> example_value3 = (63/25) * ( ... (17+15*math.sqrt(5)) ... / ( 7+15*math.sqrt(5)) ... ) >>> example_value3 == example_value1 True
匹配()
角色的技巧非常强大,适用于多种情况。这是广泛使用和强烈推荐的。
我们几乎总能找到在语句中添加额外字符的方法。在少数情况下,当我们不能添加()
字符,或者添加()
字符并不能改善情况时,我们可以使用\
将语句拆分为多个部分。
我们可以将()
字符与另一个组合字符串文字的规则组合在一起。这对于长而复杂的格式字符串特别有效:
-
在
()
字符中换行一个长字符串值。 -
将字符串拆分为子字符串:
>>> message_text = ( ... 'the internal representation ' ... 'is {mantissa:d}/2**53*2**{exponent:d}' ... ).format( ... mantissa=mantissa_whole, exponent=exponent) >>> message_text 'the internal representation is 7074237752514592/2**53*2**2'
我们总是可以把一根长绳子分成相邻的几段。一般来说,当作品被()
字符包围时,这是最有效的。然后,我们可以根据需要使用尽可能多的物理换行符。这仅限于那些字符串值特别长的情况。
以下是此技术的上下文:
>>> import math
>>> example_value = (63/25) * (17+15*math.sqrt(5)) / (7+15*math.sqrt(5))
我们可以将其分解为三个中间值。
-
Identify sub-expressions in the overall expression. Assign these to variables:
>>> a = (63/25) >>> b = (17+15*math.sqrt(5)) >>> c = (7+15*math.sqrt(5))
这通常相当简单。可能需要稍微小心地进行代数运算,以找到合理的子表达式。
-
用创建的变量替换子表达式:
>>> example_value = a * b / c
这是用变量对原始复杂子表达式的基本文本替换。
我们没有给出这些变量的描述性名称。在某些情况下,子表达式具有一些语义,我们可以使用有意义的名称来捕获这些语义。在本例中,我们对表达式的理解不够透彻,无法提供意义深远的名称。相反,我们选择了简短、任意的标识符。
Python 语言手册对逻辑行和物理行进行了区分。逻辑行包含完整的语句。它可以通过称为线路连接的技术跨越多条物理线路。本手册将技术称为显式连接和隐式连接。
使用\
进行显式直线连接有时会有所帮助。因为它很容易被忽视,所以一般不鼓励这样做。这是万不得已的办法。
在许多情况下,可以使用()
进行隐式直线连接。它通常在语义上符合表达式的结构,所以它是被鼓励的。我们可以将()
字符作为所需的语法。例如,我们已经有了()
字符作为print()
函数语法的一部分。我们可以这样做来打破一个冗长的声明:
>>> print(
... 'several values including',
... 'mantissa =', mantissa,
... 'exponent =', exponent
... )
表达式在许多 Python 语句中被广泛使用。任何表达式都可以添加()
字符。这给了我们很大的灵活性。
然而,在一些地方,我们可能会有一个很长的语句,它并不具体涉及一个表达式。最显著的例子是import
语句,它可以变长,但不使用任何可以插入括号的表达式。
然而,语言设计者允许我们使用()
字符,以便将一长串名称分成多个逻辑行:
>>> from math import (sin, cos, tan,
... sqrt, log, frexp)
在这种情况下,()
字符显然不是表达式的一部分。()
字符只是额外的语法,用于使语句与其他语句一致。
- 隐式换行也适用于匹配的
[]
字符和{}
字符。这些适用于收集数据结构,我们将在第 4 章、内置数据结构中查看这些数据结构–列表、集合、dict。
当我们有一个有用的脚本时,我们经常需要为自己和他人留下关于它的作用、它如何解决某些特定问题以及何时应该使用它的笔记。
因为清晰性很重要,所以有一些格式化方法可以帮助使文档非常清晰。该配方还包含一个建议的大纲,以便文件合理完整。
如果我们使用编写 python 脚本和模块文件-语法基础配方来构建脚本文件,我们将在脚本文件中放入一个小文档字符串。我们将展开此文档字符串。
还有一些地方应该使用文档字符串。我们将在第 3 章、函数定义和第 6 章、类和对象基础中查看这些附加位置。
我们将为两种一般类型的模块编写摘要文档字符串:
- 库模块:这些文件将主要包含函数定义和类定义。在这种情况下,docstring 摘要可以关注模块的功能,而不是它的功能。docstring 可以提供使用模块中定义的函数和类的示例。在第 3 章、函数定义和第 6 章、类和对象基础中,我们将更仔细地了解函数或类包的这一概念。
- 脚本:我们通常希望这些文件能够完成一些实际工作。在这种情况下,我们希望专注于做而不是做。docstring 应该描述它的功能以及如何使用它。选项、环境变量和配置文件是此文档字符串的重要部分。
我们有时会创建包含这两者的文件。这需要一些仔细的编辑,以在做与做之间取得适当的平衡。在大多数情况下,我们只提供这两种文档。
对于库模块和脚本,编写文档的第一步是相同的:
- 写一个脚本或模块的简要摘要。摘要没有深入探讨它是如何工作的。就像报纸文章中的lede一样,它介绍了模块的人员、内容、时间、地点、方式和原因。详细信息将在 docstring 的正文中显示。
sphinx 和 pydoc 等工具显示信息的方式暗示了特定的样式提示。在这些工具的输出中,上下文非常清楚,因此在摘要句中省略主语是很常见的。这个句子常以动词开头。
例如,类似这样的摘要:此脚本下载并解码 AKQ区域的当前特殊海上警告(SMW)有一个不必要的此脚本。我们可以放下它,从动词短语开始下载并解码。。。。
我们可以这样启动模块 docstring:
'''
Downloads and decodes the current Special Marine Warning (SMW)
for the area 'AKQ'.
'''
我们将根据模块的总体重点来分隔其他步骤。
当我们记录脚本时,我们需要关注将使用脚本的人的需求。
-
如前所示,开始创建摘要句子。
-
Sketch an outline for the rest of the docstring. We'll be using ReStructuredText ( RST ) markup. Write the topic on one line, then put a line of
=
under the topic to make them a proper section title. Remember to leave a blank line between each topic.主题可能包括:
- 简介:如何运行此脚本的摘要。如果脚本使用
argparse
模块处理命令行参数,argparse
生成的帮助文本是理想的摘要文本。 - 说明:对该脚本的功能进行更完整的说明。
- 选项:如果使用
argparse
,这是放置每个参数细节的地方。我们通常会重复argparse
帮助参数。 - 环境:如果使用
os.environ
,这里是描述环境变量及其含义的地方。 - 文件:脚本创建或读取的文件名是非常重要的信息。
- 示例:一些使用脚本的示例总是很有帮助的。
- 另见:任何相关脚本或背景信息。
其他可能有趣的话题包括退出状态、作者、漏洞、报告漏洞、历史或版权。例如,在某些情况下,关于报告 bug 的建议实际上不属于模块的 docstring,而是属于项目的 GitHub 或 SourceForge 页面的其他地方。
- 简介:如何运行此脚本的摘要。如果脚本使用
-
填写每个主题下的详细信息。准确是很重要的。由于我们将此文档嵌入到与代码相同的文件中,因此很容易检查模块中的其他地方,以确保内容正确且完整。
-
对于代码示例,我们可以使用一些很酷的 RST 标记。回想一下,所有元素都由空行分隔。在一段中,单独使用
::
。在下一段中,提供用四个空格缩进的代码示例。
下面是脚本的 docstring 示例:
'''
Downloads and decodes the current Special Marine Warning (SMW)
for the area 'AKQ'
SYNOPSIS
========
::
python3 akq_weather.py
DESCRIPTION
===========
Downloads the Special Marine Warnings
Files
=====
Writes a file, ``AKW.html``.
EXAMPLES
========
Here's an example::
slott$ python3 akq_weather.py
<h3>There are no products active at this time.</h3>
'''
在大纲部分,我们使用::
作为单独的段落。在示例部分,我们在一段末尾使用了::
。这两个版本都是对 RST 处理工具的提示,提示后面的缩进部分应作为代码排版。
当我们记录一个库模块时,我们需要关注程序员的需求,他们将导入模块以在代码中使用它。
- 为文档字符串的其余部分绘制轮廓。我们将使用 RST 标记。将主题写在一行上。在每个主题下包括一行
=
,以使主题成为适当的标题。记住在每个段落之间留一个空行。 - 如前所示,开始创建摘要句子。
- 说明:总结模块包含的内容以及模块为什么有用。
- 模块内容:本模块定义的类和函数。
- 示例:模块使用示例。
- 填写每个主题的详细信息。模块内容可能是一长串的类或函数定义。这应该是一个总结。在每个类或函数中,我们将有一个单独的 docstring,其中包含该项的详细信息。
- 有关代码示例,请参见前面的示例。使用
::
作为段落或段落结尾。将代码示例缩进四个空格。
几十年来,手册页大纲已经演变为包含 Linux 命令的有用摘要。这种编写文档的通用方法已被证明是有用的和有弹性的。我们可以利用大量的经验,按照手册页模型构建文档。
这两种描述软件的方法是基于许多单独页面的文档摘要。目标是利用众所周知的一组主题。这使我们的模块文档反映了常见做法。
我们希望准备 Sphinx Python 文档生成器可以使用的模块 docstring(请参见http://www.sphinx-doc.org/en/stable/ 。这是用于生成 Python 文档文件的工具。Sphinx 中的autodoc
扩展将读取模块、类和函数的 docstring 头,以生成与 Python 生态系统中其他模块类似的最终文档。
RST 有一个简单的语法规则,即段落由空行分隔。
这条规则使得编写文档变得非常容易,这些文档可以被各种 RST 处理工具检查并重新格式化,使其看起来非常漂亮。
当我们想要包含一段代码时,我们会有一些特殊的段落:
-
用空行将代码与文本分开。
-
将代码缩进四个空格。
-
提供前缀
::
。我们可以将其作为单独的段落,或者作为引入段落末尾的特殊双冒号:Here's an example:: more_code()
-
::
用于引入段。
在软件开发中有新奇和艺术的地方。文档并不是一个真正可以推动信封的地方。聪明的算法和复杂的数据结构可以是新颖和聪明的。
对于只想使用该软件的用户来说,独特的声音或古怪的演示文稿并不有趣。调试时,有趣的样式没有帮助。文档应该是普通的和常规的。
编写好的软件文档可能很有挑战性。太少的信息和简单地概括代码的文档之间存在着巨大的鸿沟。在某个地方,有一个很好的平衡。重要的是关注那些不太了解软件或其工作原理的人的需求。向半知识型用户提供描述软件功能和使用方法所需的信息。
在许多情况下,我们需要解决用例的两个部分:
- 软件的预期用途
- 如何自定义或扩展软件
这可能是两个不同的受众。可能有不同于开发人员的用户。每个都有不同的视角,文档的不同部分需要尊重这两个视角。
- 我们在中研究了在 DocString中编写更好的 RST 标记的其他技术。
- 如果我们使用了编写 python 脚本和模块文件–语法基础的方法,我们将在脚本文件中放入一个文档字符串。当我们在第 3 章、函数定义中构建函数时,以及在第 6 章、类和对象基础中构建类时,我们将看看其他可以放置文档字符串的地方。
- 参见http://www.sphinx-doc.org/en/stable/ 了解斯芬克斯的更多信息。
- 有关手册页大纲的更多背景信息,请参见https://en.wikipedia.org/wiki/Man_page 。
当我们有一个有用的脚本时,我们通常需要留下关于它的功能、工作方式以及应该使用的时间的注释。许多用于生成文档的工具(包括 Docutils)都使用 RST 标记。我们可以使用哪些 RST 功能使文档更具可读性?
在包括说明和文档的配方中,我们考虑将一组基本文档放入一个模块中。这是编写文档的起点。有大量 RST 格式规则。我们将介绍一些对创建可读文档很重要的方法。
-
Be sure to write an outline of the key points. This may lead to creating RST section titles to organize the material. A section title is a two-line paragraph with the title followed by an underline using
=
,-
,^
,~
, or one of the other Docutils characters for underlining.标题将如下所示。
Topic =====
标题文本在一行,下划线字符在下一行。这必须用空行包围。下划线字符可以多于标题字符,但不能少于标题字符。
RST 工具将推断我们使用下划线字符的模式。只要一致使用下划线字符,将下划线字符与所需标题匹配的算法将检测模式。实现这一点的关键是一致性以及对章节和小节的清晰理解。
开始时,它可以帮助制作一个明确的提醒便签,如下所示:
| **字符** | **级别** | | = | 1. | | - | 2. | | ^ | 3. | | ~ | 4. | -
Fill in the various paragraphs. Separate paragraphs (including the section titles) by blank lines. Extra blank lines don't hurt. Omitting blank lines will lead the RST parsers to see a single, long paragraph, which may not be what we intended.
我们可以将内联标记用于强调、强强调、代码、超链接和内联数学等。如果我们计划使用 Sphinx,那么我们可以使用更大的文本角色集合。我们将很快研究这些技术。
-
如果编程编辑器有拼写检查器,请使用它。这可能令人沮丧,因为我们经常会有代码示例,其中可能包含拼写检查失败的缩写。
文档转换程序将检查文档,查找节和正文元素。章节由标题标识。下划线用于将各部分组织成正确嵌套的层次结构。推导该值的算法相对简单,并具有以下规则:
- 如果以前见过下划线字符,则级别已知
- 如果以前未看到下划线字符,则必须将其缩进上一个大纲级别下一个级别
- 如果没有上一级,则这是第一级
正确嵌套的文档可能具有以下下划线字符序列:
====
-----
^^^^^^
^^^^^^
-----
^^^^^^
~~~~~~~~
^^^^^^
我们可以看到第一个轮廓字符=
将是一级。下一个-
未知,但出现在一级之后,因此它必须是二级。第三个标题为^
,之前未知,必须为三级。下一个^
仍然是三级。接下来的两个-
和^
分别是二级和三级。
当我们遇到新角色~
时,它位于三级标题之下,因此必须是四级标题。
从这个概述中,我们可以看到不一致会导致混乱。
如果我们在文档中改变了主意,这个算法就检测不到。如果出于莫名其妙的原因,我们决定跳过一个关卡,并尝试在第二关卡中设置一个第四关卡的标题,那根本无法做到。
RST 解析器可以识别几种不同类型的主体元素。我们已经展示了一些。更完整的列表包括:
- 文本的段落:这些段落可能使用内联标记进行不同类型的强调或突出显示。
- 文字块:这些文字块由
::
引入,缩进表示空格。它们也可以通过.. parsed-literal::
指令引入。doctest 块缩进四个空格,并包含 Python>>>
提示。 - 列表、表格和块引号:我们稍后再看。这些可以包含其他主体元素。
- 脚注:这些是特殊段落,可以放在页面底部或章节末尾。这些还可以包含其他主体元素。
- 超链接目标、替换定义和 RST 注释:这些是专门的文本项。
为了完整起见,我们将在这里注意到 RST 段落由空行分隔。RST 比这个核心规则有更多的内容。
在包括说明和文档配方中,我们查看了几种可能使用的身体元素:
-
文本段落:这是一块被空行包围的文本。在这些代码中,我们可以使用内联标记来强调单词,或者使用字体来表示我们引用的是代码的元素。我们将在使用内联标记配方中查看内联标记。
-
Lists : These are paragraphs that begin with something that looks like a number or a bullet. For bullets, use a simple
-
or*
. Other characters can be used, but these are common. We might have paragraphs like this.有子弹是有帮助的,因为:
- 他们可以帮助澄清
- 他们可以帮助组织
-
Numbered Lists : There are a variety of patterns that are recognized. We might use something like this.
四种常见的编号段落:
- 数字后跟标点符号,如
.
或)
。 - 后跟标点符号的字母,如
.
或)
。 - 后跟标点符号的罗马数字。
#
的特例,标点符号与前面的项目相同。这延续了前几段的编号。
- 数字后跟标点符号,如
-
文字块:代码样本必须按文字呈现。此文本必须缩进。我们还需要在代码前面加上前缀
::
。::
字符必须是单独的段落,或者是代码示例的引入部分的结尾。 -
Directives : A directive is a paragraph that generally looks like
.. directive::
. It may have some content that's indented so that it's contained within the directive. It might look like this:.. important:: Do not flip the bozo bit.
.. important::
段是指令。随后是指令中缩进的一小段文本。在本例中,它创建了一个单独的段落,其中包括对重要的警告。
Docutils 有许多内置指令。Sphinx 添加了大量具有各种功能的指令。
一些最常用的指令是警告指令:注意、小心、危险、错误、提示、重要、注意、提示、警告和一般警告。这些是复合体元素,因为它们可以包含多个段落和嵌套指令。
我们可能会有这样的事情来提供适当的强调:
.. note:: Note Title
We need to indent the content of an admonition.
This will set the text off from other material.
其他常见指令之一是parsed-literal
指令。
.. parsed-literal::
any text
*almost* any format
the text is preserved
but **inline** markup can be used.
这对于提供突出显示部分代码的代码示例非常方便。像这样的文本是一个简单的 body 元素,里面只能有文本。它不能有列表或其他嵌套结构。
在一个段落中,我们可以使用几种内联标记技术:
- 我们可以将一个单词或短语用
*
括起来表示*emphasis*
。 - 我们可以将一个单词或短语用
**
括起来表示**strong**
。 - 我们用单回勾(```py 号)环绕引用。链接后面跟着一个
_
。我们可以使用section title`_`来引用文档中的特定部分。我们通常不需要在 URL 周围放置任何内容。Docutils 工具可以识别这些。有时我们希望显示一个单词或短语,并隐藏 URL。我们可以使用这个:
the Sphinx documentation http://www.sphinx-doc.org/en/stable/`_`。 - 我们可以用双回勾(````)环绕代码相关的单词,使它们看起来像
pycode
。
还有一种更通用的技术,称为文本角色。角色看起来比简单地用*
字符包装一个单词或短语要复杂一些。我们使用:word:
作为角色名称,后跟单个```py 回勾中的适用单词或短语。文本角色类似于:strong:
this``。
有许多标准角色名称,包括:emphasis:
、:literal:
、:code:
、:math:
、:pep-reference:
、:rfc-reference:
、:strong:
、:subscript:
、:superscript:
和:title-reference:
。其中一些还可以使用更简单的标记,如*emphasis*
或**strong**
。其余部分只能作为显式角色使用。
此外,我们还可以用一个简单的指令定义新角色。如果我们想进行非常复杂的处理,我们可以为 docutils 提供处理角色的类定义,从而允许我们调整文档的处理方式。Sphinx 添加了大量角色,以支持函数、方法、异常、类和模块之间的详细交叉引用。
- 有关 RST 语法的更多信息,请参见http://docutils.sourceforge.net 。这包括对 docutils 工具的描述。
- 有关Sphinx Python 文档生成器的信息,请参阅http://www.sphinx-doc.org/en/stable/ 。
- Sphinx 工具在基本定义中添加了许多附加指令和文本角色。
在大多数情况下,我们的脚本将涉及许多选择。有时选择很简单,我们可以通过浏览代码来判断设计的质量。在其他情况下,选择更加复杂,并且不容易确定我们的 if 语句是否正确设计来处理所有条件。
在最简单的情况下,我们有一个条件,C,其逆条件,C。这是if...else
语句的两个条件。一个条件,C在if
条款中说明,另一个在else
条款中暗示。
我们将使用p∨ q在本解释中表示 Python 的或运算符。我们可以将这两个条件称为完成,因为:
C∨ C=、T
我们称之为完全,因为不存在其他条件。没有第三选择。这就是被排除在外的中间阶层的法则。这也是else
条款背后的操作原则。执行if
语句体或else
语句。没有第三选择。
在实际编程中,我们经常有复杂的选择。我们可能有一组条件,C={C1、C2、C3、Cn}。
我们不想简单地假设:
*C1*∨ *C2*∨ *C3*∨ *。。。*∨ Cn=T
我们可以使用来表示与any(C)
或any([C_1, C_2, C_3, ..., C_n])
类似的意思。我们需要证明;我们不能假设这是true
。
这里是可能出错的地方,我们可能会错过一些条件,Cn+1,这些条件在混乱的逻辑中迷失了方向。如果忽略这一点,则意味着我们的程序将无法在这种情况下正常工作。
我们怎么能确定我们没有错过什么?
让我们看一个if...elif
链的具体示例。在骰子的赌场游戏中,有许多规则适用于一卷两个骰子。这些规则适用于游戏的第一卷,称为出局卷:
- 2、3 或 12 是骰子,这是所有在通过线上下注的损失
- 7 或 11 是所有在通过线上下注的赢家
- 剩下的数字建立了一个点
许多玩家在传球线上下注。还有一个不通过行,这是不太常用的。我们将使用这组三个条件作为查看此配方的示例,因为其中有一个潜在的模糊子句。
当我们写一个if
语句时,即使它看起来很琐碎,我们也需要确保涵盖了所有条件。
- 列举我们知道的备选方案。在我们的示例中,我们有三条规则:(2,3,12),(7,11)和模糊的剩余数。
- 确定宇宙中所有可能的条件。对于本例,有 10 个条件:数字从 2 到 12。
- 将已知的备选方案与宇宙进行比较。这组条件C和所有可能条件U之间的比较有三种可能结果:
已知的替代方案比宇宙有更多的条件;C⊃ U。这是一个巨大的设计问题。这需要从基础上重新思考设计。
在已知条件和可能条件之间存在差距;U\C≠ ∅. 在某些情况下,我们显然没有涵盖所有可能的条件。在其他情况下,这需要一些仔细的推理。我们需要用更精确的术语替换任何含糊或定义不清的术语。
在这个例子中,我们有一个模糊的术语,可以用更具体的术语来代替。术语剩余数字似乎是值列表(4、5、6、8、9、10)。提供此列表可以消除任何可能的差距和疑问。
已知的备选方案与可能的备选方案相匹配;U≡ C。有两种常见情况:
- 我们有像C这样简单的东西∨ C。我们可以使用一个
if
和else
子句,我们不需要使用这个配方,因为我们可以很容易地推导出C。 - 我们可能有更复杂的事情。既然我们知道整个宇宙,我们就可以证明。我们需要使用这个方法来编写一系列的
if
和elif
语句,每个条件一个子句。
区别并不总是清晰的。在我们的示例中,我们没有对其中一个条件的详细说明,但条件基本上是明确的。如果我们认为缺失的条件是明显的,我们可以使用else
子句,而不是显式地写出来。如果我们认为缺失的条件可能被误解,我们应该将其视为模糊的,并使用此配方。
-
写出涵盖所有已知条件的
if...elif...elif
链。对于我们的示例,它将如下所示:dice = die_1 + die_2 if dice in (2, 3, 12): game.craps() elif dice in (7, 11): game.winner() elif dice in (4, 5, 6, 8, 9, 10): game.point(die) ```py
-
添加一个引发异常的
else
子句,如:else: raise Exception('Design Problem Here: not all conditions accounted for') ```py
这个额外的else
碰撞条件为我们提供了一种在发现逻辑问题时积极识别的方法。我们可以肯定,我们犯的任何错误都会导致一个突出的问题。
我们的目标是确保我们的计划始终有效。虽然测试有帮助,但在设计和测试用例中我们仍然可能有错误的假设。
虽然严格的逻辑是必要的,但我们仍然会犯错误。此外,其他人可能会尝试调整我们的代码并引入错误。更令人尴尬的是,我们可能会对自己的代码进行修改,从而导致代码被破坏。
else
崩溃选项迫使我们对每个条件都要明确。没有任何假设。如前所述,当引发异常时,逻辑中的任何错误都将被发现。
else
崩溃选项对性能没有显著影响。简单的else
子句比带条件的elif
子句稍微快一点。如果我们认为我们的应用程序性能在某种程度上取决于单个表达式的成本,那么我们还有更严重的设计问题需要解决。计算单个表达式的成本很少是算法中成本最高的部分。
在存在设计问题的情况下,异常崩溃是一种明智的行为。遵循将警告消息写入日志的设计模式没有多大意义。如果我们有这样的逻辑缺口,程序就会被致命地破坏,一旦发现并修复它就很重要。
在许多情况下,我们可以从程序处理过程中的某个点对所需 post 条件的检查中得出一个if...elif...elif
链。例如,我们可能需要一个语句来建立一些简单的东西,比如m是a或b中较大的一个。
(为了通过逻辑工作,我们将避免m = max(a, b)
我们可以这样形式化最终条件:
(m=a∨ *m=b)*∧ m>a∧ m>b
我们可以通过将目标写为 assert 语句,从这个最终条件向后工作:
# do something
assert (m = a or m = b) and m > a and m > b
```py
一旦我们确定了目标,我们就可以确定实现该目标的陈述。显然,像`m = a`和`m = b`这样的赋值语句是合适的,但只能在某些条件下使用。
这些语句中的每一条都是解决方案的一部分,我们可以导出一个先决条件,显示何时应该使用该语句。每个赋值语句的先决条件是`if`和`elif`表达式。我们需要在`a >= b`时使用`m = a`;我们需要在`b >= a`时使用`m=b`。将逻辑重新排列为代码可以提供以下信息:
if a >= b:
m = a
elif b >= a:
m = b
else: raise Exception( 'Design Problem')
assert (m = a or m = b) and m > a and m > b
请注意,我们的条件宇宙,*U*={*a≥ b、 b≥ a*},已完成;没有其他可能的关系。还要注意的是,在*a=b*的边缘情况下,我们实际上并不关心使用哪个赋值语句。Python 将按顺序处理决策,并执行`m = a`。这一选择的一致性不会对我们的`if...elif...elif`链条设计产生任何影响。我们应始终编写条件,而不考虑条款的评估顺序。
## 另见
* 这类似于**悬挂 else**的句法问题。参见[https://en.wikipedia.org/wiki/Dangling_else](https://en.wikipedia.org/wiki/Dangling_else) 。
* Python 的缩进消除了悬而未决的 else 语法问题。它并没有消除试图确保在一个复杂的`if...elif...elif`链中正确考虑所有条件的语义问题。
* 另请参见[https://en.wikipedia.org/wiki/Predicate_transformer_semantics](https://en.wikipedia.org/wiki/Predicate_transformer_semantics) 。
# 设计一个正确终止的 while 语句
很多时候,Python`for`语句提供了我们需要的所有迭代控件。在许多情况下,我们可以使用内置函数,如`map()`、`filter()`和`reduce()`来处理数据收集。
然而,在一些情况下,我们需要使用`while`语句。其中一些情况涉及到数据结构,我们无法创建适当的迭代器来逐步遍历这些项。其他项目涉及到与人类用户的交互,在从用户那里得到输入之前,我们没有数据。
## 准备好了吗
假设我们将提示用户输入密码。我们将使用`getpass`模块,这样就不会有回声。
此外,为了确保他们正确输入,我们将提示他们两次并比较结果。在这种情况下,一个简单的`for`陈述是行不通的。它可以被压入服务中,但生成的代码看起来很奇怪:`for`语句有一个明确的上限;提示用户输入实际上没有上限。
## 怎么做。。。
我们将看一个六步过程,它概述了设计这种迭代算法的核心。当一个简单的`for`语句不能解决我们的问题时,我们需要做这样的事情。
1. Define done. In our case, we'll have two copies of the password, `password_text` and `confirming_password_text` . The condition which must be `true` after the loop is that `password_text == confirming_password_text` . Ideally, reading from people (or files) is a bounded activity. Eventually, people will enter the matching pair of values. Until they enter the matching pair, we'll iterate indefinitely.
还有其他边界条件。例如,文件结尾。或者我们允许此人返回到以前的提示。通常,我们在 Python 中处理这些其他条件,但有例外。
当然,我们总是可以在 done 的定义中添加这些附加条件。我们可能需要一个复杂的终止条件,如文件结束或`password_text == confirming_password_text`。
在本例中,我们将选择异常处理,并假设将使用一个`try:`块。它大大简化了设计,在终止条件中只有一个子句。
我们可以这样粗略地画出循环:
```
# initialize something
while # not terminated:
# do something
assert password_text == confirming_password_text
```py
我们已经将 done 的定义写成了一个最终的`assert`语句。我们已经为迭代的其余部分添加了注释,我们将在后续步骤中填充这些注释。
2. Define a condition that's `true` while the loop is iterating. This is called an **invariant** because it's always `true` at the start and end of loop processing. It's often created by generalizing the post-condition or introducing another variable.
当从人(或文件)中读取时,我们有一个隐含的状态更改,这是不变量的一个重要部分。我们可以称之为*获取下一个输入*状态变化。我们通常必须清楚地表明,我们的循环将从输入流中获取下一个值。
我们必须确保我们的循环正确地获取下一项,不管`while`语句体中有任何复杂的逻辑。这是一个常见的错误,有一个条件,其中一个下一个输入是没有实际获取。这导致程序*挂起*——通过`while`语句体中的`if`语句,在一条逻辑路径中没有状态变化。不变量没有正确重置,或者在设计循环时没有正确连接。
在我们的例子中,不变量将使用概念上的`new-input()`条件。当我们使用`getpass()`函数读取新值时,此条件为`true`。以下是我们的扩展回路设计:
```
# initialize something
# assert the invariant new-input(password_text)
# and new-input(confirming_password_text)
while # not terminated:
# do something
# assert the invariant new-input(password_text)
# and new-input(confirming_password_text)
assert password_text == confirming_password_text
```py
3. Define the condition for leaving the loop. We need to be sure that this condition depends on the invariant being `true` . We also need to be sure that, when this termination condition is finally `false,` the target state will become `true` .
在大多数情况下,循环条件是目标状态的逻辑否定。以下是扩展设计:
```
# initialize something
# assert the invariant new-input(password_text)
# and new-input(confirming_password_text)
while password_text != confirming_password_text:
# do something
# assert the invariant new-input(password_text)
# and new-input(confirming_password_text)
assert password_text == confirming_password_text
```py
4. 定义初始化,以确保不变量为`true`,并且我们可以实际测试终止条件。在这种情况下,我们需要获取这两个变量的值。循环现在看起来像这样:
```
password_text= getpass()
confirming_password_text= getpass("Confirm: ")
# assert new-input(password_text)
# and new-input(confirming_password_text)
while password_text != confirming_password_text:
# do something
# assert new-input(password_text)
# and new-input(confirming_password_text)
assert password_text == confirming_password_text
```py
5. 写下将把不变量重置为`true`的循环体。我们需要写尽可能少的语句来做到这一点。对于这个示例循环,最少的语句非常明显,它们与初始化匹配。我们更新的循环如下:
```
password_text= getpass()
confirming_password_text= getpass("Confirm: ")
# assert new-input(password_text)
# and new-input(confirming_password_text)
while password_text != confirming_password_text:
password_text= getpass()
confirming_password_text= getpass("Confirm: ")
# assert new-input(password_text)
# and new-input(confirming_password_text)
assert password_text == confirming_password_text
```py
6. Identify a clock—a monotonically decreasing function that shows that each iteration of the loop really does make progress toward the terminating condition.
当收集人们的输入时,我们不得不假设他们最终会输入一对匹配的数据。每一次循环都会让我们离匹配的一对更近一步。为了恰当的形式化,我们可以假设在匹配之前会有*n*个输入;我们必须证明,通过环路的每次行程都会减少剩余的次数。
在复杂的情况下,我们可能需要将用户的输入视为一个值列表。在我们的例子中,我们认为用户输入是一个成对的序列:*[(p<sub>1</sub>、q<sub>1</sub>、(p<sub>2</sub>、q<sub>2</sub>、(p<sub>3</sub>、q【T113、(p<sub>n</sub>、q<sub>n</sub>】。有了一个有限的列表,我们可以更容易地推断我们的循环是否真的正在朝着完成的方向前进。*
因为我们基于目标`final`条件构建了循环,所以我们可以完全确定它做了我们想要它做的事情。如果我们的逻辑是正确的,循环将终止,并将以预期结果终止。这是所有编程的目标,使机器在给定初始状态时达到所需状态。
删除一些注释后,我们将此作为最终循环:
password_text= getpass()
confirming_password_text= getpass("Confirm: ")
while password_text != confirming_password_text:
password_text= getpass()
confirming_password_text= getpass("Confirm: ")
assert password_text == confirming_password_text
我们将最后的 post 条件保留为`assert`声明。对于复杂的循环,它既是一个内置测试,也是一个解释循环如何工作的注释。
这个设计过程通常会产生一个类似于我们根据直觉开发的循环。对于一个直观的设计,一步一步的论证并没有什么错。一旦我们这样做了几次,我们就可以更加自信地使用循环,因为我们知道我们可以证明设计的合理性。
在这种情况下,循环体和初始化恰好是相同的代码。如果这是一个问题,我们可以定义一个小的两行函数来避免重复代码。我们将在[第 3 章](03.html#page "Chapter 3. Function Definitions")、*函数定义*中了解这一点。
## 它是如何工作的。。。
我们首先阐明循环的目标。我们所做的其他一切都将确保所编写的代码会导致该目标条件。事实上,这就是所有软件设计背后的动机,我们总是试图写最少的语句来达到给定的目标状态。我们经常从*向后*从目标到初始化。推理链中的每一步本质上都是陈述某种陈述的最薄弱的先决条件,`S`,它导致我们期望的结果条件。
给定一个后置条件,我们试图求解一个语句和一个先决条件。我们一直在构建这种模式:
assert pre-condition
S
assert post-condition
post 条件是我们对 done 的定义。我们需要假设一个陈述,`S`,它导致完成,并且是该陈述的前提条件。总是有无限多的备选语句;我们关注最薄弱的前提,即假设最少的前提。
在编写初始化语句时,我们通常会发现前置条件只是`true`:任何初始状态都将作为语句的前提条件。这就是我们如何知道我们的程序可以从任何初始状态开始,并按预期完成。这是理想的。
在设计`while`语句时,我们在语句体中有一个嵌套的上下文。身体应始终处于将不变条件重新设置为`true`的过程中。在我们的示例中,这意味着从用户那里读取更多的输入。在其他示例中,我们可能正在处理字符串中的另一个字符,或一组数字中的另一个数字。
我们需要证明当不变量为`true`且循环条件为`false`时,我们的最终目标就实现了。当我们从最终目标开始,并基于最终目标创建不变量和循环条件时,这种证明更容易。
重要的是耐心地做每一步,这样我们的推理才是可靠的。我们需要证明这个循环会起作用。然后我们就可以自信地运行单元测试了。
## 另见
* 我们在*中介绍了高级循环设计的一些其他方面,以避免中断语句*的潜在问题。
* 我们还在*设计复杂的 if…elif 链*配方中研究了这一概念。
* 关于这个主题的一篇经典文章是 David Gries,*关于开发循环不变量和循环的标准策略的说明*。参见[http://www.sciencedirect.com/science/article/pii/0167642383900151](http://www.sciencedirect.com/science/article/pii/0167642383900151) 。
* 算法设计是一个大课题。一个很好的介绍是由 Skiena,*算法设计手册*。参见[http://www3.cs.stonybrook.edu/~algorith/](http://www3.cs.stonybrook.edu/~algorith/)。
# 避免中断语句的潜在问题
理解`for`语句的常见方式是,它为所有条件创建*。在语句末尾,我们可以断言,对于集合中的所有项,已经完成了一些处理。*
这不是`for`声明的唯一含义。当我们在`for`的主体中引入`break`语句时,我们将语义更改为*存在*。当`break`语句离开`for`(或`while`语句时,我们只能断言至少存在一项导致该语句结束。
这里有一个次要问题。如果循环结束时没有执行`break`,该怎么办?我们被迫断言,甚至不存在触发`break`的项目。**德摩根定律**告诉我们,对于所有条件,不存在的条件可以重新表述为*:∃ <sub>*x*</sub>*B*(*x*)≡ ∀ <sub>*x*</sub>、*B*(*x*)。在此公式中,*B(x)*是包含`break`的`if`语句上的条件。如果我们没有找到*B(x)*,那么对于所有项目,*B(x)*是`true`。这显示了所有*循环的典型*与*之间的一些对称性。存在*循环,其中包括`break`。*
离开`for`或`while`语句时的`true`条件可能不明确。结局正常吗?它执行了`break`吗?我们无法*轻易*说出,因此我们将提供一个配方,为我们提供一些设计指导。
当我们有多个`break`语句时,这可能会成为一个更大的问题,每个语句都有自己的条件。我们如何最大限度地减少复杂条件下产生的问题?
## 准备好了吗
让我们找出字符串中第一个出现的`:`或`=`。这是一个很好的例子,说明*对`for`语句存在*修改。我们不想处理所有字符,我们想知道最左边的`:`或`=`在哪里。
sample_1 = "some_name = the_value" for position in range(len(sample_1)): ... if sample_1[position] in '=:': ... break print('name=', sample_1[:position], ... 'value=', sample_1[position+1:]) name= some_name value= the_value
这个案子怎么样?
sample_2 = "name_only" for position in range(len(sample_2)): ... if sample_2[position] in '=:': ... break print('name=', sample_2[:position], ... 'value=', sample_2[position+1:])
name= name_onl value=
这是非常错误的。怎么搞的?
## 怎么做。。。
正如我们在*设计一个正确终止*配方的 while 语句时所指出的,每个语句都建立了一个 post 条件。在设计循环时,我们需要清楚地说明该条件。在这种情况下,我们没有正确地表达 post 条件。
理想情况下,post 条件应该是类似于`text[position] in '=:'`的简单条件。然而,如果给定的文本中没有`=`或`:`,那么简单的 post 条件就没有逻辑意义。当不存在符合条件的字符时,我们不能断言不存在的字符的位置。
1. 写下明显的 post 条件。我们有时称之为*快乐之路*状态,因为这是`true`没有发生异常情况时的状态。
```
text[position] in '=:'
```py
2. 为边缘案例添加后置条件。在本例中,我们有两个附加条件:
* 没有`=`或`:`。
* 根本没有字符。`len()`为零,循环实际上从未做任何事情。在这种情况下,将永远不会创建位置变量。
```
(len(text) == 0
or not('=' in text or ':' in text)
or text[position] in '=:')
```py
3. 如果正在使用一个 TyrT0p 语句,请考虑重新设计它以完成条件。这可以消除对`break`语句的需要。
4. 如果正在使用一个`for`语句,请确保完成了正确的初始化,并将各种终止条件添加到循环后的语句中。在`x = 0`之后加上`for x = ...`可能看起来是多余的。不过,对于不执行`break`语句的循环来说,这是必要的。
```
>>> position = -1 # If it's zero length
>>> for position in range(len(sample_2)):
... if sample_2[position] in '=:':
... break
...
>>> if position == -1:
... print("name=", None, "value=", None)
... elif not(text[position] == ':' or text[position] == '='):
... print("name=", sample_2, "value=", None)
... else:
... print('name=', sample_2[:position],
... 'value=', sample_2[position+1:])
name= name_only value= None
```py
在`for`之后的语句中,我们已经明确列举了所有终止条件。最终输出`name= name_only value= None`确认我们已正确处理示例文本。
## 它是如何工作的。。。
这种方法迫使我们仔细地计算 post 条件,这样我们就可以完全确定我们知道循环终止的所有原因。
在包含多个`break`语句的更复杂循环中,post 条件可能很难完全解决。回路的 post 条件必须包括离开回路的所有原因*正常*原因加上所有`break`条件。
在许多情况下,我们可以重构循环以将处理推进到循环体中。我们不简单地断言`position`是`=`或`:`字符的索引,而是包括分配`name`和`value`值的下一个处理步骤。我们可能会有这样的情况:
if len(sample_2) > 0:
name, value = sample_2, None
else:
name, value = None, None
for position in range(len(sample_2)):
if sample_2[position] in '=:':
name, value = sample_2[:position], sample2[position:]
print('name=', name, 'value=', value)
这个版本基于之前评估的一整套 post 条件,向前推进一些处理。这种重构很常见。
这个想法是放弃任何假设或直觉。有了一点纪律,我们可以从任何声明中确定后条件。
事实上,我们越是考虑后置条件,我们的软件就越精确。必须明确我们软件的目标,并通过选择最简单的语句使目标成为`true`,从目标开始反向工作。
## 还有更多。。。
我们还可以在`for`语句上使用`else`子句来确定循环是正常完成还是执行了`break`语句。我们可以用这样的方法:
for position in range(len(sample_2)):
if sample_2[position] in '=:':
name, value = sample_2[:position], sample_2[position+1:]
break
else:
if len(sample_2) > 0:
name, value = sample_2, None
else:
name, value = None, None
`else`条件有时令人困惑,我们不建议这样做。目前还不清楚它是否比任何替代品都好。很容易忘记执行`else`的原因,因为它很少使用。
## 另见
* 关于这个主题的一篇经典文章是 David Gries,*关于开发循环不变量和循环的标准策略的说明*。参见[http://www.sciencedirect.com/science/article/pii/0167642383900151](http://www.sciencedirect.com/science/article/pii/0167642383900151) 。
# 利用异常匹配规则
`try`语句让我们捕获一个异常。当引发异常时,我们有许多选择来处理它:
* **忽略它**:如果我们什么都不做,程序就会停止。我们可以通过两种方式做到这一点:首先不要使用`try`语句,或者在`try`语句中没有匹配的`except`子句。
* **记录**:我们可以写一条消息,让它传播;通常这将停止程序。
* **从中恢复**:我们可以写一个`except`子句来执行一些恢复操作,以撤销`try`子句中仅部分完成的事情的影响。我们可以更进一步,将`try`语句封装在`while`语句中,并不断重试,直到成功。
* **使其静音**:如果我们什么也不做(即`pass`),则在`try`语句之后恢复处理。这就消除了例外。
* **重写**:我们可以提出不同的异常。原始异常将成为新引发的异常的上下文。
* **将其链接**:我们将一个不同的异常链接到原始异常。我们将在*链接异常中与*语句中的 raise 一起查看这一点。
嵌套上下文呢?在这种情况下,内部`try`可以忽略异常,但外部上下文可以处理异常。每个`try`上下文的基本选项集是相同的。软件的整体行为取决于嵌套定义。
`try`语句的设计取决于 Python 异常形成类层次结构的方式。详见*第 5.4 节*、*Python 标准库*。例如,`ZeroDivisionError`也是一个`ArithmeticError`和一个`Exception`。又例如,`FileNotFoundError`也是`OSError`和`Exception`。
如果我们试图处理详细异常和一般异常,那么这种层次结构可能会导致混乱。
## 准备好了吗
假设我们将简单地使用`shutil`将文件从一个地方复制到另一个地方。可能提出的大多数异常表明问题太严重,无法解决。然而,在罕见的`FileExistsError`事件中,我们希望尝试恢复操作。
以下是我们想做的大致概述:
from pathlib import Path
import shutil
import os
source_path = Path(os.path.expanduser(
'~/Documents/Writing/Python Cookbook/source'))
target_path = Path(os.path.expanduser(
'~/Dropbox/B05442/demo/'))
for source_file_path in source_path.glob('*/*.rst'):
source_file_detail = source_file_path.relative_to(source_path)
target_file_path = target_path / source_file_detail
shutil.copy( str(source_file_path), str(target_file_path
我们有两条路径,`source_path`和`target_path`。我们已经找到了`source_path`下所有包含`*.rst`文件的目录。
表达式`source_file_path.relative_to(source_path)`给出了文件名的末尾,即基本目录之后的部分。我们使用它在`target`目录下构建一个新路径。
虽然我们可以使用`pathlib.Path`对象进行很多普通的路径处理,但在 Python 3.5 模块中,如`shutil`需要字符串文件名,而不是`Path`对象;我们需要显式地转换`Path`对象。我们只能希望 Python3.6 能够改变这一点。
处理`shutil.copy()`函数引发的异常时会出现问题。我们需要一个`try`语句,这样我们就可以从某些类型的错误中恢复。如果尝试运行以下操作,我们将看到此类错误:
FileNotFoundError: [Errno 2]
No such file or directory:
'/Users/slott/Dropbox/B05442/demo/ch_01_numbers_strings_and_tuples/index.rst'
我们如何创建一个`try`语句,以正确的顺序处理异常?
## 怎么做。。。
1. 将我们想要使用的代码缩进到`try`块中:
```
try:
shutil.copy( str(source_file_path), str(target_file_path) )
```py
2. 首先包括最具体的异常类。在这种情况下,我们对具体的`FileNotFoundError`和更一般的`OSError`有单独的响应。
```
try:
shutil.copy( str(source_file_path), str(target_file_path) )
except FileNotFoundError:
os.makedir( target_file_path.parent )
shutil.copy( str(source_file_path), str(target_file_path) )
```py
3. Include any more general exceptions later:
```
try:
shutil.copy( str(source_file_path), str(target_file_path) )
except FileNotFoundError:
os.makedirs( str(target_file_path.parent) )
shutil.copy( str(source_file_path), str(target_file_path) )
except OSError as ex:
print(ex)
```py
我们首先将异常与最具体的异常进行匹配,然后再将异常与更一般的异常进行匹配。
我们通过创建丢失的目录来处理`FileNotFoundError`。然后我们又做了一次`copy()`,知道它现在可以正常工作了。
我们压制了班上任何其他的例外情况。例如,如果存在权限问题,将简单地记录该错误。我们的目标是尝试复制所有文件。将记录导致问题的所有文件,但复制过程将继续。
## 它是如何工作的。。。
Python 的异常匹配规则非常简单:
* 按顺序处理`except`子句
* 将实际异常与异常类(或异常类的元组)匹配。匹配表示实际的异常对象(或异常对象的任何基类)属于`except`子句中的给定类。
这些规则说明了为什么我们将最特定的异常类放在第一位,而将更一般的异常类放在最后。像`Exception`这样的泛型异常类将匹配几乎所有类型的异常。我们不想先检查这个,因为不会检查其他条款。我们必须总是把一般例外放在最后。
还有一个更通用的类,`BaseException`类。没有很好的理由处理这个类的异常。如果我们这样做,我们将捕获`SystemExit`和`KeyboardInterrupt`异常,这会干扰杀死行为不正常的应用程序的能力。在定义存在于正常异常层次结构之外的新异常类时,我们仅将`BaseException`类用作超类。
## 还有更多。。。
我们的示例包括一个嵌套上下文,在该上下文中可以引发第二个异常。考虑这个条款:
except FileNotFoundError:
os.makedirs( str(target_file_path.parent) )
shutil.copy( str(source_file_path), str(target_file_path) )
如果`os.makedirs()`或`shutil.copy()`函数引发另一个异常,则此`try`语句不会处理该异常。此处提出的任何异常都将使整个程序崩溃。我们有两种方法来处理这个问题,这两种方法都涉及嵌套的`try`语句。
我们可以重写它,在恢复过程中包含嵌套的`try`:
try:
shutil.copy( str(source_file_path), str(target_file_path) )
except FileNotFoundError:
try:
os.makedirs( str(target_file_path.parent) )
shutil.copy( str(source_file_path), str(target_file_path) )
except OSError as ex:
print(ex)
except OSError as ex:
print(ex)
在本例中,我们在两个地方重复了`OSError`处理。在嵌套上下文中,我们将记录异常并让它传播,这可能会停止程序。在外部环境中,我们将做同样的事情。
我们说*可能会停止程序*,因为该代码可以在`try`语句中使用,该语句可能会处理这些异常。如果没有其他`try`上下文,则这些未处理的异常将停止程序。
我们还可以重写我们的整体语句,使之具有嵌套的`try`语句,将两种异常处理策略分离为更多的局部和全局考虑。它看起来是这样的:
try:
try:
shutil.copy( str(source_file_path), str(target_file_path) )
except FileNotFoundError:
os.makedirs( str(target_file_path.parent) )
shutil.copy( str(source_file_path), str(target_file_path) )
except OSError as ex:
print(ex)
内部`try`语句中进行`makedirs`处理的副本只处理`FileNotFoundError`异常。任何其他异常都将传播到外部`try`语句。在本例中,我们嵌套了异常处理,以便泛型处理封装了特定处理。
## 另见
* 在*中,使用 except:子句*配方避免潜在问题,我们在设计异常时考虑了一些额外的注意事项
* 在*链接异常与 raise from 语句*配方中,我们将研究如何链接异常,以便单个异常类包装不同的详细异常
# 使用 except:子句避免潜在问题
异常处理中存在一些常见错误。这些可能会导致程序无响应。
我们可能犯的错误之一是使用`except:`子句。如果我们对试图处理的例外情况不谨慎,我们还可能犯一些其他错误。
此配方将显示一些我们可以避免的常见异常处理错误。
## 准备好了吗
在*避免 exception:子句*配方的潜在问题中,我们在设计异常处理时考虑了一些注意事项。在这个配方中,我们不鼓励使用`BaseException`,因为我们可以干扰停止行为不端的 Python 程序。
我们将在这个食谱中扩展*什么不做*的概念。
## 怎么做。。。
使用`except Exception:`作为最通用的异常管理类型。
处理太多的异常可能会干扰我们停止行为不端的 Python 程序的能力。当我们点击*Ctrl*+*C*或通过`kill -2`发送`SIGINT`信号时,我们通常希望程序停止。我们很少希望程序编写消息并继续运行,或者完全停止响应。
还有一些其他类别的例外情况,我们在尝试处理时应该小心:
* 系统错误
* 访问违例
* 记忆者
通常,这些异常意味着 Python 内部的某些地方情况很糟糕。我们应该允许程序失败,找到根本原因并修复它,而不是让这些异常保持沉默,或者尝试一些恢复。
## 它是如何工作的。。。
我们应该避免使用两种技术:
* 不要捕捉`BaseException`类
* 不要毫无例外地使用`except:`类。这符合所有例外情况;这将包括我们应该避免尝试处理的异常。
在没有特定类的情况下使用`except BaseException`或 except 可能会导致程序在我们需要停止它的时候变得无响应。
此外,如果我们捕获这些异常中的任何一个,我们可能会干扰这些内部异常的处理方式:
* `SystemExit`
* `KeyboardInterrupt`
* `GeneratorExit`
如果我们沉默、包装或重写其中任何一个,我们可能会在不存在问题的地方制造出一个问题。我们可能把一个简单的问题恶化成一个更大、更神秘的问题。
### 注
编写一个永不崩溃的程序是一个崇高的愿望。干扰 Python 的一些内部异常不会创建更可靠的程序。相反,它创建了一个程序,在这个程序中,一个明显的失败被掩盖起来,变成了一个模糊的谜。
## 另见
* 在*利用异常匹配规则*配方中,我们在设计异常时考虑了一些注意事项
* 在*链接异常与 raise from 语句*配方中,我们研究了如何链接异常,以便单个异常类封装不同的详细异常。
# 将异常链接到 raise from 语句
在某些情况下,我们可能希望将一些看似无关的异常合并到一个通用异常中。一个复杂的模块通常会定义一个单一的泛型`Error`异常,该异常适用于模块内可能出现的许多情况。
大多数情况下,通用异常是所需的全部。如果模块的`Error`被提升,则表示某些内容不起作用。
不太常见的情况是,我们需要用于调试或监视目的的详细信息。我们可能希望将它们写入日志,或者在电子邮件中包含详细信息。在这种情况下,我们需要提供支持细节来放大或扩展通用异常。我们可以通过从泛型异常链接到根本原因异常来实现这一点。
## 准备好了吗
假设我们正在编写一些复杂的字符串处理。我们希望将许多不同种类的详细异常视为一个单一的一般性错误,这样我们的软件用户就不会受到实现细节的影响。我们可以将详细信息附加到一般错误。
## 怎么做。。。
1. To create a new exception, we can do this:
```
class Error(Exception):
pass
```py
这足以定义一个新的异常类。
2. When handling exceptions, we can chain them using the `raise from` statement like this:
```
try:
something
except (IndexError, NameError) as exception:
print("Expected", exception)
raise Error("something went wrong") from exception
except Exception as exception:
print("Unexpected", exception)
raise
```py
在第一个`except`子句中,我们匹配了两种异常类。无论我们得到哪种类型的异常,我们都会从模块的泛型`Error`异常类中引发一个新异常。新异常将链接到根本原因异常。
在第二个`except`子句中,我们匹配了泛型`Exception`类。我们编写了一条日志消息并重新引发了异常。这里,我们不是链接,而是在另一个上下文中继续异常处理。
## 它是如何工作的。。。
Python 异常类都有记录异常原因的位置。我们可以使用`raise Exception from Exception`语句设置这个`__cause__`属性。
以下是引发此异常时的外观:
class Error(Exception): ... pass try:
... 'hello world'[99] ... except (IndexError, NameError) as exception: ... raise Error("index problem") from exception ... Traceback (most recent call last): File "<doctest default[0]>", line 2, in 'hello world'[99] IndexError: string index out of range
我们刚才看到的异常是以下异常的直接原因:
Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/doctest.py", line 1318, in __run compileflags, 1), test.globs) File "<doctest default[0]>", line 4, in raise Error("index problem") from exception Error: index problem
这显示了一个链接异常。`Traceback`消息中的第一个异常是`IndexError`异常。这是直接原因。`Traceback`中的第二个例外是我们的通用`Error`例外。这是一个通用摘要异常,链接到原始原因。
应用程序将在`try:`语句中看到`Error`异常。我们可能会有这样的情况:
try:
some_function()
except Error as exception:
print(exception)
print(exception .__cause__)
这里我们展示了一个名为`some_function()`的函数,它可以引发泛型`Error`异常。如果此函数确实引发异常,`except`子句将匹配泛型`Error`异常。我们可以打印异常的消息`exception`,以及根本原因异常`exception.__cause__`。在许多应用程序中,`exception.__cause__`值可能会写入调试日志,而不是显示给用户。
## 还有更多。。。
如果在异常处理程序中引发异常,这也会创建一种链式异常关系。这是*上下文*关系,而不是*原因*关系。
上下文消息看起来类似。信息略有不同。上面写着`During handling of the above exception, another exception occurred:`。第一个`Traceback`将显示原始异常。第二条消息是在不使用显式 from 连接的情况下引发的异常。
通常情况下,上下文是指在`except`处理块中出现错误的非计划内容。例如,我们可能有:
try:
something
except ValueError as exception:
print("Some message", exceotuib)
这将在`ValueError`异常的上下文中引发`NameError`异常。`NameError`异常源于将异常变量拼写错误为`exceotuib`。
## 另见
* 在*利用异常匹配规则*配方中,我们在设计异常时考虑了一些注意事项
* 在*中,使用 except:子句*配方避免潜在问题,我们在设计异常时考虑了一些额外的注意事项
# 使用 with 语句管理上下文
在许多情况下,我们的脚本将与外部资源纠缠在一起。最常见的例子是磁盘文件和到外部主机的网络连接。一个常见的错误是永远保留这些纠缠,毫无用处地占用这些资源。这些有时被称为内存**泄漏**,因为每次打开新文件而不关闭以前使用的文件时,可用内存都会减少。
我们想隔离每一个纠缠,这样我们就可以确保资源被正确地获取和释放。我们的想法是创建一个上下文,脚本在其中使用外部资源。在上下文结束时,我们的程序不再绑定到资源,我们希望得到资源被释放的保证。
## 准备好了吗
假设我们想将数据行写入 CSV 格式的文件。完成后,我们希望确保文件已关闭,各种操作系统资源(包括缓冲区和文件句柄)已释放。我们可以在上下文管理器中实现这一点,它保证文件将被正确关闭。
由于我们将使用 CSV 文件,我们可以使用`csv`模块来处理格式的细节:
import csv
我们还将使用`pathlib`模块定位要处理的文件:
import pathlib
为了编写一些内容,我们将使用以下愚蠢的数据源:
some_source = [[2,3,5], [7,11,13], [17,19,23]]
这将为我们提供一个了解`with`语句的上下文。
## 怎么做。。。
1. 通过打开文件或使用`urllib.request.urlopen()`创建网络连接来创建上下文。其他常见的上下文包括归档,如`zip`文件和`tar`文件:
```
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
```py
2. 包括所有处理,缩进到`with`语句中:
```
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'data', 'headings'])
for data in some_source:
writer.writerow(data)
```py
3. 当我们将文件用作上下文管理器时,该文件将在缩进上下文块的末尾自动关闭。即使引发异常,文件仍会正确关闭。超出上下文完成并释放资源后完成的处理:
```
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'headings'])
for data in some_source:
writer.writerow(data)
print('finished writing', target_path)
```py
`with`上下文之外的语句将在上下文关闭后执行。由`target_path.open()`打开的文件的命名资源将被正确关闭。
即使在`with`语句中引发异常,文件仍然正确关闭。上下文管理器将收到异常通知。它可以关闭文件并允许异常传播。
## 它是如何工作的。。。
上下文管理器收到来自代码块的两种退出通知:
* 正常退出,无一例外
* 提出了一个例外
上下文管理器将在任何情况下使我们的程序与外部资源分离。可以关闭文件。可以断开网络连接。可以提交或回滚数据库事务。可以释放锁。
我们可以通过在`with`语句中包含一个手动异常来进行实验。这可以表明文件已正确关闭。
try:
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'headings'])
for data in some_source:
writer.writerow(data)
raise Exception("Just Testing")
except Exception as exc:
print(target_file.closed)
print(exc)
print('finished writing', target_path)
在本例中,我们将实际工作包装在一个`try`语句中。这允许我们在将第一个文件写入 CSV 文件后引发异常。当引发异常时,我们可以打印异常。此时,该文件也将被关闭。输出结果如下所示:
True
Just Testing
finished writing code/test.csv
这表明文件已正确关闭。它还向我们显示与异常相关的消息,以确认这是我们手动引发的异常。输出的`test.csv`文件将只包含来自`some_source`变量的第一行数据。
## 还有更多。。。
Python 为我们提供了许多上下文管理器。我们注意到,打开的文件是一个上下文,正如`urllib.request.urlopen()`创建的开放网络连接一样。
对于所有文件操作和所有网络连接,我们应该使用一个`with`语句作为上下文管理器。很难找到这条规则的例外。
事实证明,`decimal`模块利用上下文管理器允许对十进制算法的执行方式进行本地化更改。我们可以使用`decimal.localcontext()`函数作为上下文管理器来更改由`with`语句隔离的计算的舍入规则或精度。
我们也可以定义自己的上下文管理器。`contextlib`模块包含函数和装饰器,可以帮助我们围绕未明确提供它们的资源创建上下文管理器。
使用锁时,`with`上下文是获取和释放锁的理想方式。参见[https://docs.python.org/3/library/threading.html#with-锁](https://docs.python.org/3/library/threading.html#with-locks)用于`threading`模块创建的锁对象与上下文管理器之间的关系。
## 另见
* 参见[https://www.python.org/dev/peps/pep-0343/](https://www.python.org/dev/peps/pep-0343/) 关于 with 语句的起源