现代 Python 秘籍(二)
原文:
zh.annas-archive.org/md5/185a6e8218e2ea258a432841b73d4359
译者:飞龙
第二章:语句和语法
在本章中,我们将查看以下配方:
-
编写 Python 脚本和模块文件
-
编写长行代码
-
包括描述和文档
-
在文档字符串中更好的 RST 标记
-
设计复杂的 if...elif 链
-
设计一个终止的 while 语句
-
避免 break 语句可能出现的问题
-
利用异常匹配规则
-
避免 except:子句可能出现的问题
-
使用 raise from 语句链接异常
-
使用 with 语句管理上下文
介绍
Python 语法设计得非常简单。有一些规则;我们将查看语言中一些有趣的语句,以了解这些规则。仅仅看规则而没有具体的例子可能会令人困惑。
我们将首先介绍创建脚本文件的基础知识。然后我们将继续查看一些常用语句。Python 语言中只有大约二十种不同类型的命令语句。我们已经在第一章中看过两种语句,Numbers, Strings, and Tuples:赋值语句和表达式语句。
当我们写这样的东西时:
**>>> print("hello world")**
**hello world**
我们实际上执行的是一个只包含函数print()
评估的语句。这种语句-在其中我们评估一个函数或对象的方法-是常见的。
我们已经看到的另一种语句是赋值语句。Python 在这个主题上有很多变化。大多数时候,我们将一个值赋给一个变量。然而,有时我们可能会同时给两个变量赋值,就像这样:
**quotient, remainder = divmod(355, 113)**
这些配方将查看一些更复杂的语句,包括if
,while
,for
,try
,with
和raise
。在探索不同的配方时,我们还将涉及其他一些。
编写 Python 脚本和模块文件-语法基础
为了做任何真正有用的事情,我们需要编写 Python 脚本文件。我们可以在交互>>>
提示符下尝试语言。然而,对于真正的工作,我们需要创建文件。编写软件的整个目的是为我们的数据创建可重复的处理。
我们如何避免语法错误,并确保我们的代码与常用的代码匹配?我们需要查看一些style的常见方面-我们如何使用空白来澄清我们的编程。
我们还将研究一些更多的技术考虑因素。例如,我们需要确保以 UTF-8 编码保存我们的文件。虽然 Python 仍然支持 ASCII 编码,但对于现代编程来说,这是一个不好的选择。我们还需要确保使用空格而不是制表符。如果我们尽可能使用 Unix 换行符,我们也会发现事情稍微简单一些。
大多数文本编辑工具都可以正确处理 Unix(换行符)和 Windows 或 DOS(回车换行符)的行尾。任何不能处理这两种行尾的工具都应该避免使用。
准备好了
要编辑 Python 脚本,我们需要一个好的编程文本编辑器。Python 自带一个方便的编辑器,IDLE。它工作得相当不错。它让我们可以在文件和交互>>>
提示之间来回跳转,但它不是一个很好的编程编辑器。
有数十种优秀的编程编辑器。几乎不可能只建议一个。所以我们将建议几个。
ActiveState 有非常复杂的 Komodo IDE。Komodo Edit 版本是免费的,并且与完整的 Komodo IDE 做了一些相同的事情。它可以在所有常见的操作系统上运行;这是一个很好的第一选择,因为无论我们在哪里编写代码,它都是一致的。
请参阅komodoide.com/komodo-edit/
。
Notepad++适用于 Windows 开发人员。请参阅notepad-plus-plus.org
。
BBEdit 非常适合 Mac OS X 开发人员。请参阅www.barebones.com/products/bbedit/
。
对于 Linux 开发人员,有几个内置的编辑器,包括 VIM、gedit 或 Kate。这些都很好。由于 Linux 倾向于偏向开发人员,可用的编辑器都适合编写 Python。
重要的是,我们在工作时通常会打开两个窗口:
-
我们正在处理的脚本或文件。
-
Python 的
>>>
提示(可能来自 shell,也可能来自 IDLE),我们可以尝试一些东西,看看什么有效,什么无效。我们可能会在 Notepad++中创建脚本,但使用 IDLE 来尝试数据结构和算法。
实际上我们这里有两个配方。首先,我们需要为我们的编辑器设置一些默认值。然后,一旦编辑器正确设置,我们就可以为我们的脚本文件创建一个通用模板。
如何做...
首先,我们将看一下我们首选编辑器中需要做的一般设置。我们将使用 Komodo 示例,但基本原则适用于所有编辑器。一旦我们设置了编辑首选项,我们就可以创建我们的脚本文件。
-
打开首选编辑器。查看首选项页面。
-
查找首选文件编码的设置。使用 Komodo Edit 首选项,它在国际化选项卡上。将其设置为UTF-8。
-
查找缩进设置。如果有一种方法可以使用空格而不是制表符,请检查此选项。使用 Komodo Edit,我们实际上是反过来做的——我们取消优先使用空格而不是制表符。
注意
规则是:我们想要空格;我们不想要制表符。
还要将每个缩进的空格设置为四个。这对于 Python 代码来说很典型。它允许我们有几个缩进级别,但仍然保持代码相当窄。
一旦我们确定我们的文件将以 UTF-8 编码保存,并且我们也确定我们使用空格而不是制表符,我们可以创建一个示例脚本文件:
- 大多数 Python 脚本文件的第一行应该是这样的:
#!/usr/bin/env python3
这将在你正在编写的文件和 Python 之间建立关联。
对于 Windows,文件名到程序的关联是通过 Windows 控制面板中的一个设置来完成的。在默认程序控制面板中,有一个设置关联面板。此控制面板显示.py
文件绑定到 Python 程序。这通常由安装程序设置,我们很少需要更改它或手动设置它。
注意
Windows 开发人员可以无论如何包含序言行。这将使 Mac OS X 和 Linux 的人们从 GitHub 下载项目时感到高兴。
- 在序言之后,应该有一个三引号的文本块。这是我们要创建的文件的文档字符串(称为docstring)。这在技术上不是强制性的,但对于解释文件包含的内容至关重要。
'''
A summary of this script.
'''
因为 Python 的三引号字符串可以无限长,所以可以随意写入必要的内容。这应该是描述脚本或库模块的主要方式。这甚至可以包括它是如何工作的示例。
- 现在来到脚本的有趣部分:真正执行操作的部分。我们可以编写所有需要完成工作的语句。现在,我们将使用这个作为占位符:
print('hello world')
有了这个,我们的脚本就有了作用。在其他示例中,我们将看到许多其他用于执行操作的语句。通常会创建函数和类定义,并编写语句来使用函数和类执行操作。
在我们的脚本的顶层,所有语句必须从左边缘开始,并且必须在一行上完成。有一些复杂的语句,其中将嵌套在其中的语句块。这些内部语句块必须缩进。通常情况下,因为我们将缩进设置为四个空格,我们可以按Tab键进行缩进。
我们的文件应该是这样的:
#!/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 程序会查看文件的第一行,以确定文件包含的内容。文件的前几个字节有时被称为魔术,因为 shell 程序正在窥视它们。Shell 程序会寻找#!
这个两个字符的序列,以确定负责这些数据的程序。我们更喜欢使用/usr/bin/env
来启动 Python 程序。我们可以利用这一点来通过env
程序进行 Python 特定的环境设置。
还有更多...
Python 标准库文档部分源自模块文件中存在的文档字符串。在模块中编写复杂的文档字符串是常见做法。有一些工具,如 Pydoc 和 Sphinx,可以将模块文档字符串重新格式化为优雅的文档。我们将在单独的部分中学习这一点。
此外,单元测试用例可以包含在文档字符串中。像doctest这样的工具可以从文档字符串中提取示例并执行代码,以查看文档中的答案是否与运行代码找到的答案匹配。本书的大部分内容都是通过 doctest 验证的。
三引号文档字符串优于#
注释。#
和行尾之间的文本会被忽略,并被视为注释。由于这仅限于单行,因此使用得很少。文档字符串的大小可以是无限的;它们被广泛使用。
在 Python 3.5 中,我们有时会在脚本文件中看到这样的东西:
color = 355/113 # type: float
# type: float
注释可以被类型推断系统用来确定程序实际执行时可能出现的各种数据类型。有关更多信息,请参阅Python Enhancement Proposal 484:www.python.org/dev/peps/pep-0484/
。
有时文件中还包含另一个开销。VIM 编辑器允许我们在文件中保留编辑首选项。这被称为modeline。我们经常需要通过在我们的~/.vimrc
文件中包含set modeline
设置来启用 modelines。
一旦我们启用了 modelines,我们可以在文件末尾包含一个特殊的# vim
注释来配置 VIM。
这是一个对 Python 有用的典型 modeline:
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
这将把 Unicode u+0009
TAB 字符转换为八个空格,当我们按下Tab键时,我们将移动四个空格。这个设置被保存在文件中;我们不需要进行任何 VIM 设置来将这些设置应用到我们的 Python 脚本文件中。
另请参阅
-
我们将在包括描述和文档和在文档字符串中编写更好的 RST 标记这两个部分中学习如何编写有用的文档字符串
-
有关建议的样式的更多信息,请参阅
www.python.org/dev/peps/pep-0008/
编写长行代码
有很多时候,我们需要编写非常长的代码行,以至于它们非常难以阅读。许多人喜欢将代码行的长度限制在 80 个字符或更少。这是一个众所周知的图形设计原则,即较窄的行更容易阅读;意见不一,但 65 个字符经常被认为是理想的长度。参见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))**
我们可以将这分解为三个中间值。
- 识别整体表达式中的子表达式。将这些分配给变量:
**>>> 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)**
在这种情况下,()
字符绝对不是表达式的一部分。()
字符只是额外的语法,包括使语句与其他语句一致。
另请参阅
- 隐式行连接也适用于匹配的
[]
字符和{}
字符。这些适用于我们将在第四章中查看的集合数据结构,内置数据结构 - 列表、集合、字典。
包括描述和文档
当我们有一个有用的脚本时,我们经常需要为自己和其他人留下关于它的说明,它是如何解决某个特定问题的,以及何时应该使用它的笔记。
因为清晰很重要,有一些格式化的方法可以帮助使文档非常清晰。这个方法还包含了一个建议的大纲,以便文档会相当完整。
准备工作
如果我们使用编写 Python 脚本和模块文件 - 语法基础的方法来构建一个脚本文件,我们将在我们的脚本文件中放置一个小的文档字符串。我们将扩展这个文档字符串。
还有其他应该使用文档字符串的地方。我们将在第三章和第六章中查看这些额外的位置,函数定义和类和对象的基础知识。
我们有两种一般类型的模块,我们将编写摘要文档字符串:
-
库模块:这些文件将主要包含函数定义以及类定义。在这种情况下,文档字符串摘要可以侧重于模块是什么,而不是它做什么。文档字符串可以提供使用模块中定义的函数和类的示例。在第三章,函数定义,和第六章,类和对象的基础,我们将更仔细地研究这个函数包或类包的概念。
-
脚本:这些通常是我们期望能够完成一些实际工作的文件。在这种情况下,我们希望关注的是做而不是存在。文档字符串应该描述它的功能以及如何使用它。选项、环境变量和配置文件是这个文档字符串的重要部分。
我们有时会创建包含两者的文件。这需要一些仔细的编辑来在做和存在之间取得适当的平衡。在大多数情况下,我们将简单地提供两种文档。
如何做...
编写文档的第一步对于库模块和脚本是相同的:
- 写一个简要概述脚本或模块是什么或做什么。摘要不要深入介绍它的工作原理。就像报纸文章中的导语一样,它介绍了模块的谁、什么、何时、何地、如何和为什么。详细信息将在文档字符串的正文中提供。
工具如 sphinx 和 pydoc 显示信息的方式暗示了特定的样式提示。在这些工具的输出中,上下文是非常清晰的,因此在摘要句中通常可以省略主语。句子通常以动词开头。
例如,像这样的摘要:这个脚本下载并解码了当前的特殊海洋警告(SMW)有一个多余的这个脚本。我们可以去掉它,然后以动词短语下载并解码...开始。
我们可能会这样开始我们的模块文档字符串:
'''
Downloads and decodes the current Special Marine Warning (SMW)
for the area 'AKQ'.
'''
我们将根据模块的一般重点分开其他步骤。
为脚本编写文档字符串
当我们记录脚本时,我们需要关注将使用脚本的人的需求。
-
像之前展示的那样开始,创建一个摘要句。
-
勾勒出文档字符串的其余部分的大纲。我们将使用ReStructuredText(RST)标记。在一行上写出主题,然后在主题下面放一行
=
,使它们成为一个适当的章节标题。记得在每个主题之间留下一个空行。
主题可能包括:
-
概要:如何运行这个脚本的摘要。如果脚本使用
argparse
模块来处理命令行参数,那么argparse
生成的帮助文本就是理想的摘要文本。 -
描述:这个脚本的更完整的解释。
-
选项:如果使用了
argparse
,这是放置每个参数详细信息的地方。通常我们会重复argparse
的帮助参数。 -
环境:如果使用了
os.environ
,这是描述环境变量及其含义的地方。 -
文件:由脚本创建或读取的文件名称是非常重要的信息。
-
示例:始终有一些使用脚本的示例会很有帮助。
-
另请参阅:任何相关的脚本或背景信息。
其他可能有趣的主题包括退出状态,作者,错误,报告错误,历史或版权。在某些情况下,例如关于报告错误的建议,实际上并不属于模块的文档字符串,而是属于项目的 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 处理工具的提示,表明接下来的缩进部分应该被排版为代码。
为库模块编写 docstrings
当我们记录库模块时,我们需要关注的是那些将导入模块以在其代码中使用的程序员的需求。
-
为 docstring 的其余部分草拟一个大纲。我们将使用 RST 标记。在一行上写出主题。在每个主题下面加一行
=
,使主题成为一个适当的标题。记得在每个段落之间留下一个空行。 -
如前所示开始,创建一个摘要句子。
-
描述:模块包含的内容以及模块的用途摘要。
-
模块内容:此模块中定义的类和函数。
-
示例:使用模块的示例。
-
为每个主题填写详细信息。模块内容可能是一个很长的类或函数定义列表。这应该是一个摘要。在每个类或函数内部,我们将有一个单独的 docstring,其中包含该项的详细信息。
-
有关代码示例,请参阅前面的示例。使用
::
作为段落或段落结束。将代码示例缩进四个空格。
工作原理...
几十年来,man page的大纲已经发展成为 Linux 命令的有用摘要。这种撰写文档的一般方法被证明是有用和有韧性的。我们可以利用这一大量的经验,并结构化我们的文档以遵循 man page 模型。
这两种描述软件的方法都是基于许多单独页面文档的摘要。目标是利用众所周知的主题集。这使得我们的模块文档与常见做法相一致。
我们希望准备模块 docstrings,这些 docstrings 可以被 Sphinx Python 文档生成器使用(参见www.sphinx-doc.org/en/stable/
)。这是用于生成 Python 文档文件的工具。Sphinx 中的autodoc
扩展将读取我们的模块、类和函数上的 docstring 头,以生成最终的文档,看起来像 Python 生态系统中的其他模块。
还有更多...
RST 有一个简单的语法规则,即段落之间用空行分隔。
这条规则使得编写的文档可以被各种 RST 处理工具检查,并重新格式化得非常漂亮。
当我们想要包含一段代码块时,我们将有一些特殊的段落:
-
用空行将代码与文本分开。
-
代码缩进四个空格。
-
提供一个
::
前缀。我们可以将其作为自己单独的段落,或者作为引导段落末尾的特殊双冒号:
Here's an example::
more_code()
::
用于引导段落。
在软件开发中有创新和艺术的地方。文档并不是推动创新的地方。聪明的算法和复杂的数据结构可能是新颖和聪明的。
注意
对于只想使用软件的用户来说,独特的语气或古怪的表达并不有趣。在调试时,幽默的风格也不会有帮助。文档应该是平常和常规的。
编写良好的软件文档可能是具有挑战性的。在太少的信息和仅仅重复代码的文档之间存在着巨大的鸿沟。在某个地方,有一个很好的平衡。重要的是要专注于那些对软件或其工作原理了解不多的人的需求。为这些半知识用户提供他们需要描述软件做什么以及如何使用它的信息。
在许多情况下,我们需要解决用例的两个部分:
-
软件的预期用途
-
如何自定义或扩展软件
这可能是两个不同的受众。可能有用户与开发人员不同。每个人都有不同的观点,文档的不同部分需要尊重这两种观点。
另请参阅
-
我们将在在 docstrings 中编写更好的 RST 标记中查看其他技术。
-
如果我们使用了编写 python 脚本和模块文件-语法基础的方法,我们将在我们的脚本文件中放置一个文档字符串。当我们在第三章中构建函数时,函数定义,以及在第六章中构建类时,类和对象的基础,我们将看到其他可以放置文档字符串的地方。
-
有关 Sphinx 的更多信息,请参阅
www.sphinx-doc.org/en/stable/
。 -
有关 man 页面大纲的更多背景信息,请参阅
en.wikipedia.org/wiki/Man_page
。
在 docstrings 中编写更好的 RST 标记
当我们有一个有用的脚本时,通常需要留下关于它的功能、工作原理以及何时使用的注释。许多用于生成文档的工具,包括 Docutils,都使用 RST 标记。我们可以使用哪些 RST 功能来使文档更易读?
准备工作
在包括描述和文档的方法中,我们看到了将基本的文档放入模块中。这是编写我们的文档的起点。有许多 RST 格式规则。我们将看一些对于创建可读文档很重要的规则。
如何做...
- 一定要写下关键点的大纲。这可能会导致创建 RST 部分标题来组织材料。部分标题是一个两行的段落,标题后面跟着一个下划线,使用
=
,-
,^
,~
或其他 Docutils 字符来划线。
标题将看起来像这样。
Topic
=====
标题文本在一行上,下划线字符在下一行上。这必须被空行包围。下划线字符可以比标题字符多,但不能少。
RST 工具将推断我们使用下划线字符的模式。只要下划线字符一致使用,匹配下划线字符到期望标题的算法将检测到这种模式。这取决于一致性和对部分和子部分的清晰理解。
刚开始时,可以帮助制作一个明确的提醒便条,如下所示:
字符 | 级别 |
---|---|
= | 1 |
- | 2 |
^ | 3 |
~ | 4 |
- 填写各种段落。用空行分隔段落(包括部分标题)。额外的空行不会有害。省略空行将导致 RST 解析器看到一个单一的长段落,这可能不是我们想要的。
我们可以使用内联标记来强调、加重强调、代码、超链接和内联数学等,还有其他一些东西。如果我们打算使用 Sphinx,那么我们将有一个更大的文本角色集合可以使用。我们将很快看到这些技术。
- 如果编程编辑器有拼写检查器,请使用。这可能会令人沮丧,因为我们经常会有包含拼写检查失败的缩写的代码示例。
工作原理...
docutils 转换程序将检查文档,寻找部分和正文元素。一个部分由一个标题标识。下划线用于将部分组织成正确嵌套的层次结构。推断这一点的算法相对简单,并具有以下规则:
-
如果之前已经看到了下划线字符,则已知级别
-
如果之前没有看到下划线字符,则必须缩进到前一个大纲级别的下一级
-
如果没有上一级,这就是第一级
一个正确嵌套的文档可能具有以下下划线字符序列:
====
-----
^^^^^^
^^^^^^
-----
^^^^^^
~~~~~~~~
^^^^^^
我们可以看到,第一个大纲字符=
将是一级。接下来的-
是未知的,但出现在一级之后,所以必须是二级。第三个标题有^
,之前未知,必须是三级。下一个^
仍然是三级。接下来的两个-
和^
分别是二级和三级。
当我们遇到新字符~
时,它位于三级之下,因此必须是四级标题。
注意
从这个概述中,我们可以看到不一致会导致混乱。
如果我们在文档的中途改变主意,这个算法就无法检测到。如果出于莫名其妙的原因,我们决定跳过一个级别并尝试在二级部分内有一个四级标题,那是不可能的。
RST 解析器可以识别几种不同类型的正文元素。我们展示了一些。更完整的列表包括:
-
文本段落:这些可能使用内联标记来强调或突出不同种类的内容。
-
文字块:这些是用
::
引入并缩进空格的。它们也可以用.. parsed-literal::
指令引入。一个 doctest 块缩进四个空格,并包括 Python 的>>>
提示符。 -
列表、表格和块引用:我们稍后会看到这些。这些可以包含其他正文元素。
-
脚注:这些是可以放在页面底部或章节末尾的特殊段落。这些也可以包含其他正文元素。
-
超链接目标、替换定义和 RST 注释:这些是专门的文本项目。
还有更多...
为了完整起见,我们在这里指出,RST 段落之间用空行分隔。RST 比这个核心规则要复杂得多。
在包括描述和文档配方中,我们看了几种不同类型的正文元素:
-
文本段落:这是由空行包围的文本块。在其中,我们可以使用内联标记来强调单词,或者使用字体来显示我们正在引用代码元素。我们将在使用内联标记配方中查看内联标记。
-
列表:这些是以看起来像数字或项目符号开头的段落。对于项目符号,使用简单的
-
或*
。也可以使用其他字符,但这些是常见的。我们可能有这样的段落。
有项目符号会有帮助,因为:
-
它们可以帮助澄清
-
它们可以帮助组织
-
编号列表:有各种被识别的模式。我们可能会使用这样的东西。
四种常见的编号段落:
-
数字后面跟着像
.
或)
这样的标点符号。 -
一个字母后面跟着像
.
或)
这样的标点符号。 -
一个罗马数字后面跟着标点符号。
-
一个特殊情况是使用与前面项目相同的标点符号的
#
。这继续了前面段落的编号。 -
文字块:代码示例必须以文字形式呈现。这个文本必须缩进。我们还需要用
::
前缀代码。::
字符必须是一个单独的段落,或者是代码示例的引导结束。 -
指令:指令是一个段落,通常看起来像
.. directive::
。它可能有一些内容,缩进以便包含在指令内。它可能看起来像这样:
.. important::
Do not flip the bozo bit.
.. important::
段落是指令。这之后是一个缩进在指令内的短段落文字。在这种情况下,它创建了一个包含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.
这对于提供代码示例非常方便,其中代码的某些部分被突出显示。这样的文字就是一个简单的主体元素,里面只能有文本。它不能有列表或其他嵌套结构。
使用内联标记
在段落中,我们可以使用几种内联标记技术:
-
我们可以用
*
将单词或短语括起来以进行*强调*
。 -
我们可以用
**
将单词或短语括起来以进行**强调**
。 -
我们用单个反引号(
`
)包围引用。链接后面带有_
。我们可以用`section title`_
来指代文档中的特定章节。我们通常不需要在 URL 周围放置任何东西。Docutils 工具可以识别这些。有时我们希望显示一个单词或短语,隐藏 URL。我们可以用这个:`the Sphinx documentation <http://www.sphinx-doc.org/en/stable/>`_
。
-
我们可以将代码相关的单词使用两个反引号括起来,使其看起来像:
``code``
还有一种更一般的技术叫做文本角色。角色看起来比简单地用*
字符包装一个单词或短语要复杂一些。我们使用:word:
作为角色名称,后面跟着适用的单词或短语在单个`
反引号中。文本角色看起来像这样:strong:`this`
。
有许多标准角色名称,包括:emphasis:
、:literal:
、:code:
、:math:
、:pep-reference:
、:rfc-reference:
、:strong:
、:subscript:
、:superscript:
和:title-reference:
。其中一些也可以用更简单的标记,如*emphasis*
或**strong**
。其余只能作为显式角色使用。
此外,我们可以使用一个简单的指令定义新角色。如果我们想要进行非常复杂的处理,我们可以为处理角色提供 docutils 的类定义,从而允许我们调整文档处理的方式。Sphinx 添加了大量角色以支持函数、方法、异常、类和模块之间的详细交叉引用。
另请参阅
-
有关 RST 语法的更多信息,请参阅
docutils.sourceforge.net
。其中包括对 docutils 工具的描述。 -
有关Sphinx Python Documentation Generator的信息,请参阅
www.sphinx-doc.org/en/stable/
。 -
Sphinx 工具添加了许多附加指令和文本角色到基本定义中。
设计复杂的 if...elif 链
大多数情况下,我们的脚本会涉及到一系列选择。有时选择很简单,我们可以通过查看代码来判断设计的质量。在其他情况下,选择更加复杂,很难确定我们的 if 语句是否正确设计以处理所有条件。
在最简单的情况下,我们有一个条件,C,和它的反义,C。这是if...else
语句的两个条件。一个条件,¬C,在if
子句中说明,另一个在else
中暗示。
在本解释中,我们将使用 p ∨ q 表示 Python 的OR运算符。我们可以称这两个条件为完整,因为:
C ∨ C = ¬ T
我们称之为完全,因为没有其他条件可以存在。没有第三个选择。这就是排中律。这也是else
子句背后的操作原则。if
语句体被执行或else
语句被执行。没有第三个选择。
在实际编程中,我们经常有复杂的选择。我们可能有一组条件,C = {C[1],C[2],C[3],...,C[n]}。
我们不希望简单地假设:
C[1] ∨ C[2] ∨ C[3] ∨ ... ∨ C[n] = T
我们可以使用 来表示与any(C)
类似的含义,或者any([C_1, C_2, C_3, ..., C_n])
。我们需要证明 ;我们不能假设这是true
。
下面是可能出错的情况——我们可能错过了一些条件,C[n+1],在逻辑混乱中丢失了。错过这个将意味着我们的程序将无法正确处理此案例。
我们如何确定我们没有漏掉什么?
准备就绪
让我们看一个具体的例子,一个if...elif
链。在Craps赌场游戏中,有一些适用于两个骰子的投掷的规则。这些规则适用于游戏的第一次投掷,称为come out投掷:
-
2,3 或 12,是Craps,这对所有在通过线上下的所有赌注来说都是一个损失
-
7 或 11 对所有放在通过线上的赌注都是赢家
-
剩余数字确定了一个点
许多玩家把他们的赌注放在通过线上。还有一个don't pass线,这个线不常用。我们将使用这三个条件集作为例子来查看这个方法,因为它有一个可能模糊的子句。
如何做...
当我们写一个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)
- 添加一个引发异常的
else
子句,就像这样:
else:
raise Exception('Design Problem Here: not all conditions accounted for')
这个额外的 else
崩溃条件给了我们一种积极识别逻辑问题的方法。我们可以确信,我们所犯的任何错误都将导致一个引人注目的问题。
工作原理...
我们的目标是确保我们的程序始终正常运行。尽管测试有所帮助,但我们仍然可能在设计和测试用例中有错误的假设。
尽管严谨的逻辑是必不可少的,我们仍然可能犯错。此外,其他人可能尝试调整我们的代码并引入错误。更尴尬的是,我们可能对自己的代码进行更改导致程序崩溃。
else
崩溃选项迫使我们对每个条件都要明确。不做任何假设。正如我们之前指出的,我们逻辑中的任何错误都将在引发异常时被发现。
else
崩溃选项对性能影响不大。一个简单的 else
子句比带有条件的 elif
子句稍快一些。如果我们认为我们的应用程序性能在任何方面取决于单个表达式的成本,那么我们有更严重的设计问题要解决。评估单个表达式的成本很少是算法中最昂贵的部分。
在设计问题存在的情况下,遇到异常崩溃是一个明智的行为。按照写入警告消息到日志的设计模式并没有太多意义。如果存在这种逻辑漏洞,程序就已经严重出错了,发现问题后尽快找到并修复是很重要的。
还有更多...
在许多情况下,我们可以通过检查程序处理的某个点的期望后置条件来推导出一个 if...elif...elif
链。例如,我们可能需要一个陈述来建立像 m 是 a 或 b 中较大的一个这样简单的事情。
(为了通过逻辑,我们将避免 m = max(a, b)
。)
我们可以这样形式化最终条件:
(m = a ∨ m = b) ∧ *m > a * ∧ m > b
我们可以通过将目标写成一个断言语句来从最终条件开始逆向工作:
# do something
assert (m = a or m = b) and m > a and m > b
一旦我们陈述了目标,我们就可以确定导致该目标的陈述。显然,像 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的语法问题。参见
en.wikipedia.org/wiki/Dangling_else
。 -
Python 的缩进消除了悬挂 else 语法问题。它并没有解决在复杂的
if...elif...elif
链中确保所有条件都得到适当处理的语义问题。
设计一个正确终止的 while 语句
大多数情况下,Python 的for
语句提供了我们需要的所有迭代控制。在许多情况下,我们可以使用内置函数如map()
,filter()
和reduce()
来处理数据集合。
然而,有一些情况我们需要使用while
语句。其中一些情况涉及到我们无法创建适当的迭代器来遍历项目的数据结构。其他情况涉及与人类用户的交互,我们在从这些人那里得到输入之前没有数据。
准备工作
假设我们要提示用户输入密码。我们将使用getpass
模块,以便没有回显。
此外,为了确保他们已经正确输入了密码,我们将要求他们输入两次并比较结果。这是一个简单的for
语句不会很好地处理的情况。它可以被迫服役,但结果代码看起来很奇怪:for
语句有一个显式的上限;提示用户输入实际上没有一个上限。
如何做……
我们将介绍一个六步流程,概述了设计这种迭代算法核心的内容。这是当一个简单的for
语句不能解决我们的问题时需要做的事情。
- 定义完成。在我们的情况下,我们将有两份密码,
password_text
和confirming_password_text
。循环后必须为true
的条件是password_text == confirming_password_text
。理想情况下,从人们(或文件)那里读取是一个有界的活动。最终,人们会输入匹配的值对。在他们输入匹配的值对之前,我们将无限迭代。
还有其他边界条件。例如,文件结束。或者我们允许人返回到先前的提示。一般来说,我们在 Python 中用异常处理这些其他条件。
当然,我们可以将这些额外条件添加到我们的完成定义中。我们可能需要一个复杂的终止条件,例如文件结尾或password_text == confirming_password_text
。
在这个例子中,我们将选择异常处理,并假设将使用try:
块。只在终止条件中有一个单一子句大大简化了设计。
我们可以这样勾画出循环的大致情况:
# initialize something
while # not terminated:
# do something
assert password_text == confirming_password_text
我们将我们的“完成定义”写成了最后的assert
语句。我们已经为之后的迭代包含了注释,我们将在后续步骤中填写。
- 定义一个在循环迭代时为
true
的条件。这被称为不变量,因为在循环处理的开始和结束时它总是true
。通常通过泛化后置条件或引入另一个变量来创建它。
当从人(或文件)那里读取时,我们有一个隐含的状态改变,这是不变量的重要部分。我们可以称之为状态改变中的获取下一个输入。我们经常必须清楚地表达,我们的循环将从输入流中获取下一个值。
我们必须确保我们的循环能够正确获取下一个项目,尽管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
- 定义离开循环的条件。我们需要确保这个条件取决于不变量为
true
。我们还需要确保,当这个终止条件最终为false
时,目标状态将变为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
- 定义初始化,确保不变量为
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
- 编写循环体,将不变量重置为
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
-
确定一个时钟——一个单调递减的函数,显示每次循环确实朝着终止条件取得进展。
当从人那里收集输入时,我们被迫做一个假设——最终他们会输入匹配的对。每次循环都使我们离匹配对更近一步。为了正确形式化,我们可以假设在它们匹配之前会有n个输入;我们必须展示每次循环减少剩余数量。
在复杂情况下,我们可能需要将用户的输入视为值列表。对于我们的示例,我们会将用户输入视为一系列对:[(p[1] , q[1] ),(p[2] , q[2] ),(p[3] , q[3] ),...,(p[n] , q[n] )]。通过有限的列表,我们可以更容易地推断我们的循环是否真正朝着完成进展。
因为我们基于目标最终
条件构建了循环,所以我们可以绝对确定它做了我们想要的事情。如果我们的逻辑是合理的,循环将终止,并且将以预期的结果终止。这是所有编程的目标——让机器在给定一些初始状态的情况下达到期望的状态。
移除一些注释后,我们得到了我们的最终循环:
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
我们将最终的后置条件保留为一个assert
语句。对于复杂的循环,它既是一个内置测试,也是一个解释循环工作原理的注释。
这个设计过程通常会产生一个看起来类似于基于直觉开发的循环。有逐步证明直觉设计的没什么问题。一旦我们这样做了几次,我们就可以更有信心地使用循环,因为我们可以证明设计是合理的。
在这种情况下,循环体和初始化碰巧是相同的代码。如果这是一个问题,我们可以定义一个小两行的函数来避免重复代码。我们将在第三章函数定义中讨论这个问题。
它的工作原理...
我们首先明确循环的目标。我们所做的一切都将确保编写的代码导致该目标条件。实际上,这就是所有软件设计背后的动机——我们始终试图编写导致给定目标状态的最少语句。我们通常是反向从目标到初始化。推理链中的每一步实质上都是陈述了某个语句S
的最弱前置条件,该语句导致我们期望的结果条件。
鉴于后置条件,我们试图解决一个语句和一个前置条件。我们总是在构建这个模式:
assert pre-condition
S
assert post-condition
后置条件是我们的完成定义。我们需要假设一个导致完成的语句,S
,以及该语句的前置条件。总是存在无限数量的替代语句;我们专注于最弱的前置条件——假设最少的那个。
在某个时刻——通常是在编写初始化语句时——我们发现前置条件仅仅是true
:任何初始状态都可以作为语句的前置条件。这就是我们知道我们的程序可以从任何初始状态开始并按预期完成的方式。这是理想的。
在设计while
语句时,我们在语句体内有一个嵌套的上下文。语句体应始终处于将不变条件重新设置为true
的过程中。在我们的例子中,这意味着从用户那里读取更多输入。在其他例子中,我们可能正在处理字符串中的另一个字符,或者从一组数字中取另一个数字。
我们需要证明当不变量为true
且循环条件为false
时,我们的最终目标已经实现。当我们从最终目标出发并根据该最终目标创建不变量和循环条件时,这个证明会更容易。
重要的是要耐心地完成每一步,以确保我们的推理是坚实的。我们需要能够证明循环将正常工作。然后我们可以有信心地运行单元测试。
另请参阅
-
我们在避免使用 break 语句可能导致的问题配方中查看了高级循环设计的其他方面。
-
我们在设计复杂的 if...elif 链配方中也研究了这个概念。
-
关于这个话题的经典文章是由大卫·格里斯撰写的一篇论文,关于发展循环不变量和循环的标准策略的注释。参见
www.sciencedirect.com/science/article/pii/0167642383900151
。 -
算法设计是一个大的主题。一本很好的介绍是由斯基耐纳撰写的 算法设计手册。参见
www3.cs.stonybrook.edu/~algorith/
。
避免使用 break 语句可能导致的问题
理解for
语句的常见方式是它创建了一个对于所有的条件。在语句结束时,我们可以断言,在集合中的所有项目都进行了一些处理。
这并不是for
语句的唯一含义。当我们在for
的主体内引入break
语句时,我们将语义更改为存在。当break
语句离开for
(或while
)语句时,我们只能断言至少存在一个导致语句结束的项目。
这里有一个次要问题。如果循环在不执行break
的情况下结束了怎么办?我们被迫断言不存在即使一个触发了break
的项目。德摩根定律告诉我们,不存在的条件可以重新陈述为对于所有的条件:¬∃[x]B(x) ≡ ∀[x]¬ B(x)。在这个公式中,B(x) 是包括break
的if
语句的条件。如果我们从未找到 B(x),那么对于所有的项目,¬ B(x) 都是true
。这显示了典型的对于所有循环和包括break
的存在循环之间的一些对称性。
离开for
或while
语句时为true
的条件可能是模糊的。它是正常结束的吗?它执行了break
吗?我们不能轻易地判断,所以我们将提供一套给出一些设计指导的配方。
当我们有多个带有各自条件的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 语句配方中指出的,每个语句都建立了一个后置条件。在设计循环时,我们需要表达这个条件。在这种情况下,我们没有正确表达后置条件。
理想情况下,后置条件应该是像text[position] in '=:'
这样简单的东西。但是,如果给定的文本中没有=
或:
,简单的后置条件就没有逻辑意义。当没有任何符合条件的字符存在时,我们无法对不在那里的字符的位置做出任何断言。
- 写出明显的后置条件。我们有时称之为幸运路径条件,因为当没有发生任何异常情况时,它是
true
的。
text[position] in '=:'
-
为边界情况添加后置条件。在这个例子中,我们有两个额外的条件:
-
没有
=
或:
。 -
根本没有字符。
len()
为零,循环实际上什么也没做。在这种情况下,位置变量将永远不会被创建。
-
(len(text) == 0
or not('=' in text or ':' in text)
or text[position] in '=:')
-
如果正在使用
while
语句,请考虑重新设计为具有完成条件。这可以消除对break
语句的需要。 -
如果正在使用
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
在for
后的语句中,我们已经明确列出了所有的终止条件。最终输出,name= name_only value= None
,确认我们已经正确处理了示例文本。
运作原理...
这种方法迫使我们仔细计算后置条件,以确保我们绝对确定知道循环终止的所有原因。
在更复杂的循环中——具有多个break
语句——后置条件可能很难完全计算出来。循环的后置条件必须包括离开循环的所有原因——正常原因以及所有的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)
这个版本基于先前评估的完整后置条件向前推进了一些处理。这种重构很常见。
思路是放弃任何假设或直觉。稍加纪律,我们可以确定任何语句的后置条件。
实际上,我们思考后置条件的次数越多,我们的软件就可以越精确。关于我们软件目标的目标一定要明确,并通过选择使目标变为true
的最简单的语句来逆向工作。
还有更多...
我们也可以在for
语句上使用else
子句来确定循环是否正常结束或执行了break
语句。我们可以像这样使用:
We can also use an else clause on a for statement to determine if the loop finished normally or a break statement was executed. We can use something like this:
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 撰写的,关于开发循环不变量和循环的标准策略的注释。参见
www.sciencedirect.com/science/article/pii/0167642383900151
。
利用异常匹配规则
try
语句让我们捕获异常。当异常被引发时,我们有多种处理方式:
-
忽略它:如果我们什么都不做,程序会停止。我们可以通过两种方式实现这一点——首先不使用
try
语句,或者在try
语句中没有匹配的except
子句。 -
记录日志:我们可以写一条消息并让其传播;通常这会导致程序停止。
-
从中恢复:我们可以编写一个
except
子句来执行一些恢复操作,以撤消在try
子句中部分完成的操作的影响。我们可以进一步将try
语句包装在while
语句中,并持续重试直到成功。 -
忽略它:如果我们什么都不做(即
pass
),那么在try
语句之后会恢复处理。这会消除异常。 -
重写它:我们可以引发一个不同的异常。原始异常成为新引发异常的上下文。
-
链式处理:我们将一个不同的异常链接到原始异常。我们将在使用
raise from
语句链接异常这个示例中看到这一点。
嵌套上下文怎么办?在这种情况下,内部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
对象。我们只能希望 Python 3.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
语句?
如何做到这一点...
- 在
try
块中缩进写入我们想要使用的代码:
try:
shutil.copy( str(source_file_path), str(target_file_path) )
- 先包括最具体的异常类。在这种情况下,我们针对具体的
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) )
- 包括稍后的更一般的异常:
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)
我们先匹配最具体的异常,然后再匹配更通用的异常。
我们通过创建缺失的目录来处理`FileNotFoundError`,然后再次执行`copy()`,知道现在它会正常工作。
我们消除了其他任何`OSError`类的异常。例如,如果有权限问题,那么该错误将被简单地记录。我们的目标是尝试复制所有文件。任何导致问题的文件都将被记录,但复制过程将继续。
它是如何工作的...
Python 的异常匹配规则旨在保持简单:
-
按顺序处理
except
子句 -
将实际异常与异常类(或异常类元组)进行匹配。匹配意味着实际异常对象(或异常对象的任何基类)是
except
子句中给定类的对象。
这些规则说明了我们为什么要先放置最具体的异常类,然后是更一般的异常类。像Exception
这样的通用异常类几乎匹配每一种类型的异常。我们不希望它首先出现,因为不会检查任何其他子句。我们必须始终将通用异常放在最后。
还有一个更通用的类,BaseException
类。没有好理由来处理这个类的异常。如果我们这样做,我们将捕获SystemExit
和KeyboardInterrupt
异常,这会干扰杀死表现不良应用程序的能力。我们仅在定义存在于正常异常层次结构之外的新异常类时才使用BaseException
类作为超类。
还有更多...
我们的示例包括一个嵌套上下文,在其中可能引发第二个异常。考虑到这个except
子句:
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:
子句。如果我们对尝试处理的异常不谨慎,还有一些其他错误可能会发生。
这个示例将展示一些我们可以避免的常见异常处理错误。
准备就绪
在避免使用except:
子句可能会出现的问题的示例中,我们看到了在设计异常处理时的一些考虑因素。在那个示例中,我们不建议使用BaseException
,因为我们可能会干扰停止行为异常的 Python 程序。
我们将在这个示例中扩展不应该做什么的想法。
如何做...
使用except Exception:
作为最通用的异常管理方式。
处理太多异常可能会干扰我们停止异常的 Python 程序的能力。当我们按下Ctrl + C,或通过kill -2
发送SIGINT
信号时,我们通常希望程序停止。我们很少希望程序写一条消息并继续运行,或者完全停止响应。
还有一些其他类别的异常,我们应该警惕尝试处理:
-
SystemError
-
RuntimeError
-
MemoryError
通常,这些异常意味着 Python 内部某处出现了问题。与其消除这些异常,或尝试进行一些恢复,我们应该允许程序失败,找出根本原因并修复它。
它是如何工作的...
有两种技术我们应该避免:
-
不要捕获
BaseException
类 -
不要使用
except:
而不指定异常类。这会匹配所有异常;这将包括我们应该避免尝试处理的异常。
使用except BaseException
或不指定具体类的except
可能会导致程序在我们需要停止它的时候变得无响应。
此外,如果我们捕获了其中任何异常,我们可能会干扰这些内部异常的处理方式:
-
SystemExit
-
KeyboardInterrupt
-
GeneratorExit
如果我们静默、包装或重写其中任何一个,我们可能已经制造了一个本不存在的问题。我们可能已经将一个简单的问题加剧成一个更大更神秘的问题。
注意
编写一个从不崩溃的程序是一种高贵的愿望。干扰 Python 的一些内部异常不会创建一个更可靠的程序。相反,它会创建一个清晰的失败被掩盖并成为模糊的神秘的程序。
另请参见
-
在利用异常匹配规则食谱中,我们将探讨设计异常时的一些考虑因素。
-
在使用
raise from
语句链接异常食谱中,我们将看看如何链接异常,使得单一异常类包装不同的详细异常。
使用raise from
语句链接异常
在某些情况下,我们可能希望将一些看似不相关的异常合并为一个单一的通用异常。一个复杂的模块通常定义一个适用于模块内部可能出现的许多情况的单一通用Error
异常。
大多数情况下,通用异常就足够了。如果引发了模块的Error
,则说明某些地方出了问题。
较少情况下,我们希望获取详细信息以进行调试或监视目的。我们可能希望将它们写入日志,或将详细信息包含在电子邮件中。在这种情况下,我们需要提供支持详细信息,以放大或扩展通用异常。我们可以通过从通用异常链接到根本原因异常来做到这一点。
准备就绪
假设我们正在编写一些复杂的字符串处理。我们希望将许多不同类型的详细异常视为单个通用错误,以使我们软件的用户免受实现细节的影响。我们可以将详细信息附加到通用错误。
如何做...
- 要创建一个新的异常,我们可以这样做:
class Error(Exception):
pass
这就足以定义一个新的异常类。
- 在处理异常时,我们可以使用
raise from
语句将它们链接起来,就像这样:
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
在第一个`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 <module>
'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 <module>
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__
的值可能会被写入调试日志而不是显示给用户。
还有更多...
如果在异常处理程序内部引发异常,这也会创建一种链接的异常关系。这是上下文关系而不是原因关系。
上下文消息看起来相似。消息略有不同。它说在处理上述异常时,发生了另一个异常:
。第一个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
语句的上下文。
如何做...
- 通过打开文件或使用
urllib.request.urlopen()
创建网络连接来创建上下文。其他常见的上下文包括zip
文件和tar
文件:
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
- 包括所有处理,缩进在
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)
- 当我们将文件作为上下文管理器使用时,文件将在缩进的上下文块结束时自动关闭。即使引发异常,文件仍会被正确关闭。将在上下文完成并释放资源后执行的处理内容缩进:
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)
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
上下文是获取和释放锁的理想方式。请参阅docs.python.org/3/library/threading.html#with-locks
了解由threading
模块创建的锁对象与上下文管理器之间的关系。
另请参阅
- 请参阅
www.python.org/dev/peps/pep-0343/
了解 with 语句的起源
第三章:函数定义
在本章中,我们将看一下以下配方:
-
设计带有可选参数的函数
-
使用超灵活的关键字参数
-
使用*分隔符强制关键字参数
-
在函数参数上写明确的类型
-
基于部分函数选择参数顺序
-
使用 RST 标记编写清晰的文档字符串
-
围绕 Python 的堆栈限制设计递归函数
-
使用脚本库开关编写可重用脚本
介绍
函数定义是将一个大问题分解为较小问题的一种方式。数学家们已经做了几个世纪了。这也是将我们的 Python 编程打包成智力可管理的块的一种方式。
在这些配方中,我们将看一些函数定义技术。这将包括处理灵活参数的方法以及根据一些更高级别的设计原则组织参数的方法。
我们还将看一下 Python 3.5 的 typing 模块以及如何为我们的函数创建更正式的注释。我们可以开始使用mypy
项目,以对数据类型的使用进行更正式的断言。
设计带有可选参数的函数
当我们定义一个函数时,通常需要可选参数。这使我们能够编写更灵活的函数,并且可以在更多情况下使用。
我们也可以将这看作是创建一系列密切相关函数的一种方式,每个函数具有略有不同的参数集合 - 称为签名 - 但都共享相同的简单名称。许多函数共享相同的名称的想法可能有点令人困惑。因此,我们将更多地关注可选参数的概念。
可选参数的一个示例是int()
函数。它有两种形式:
-
int(str)
: 例如,int('355')
的值为355
。在这种情况下,我们没有为可选的base
参数提供值;使用了默认值10
。 -
int(str, base)
:例如,int('0x163', 16)
的值是355
。在这种情况下,我们为base
参数提供了一个值。
准备工作
许多游戏依赖于骰子的集合。赌场游戏Craps使用两个骰子。像Zilch(或Greed或Ten Thousand)这样的游戏使用六个骰子。游戏的变体可能使用更多。
拥有一个可以处理所有这些变化的掷骰子函数非常方便。我们如何编写一个骰子模拟器,可以处理任意数量的骰子,但是将使用两个作为方便的默认值?
如何做...
我们有两种方法来设计带有可选参数的函数:
-
一般到特定:我们首先设计最一般的解决方案,并为最常见的情况提供方便的默认值。
-
特定到一般:我们首先设计几个相关的函数。然后将它们合并为一个涵盖所有情况的一般函数,将原始函数中的一个单独出来作为默认行为。
从特定到一般的设计
在遵循特定到一般策略时,我们将设计几个单独的函数,并寻找共同的特征:
- 编写函数的一个版本。我们将从Craps游戏开始,因为它似乎最简单:
**>>> import random**
**>>> def die():**
**... return random.randint(1,6)**
**>>> def craps():**
**... return (die(), die())**
我们定义了一个方便的辅助函数die()
,它封装了有时被称为标准骰子的基本事实。有五个可以使用的立体几何体,可以产生四面体、六面体、八面体、十二面体和二十面体骰子。六面骰子有着悠久的历史,最初是作为骰子骨头,很容易修剪成六面立方体。
这是底层die()
函数的一个示例:
**>>> random.seed(113)**
**>>> die(), die()**
**(1, 6)**
我们掷了两个骰子,以展示值如何组合以掷更大堆的骰子。
我们的Craps游戏函数看起来是这样的:
**>>> craps()**
**(6, 3)**
**>>> craps()**
**(1, 4)**
这显示了Craps游戏的一些两个骰子投掷。
- 编写函数的另一个版本:
**>>> def zonk():**
**... return tuple(die() for x in range(6))**
我们使用了一个生成器表达式来创建一个有六个骰子的元组对象。我们将在第八章中深入研究生成器表达式,函数式和反应式编程特性。
我们的生成器表达式有一个变量x
,它被忽略了。通常也可以看到这样写成tuple(die() for _ in range(6))
。变量_
是一个有效的 Python 变量名;这个名字可以作为一个提示,表明我们永远不想看到这个变量的值。
这是使用zonk()
函数的一个例子:
**>>> zonk()**
**(5, 3, 2, 4, 1, 1)**
这显示了六个单独骰子的结果。有一个短顺(1-5)以及一对一。在游戏的某些版本中,这是一个很好的得分手。
- 找出两个函数中的共同特征。这可能需要对各种函数进行一些重写,以找到一个共同的设计。在许多情况下,我们最终会引入额外的变量来替换常数或其他假设。
在这种情况下,我们可以将两元组的创建概括化。我们可以引入一个基于range(2)
的生成器表达式,它将两次评估die()
函数:
**>>> def craps():**
**... return tuple(die() for x in range(2))**
这似乎比解决特定的两个骰子问题需要更多的代码。从长远来看,使用一个通用函数意味着我们可以消除许多特定的函数。
- 合并这两个函数。这通常涉及到暴露一个之前是常数或其他硬编码假设的变量:
**>>> def dice(n):**
**... return tuple(die() for x in range(n))**
这提供了一个通用函数,涵盖了Craps和Zonk的需求:
**>>> dice(2)**
**(3, 2)**
**>>> dice(6)**
**(5, 3, 4, 3, 3, 4)**
- 确定最常见的用例,并将其作为引入的任何参数的默认值。如果我们最常见的模拟是Craps,我们可能会这样做:
**>>> def dice(n=2):**
**... return tuple(die() for x in range(n))**
现在我们可以简单地在Craps中使用dice()
。我们需要在Zonk中使用dice(6)
。
从一般到特殊的设计
在遵循从一般到特殊的策略时,我们会首先确定所有的需求。我们通常会通过在需求中引入变量来做到这一点:
- 总结掷骰子的需求。我们可能有一个像这样的列表:
-
Craps:两个骰子。
-
Zonk中的第一次掷骰子:六个骰子。
-
Zonk中的后续掷骰子:一到六个骰子。
这个需求列表显示了掷n个骰子的一个共同主题。
- 用一个显式参数重写需求,代替任何字面值。我们将用参数n替换所有的数字,并展示我们引入的这个新参数的值:
-
Craps:n个骰子,其中n=2。
-
Zonk中的第一次掷骰子:n个骰子,其中n=6。
-
Zonk中的后续掷骰子:n个骰子,其中1≤n≤6。
这里的目标是确保所有的变化确实有一个共同的抽象。在更复杂的问题中,看似相似的东西可能没有一个共同的规范。
我们还希望确保我们已经正确地对各种函数进行了参数化。在更复杂的情况下,我们可能有一些不需要被参数化的值;它们可以保持为常数。
- 编写符合一般模式的函数:
**>>> def dice(n):**
**... return (die() for x in range(n))**
在第三种情况下——Zonk中的后续掷骰子——我们确定了一个1≤n≤6的约束。我们需要确定这是否是我们dice()
函数的约束,还是这个约束是由使用dice
函数的模拟应用所施加的。
在这种情况下,约束是不完整的。Zonk的规则要求没有被掷动的骰子形成某种得分模式。约束不仅仅是骰子的数量在一到六之间;约束与游戏状态有关。似乎没有充分的理由将dice()
函数与游戏状态联系起来。
- 为最常见的用例提供一个默认值。如果我们最常见的模拟是Craps,我们可能会这样做:
**>>> def dice(n=2):**
**... return tuple(die() for x in range(n))**
现在我们可以简单地在Craps中使用dice()
。我们需要在Zonk中使用dice(6)
。
工作原理...
Python 提供参数值的规则非常灵活。有几种方法可以确保每个参数都有一个值。我们可以将其视为以下方式工作:
-
将每个参数设置为任何提供的默认值。
-
对于没有名称的参数,参数值是按位置分配给参数的。
-
对于具有名称的参数,例如
dice(n=2)
,参数值是使用名称分配的。通过位置和名称同时分配参数是错误的。 -
如果任何参数没有值,这是一个错误。
这些规则允许我们根据需要提供默认值。它们还允许我们混合位置值和命名值。默认值的存在是使参数可选的原因。
可选参数的使用源于两个考虑因素:
-
我们可以对处理进行参数化吗?
-
该参数的最常见参数值是什么?
在流程定义中引入参数可能是具有挑战性的。在某些情况下,有代码可以帮助我们用参数替换文字值(例如 2 或 6)。
然而,在某些情况下,文字值不需要被参数替换。它可以保留为文字值。我们并不总是想用参数替换每个文字值。例如,我们的die()
函数有一个文字值为 6,因为我们只对标准的立方骰子感兴趣。这不是一个参数,因为我们不认为有必要制作更一般的骰子。
还有更多...
如果我们想非常彻底,我们可以编写专门的版本函数,这些函数是我们更通用的函数的专门版本。这些函数可以简化应用程序:
**>>> def craps():**
**... return dice(2)**
**>>> def zonk():**
**... return dice(6)**
我们的应用程序功能-craps()
和zonk()
-依赖于一个通用函数dice()
。这又依赖于另一个函数die()
。我们将在基于部分函数选择参数顺序食谱中重新讨论这个想法。
这个依赖堆栈中的每一层都引入了一个方便的抽象,使我们不必理解太多细节。这种分层抽象的想法有时被称为chunking。这是一种通过隔离细节来管理复杂性的方法。
这种设计模式的常见扩展是在这个函数层次结构中的多个级别提供参数。如果我们想要对die()
函数进行参数化,我们将为dice()
和die()
提供参数。
对于这种更复杂的参数化,我们需要在我们的层次结构中引入更多具有默认值的参数。我们将从die()
中添加一个参数开始。这个参数必须有一个默认值,这样我们就不会破坏我们现有的测试用例:
**>>> def die(sides=6):**
**... return random.randint(1,6)**
在引入这个参数到抽象堆栈的底部之后,我们需要将这个参数提供给更高级别的函数:
**>>> def dice(n=2, sides=6):**
**... return tuple(die(sides) for x in range(n))**
我们现在有很多种使用dice()
函数的方法:
-
所有默认值:
dice()
很好地覆盖了Craps。 -
所有位置参数:
dice(6, 6)
将覆盖Zonk。 -
位置和命名参数的混合:位置值必须首先提供,因为顺序很重要。例如,
dice(2, sides=8)
将覆盖使用两个八面体骰子的游戏。 -
所有命名参数:
dice(sides=4, n=4)
这将处理我们需要模拟掷四个四面体骰子的情况。在使用所有命名参数时,顺序并不重要。
在这个例子中,我们的函数堆栈只有两层。在更复杂的应用程序中,我们可能需要在层次结构的许多层中引入参数。
另请参阅
-
我们将在基于部分函数选择参数顺序食谱中扩展一些这些想法。
-
我们使用了涉及不可变对象的可选参数。在这个配方中,我们专注于数字。在第四章中,内置数据结构-列表、集合、字典,我们将研究可变对象,它们具有可以更改的内部状态。在避免函数参数的可变默认值配方中,我们将研究一些重要的额外考虑因素,这些因素对于设计具有可变对象的可选值的函数非常重要。
使用超级灵活的关键字参数
一些设计问题涉及解决一个未知的简单方程,给定足够的已知值。例如,速率、时间和距离之间有一个简单的线性关系。我们可以解决任何一个,只要知道另外两个。以下是我们可以用作示例的三条规则:
-
d = r × t
-
r = d / t
-
t = d / r
在设计电路时,例如,基于欧姆定律使用了一组类似的方程。在这种情况下,方程将电阻、电流和电压联系在一起。
在某些情况下,我们希望提供一个简单、高性能的软件实现,可以根据已知和未知的情况执行三种不同的计算中的任何一种。我们不想使用通用的代数框架;我们想将三个解决方案捆绑到一个简单、高效的函数中。
准备工作
我们将构建一个单一函数,可以通过体现任意两个已知值的三个解来解决速率-时间-距离(RTD)计算。通过微小的变量名称更改,这适用于令人惊讶的许多现实世界问题。
这里有一个技巧。我们不一定想要一个单一的值答案。我们可以通过创建一个包含三个值的小 Python 字典来稍微概括这一点。我们将在第四章中更多地了解字典。
当出现问题时,我们将使用warnings
模块而不是引发异常:
**>>> import warnings**
有时,产生一个有疑问的结果比停止处理更有帮助。
如何做...
解出每个未知数的方程。我们先前已经展示了这一点,例如d = r * t,RTD 计算:
- 这导致了三个单独的表达式:
-
距离=速率*时间
-
速率=距离/时间
-
时间=距离/速率
- 根据一个值为
None
时未知的情况,将每个表达式包装在一个if
语句中:
if distance is None:
distance = rate * time
elif rate is None:
rate = distance / time
elif time is None:
time = distance / rate
- 参考第二章中的设计复杂的 if...elif 链,语句和语法,以指导设计这些复杂的
if...elif
链。包括else
崩溃选项的变体:
else:
warnings.warning( "Nothing to solve for" )
- 构建生成的字典对象。在简单情况下,我们可以使用
vars()
函数简单地将所有本地变量作为生成的字典发出。在某些情况下,我们可能有一些本地变量不想包括;在这种情况下,我们需要显式构建字典:
return dict(distance=distance, rate=rate, time=time)
- 使用关键字参数将所有这些包装为一个函数:
def rtd(distance=None, rate=None, time=None):
if distance is None:
distance = rate * time
elif rate is None:
rate = distance / time
elif time is None:
time = distance / rate
else:
warnings.warning( "Nothing to solve for" )
return dict(distance=distance, rate=rate, time=time)
我们可以像这样使用生成的函数:
**>>> def rtd(distance=None, rate=None, time=None):
... if distance is None:
... distance = rate * time
... elif rate is None:
... rate = distance / time
... elif time is None:
... time = distance / rate
... else:
... warnings.warning( "Nothing to solve for" )
... return dict(distance=distance, rate=rate, time=time)
>>> rtd(distance=31.2, rate=6)
{'distance': 31.2, 'time': 5.2, 'rate': 6}**
这告诉我们,以 6 节的速率行驶 31.2 海里将需要 5.2 小时。
为了得到格式良好的输出,我们可以这样做:
**>>> result= rtd(distance=31.2, rate=6)**
**>>> ('At {rate}kt, it takes '**
**... '{time}hrs to cover {distance}nm').format_map(result)**
**'At 6kt, it takes 5.2hrs to cover 31.2nm'**
为了打破长字符串,我们使用了第二章中的设计复杂的 if...elif 链。
工作原理...
因为我们为所有参数提供了默认值,所以我们可以为三个参数中的两个提供参数值,然后函数就可以解决第三个参数。这样可以避免我们编写三个单独的函数。
将字典作为最终结果返回并不是必要的。这只是方便。它允许我们无论提供了哪些参数值,都有一个统一的结果。
还有更多...
我们有另一种表述,涉及更多的灵活性。Python 函数有一个所有其他关键字参数,前缀为**
。通常显示如下:
def rtd2(distance, rate, time, **keywords):
print(keywords)
任何额外的关键字参数都会被收集到提供给**keywords
参数的字典中。然后我们可以用额外的参数调用这个函数。像这样评估这个函数:
rtd2(rate=6, time=6.75, something_else=60)
然后我们会看到keywords
参数的值是一个带有{'something_else': 60}
值的字典对象。然后我们可以对这个结构使用普通的字典处理技术。这个字典中的键和值是在函数被评估时提供的名称和值。
我们可以利用这一点,并坚持要求所有参数都提供关键字:
def rtd2(**keywords):
rate= keywords.get('rate', None)
time= keywords.get('time', None)
distance= keywords.get('distance', None)
etc.
这个版本使用字典get()
方法在字典中查找给定的键。如果键不存在,则提供None
的默认值。
(返回None
的默认值是get()
方法的默认行为。我们的示例包含一些冗余,以阐明处理过程。对于一些非常复杂的情况,我们可能有除None
之外的默认值。)
这有可能具有稍微更灵活的优势。它可能的缺点是使实际参数名称非常难以辨别。
我们可以遵循使用 RST 标记编写清晰文档字符串的配方,并提供一个良好的文档字符串。然而,通过文档隐式地提供参数名称似乎更好一些。
另请参阅
- 我们将查看使用 RST 标记编写清晰文档字符串配方中函数的文档
使用*分隔符强制使用关键字参数
有些情况下,我们需要将大量的位置参数传递给函数。也许我们遵循了设计具有可选参数的函数的配方,这导致我们设计了一个参数如此之多的函数,以至于变得令人困惑。
从实用的角度来看,一个具有超过三个参数的函数可能会令人困惑。大量的传统数学似乎集中在一个和两个参数函数上。似乎没有太多常见的数学运算符涉及三个或更多的操作数。
当难以记住参数的所需顺序时,参数太多了。
准备工作
我们将查看一个具有大量参数的函数。我们将使用一个准备风冷表并将数据写入 CSV 格式输出文件的函数。
我们需要提供一系列温度、一系列风速以及我们想要创建的文件的信息。这是很多参数。
基本公式是这样的:
T[wc] ( T[a], V* ) = 13.12 + 0.6215 T[a] - 11.37 V ^(0.16) + 0.3965 T[a] V ^(0.16)
风冷温度,T[wc],基于空气温度,T[a],以摄氏度为单位,以及风速,V,以 KPH 为单位。
对于美国人来说,这需要一些转换:
-
从°F 转换为°C:C = 5( F -32) / 9
-
将风速从 MPH,V[m],转换为 KPH,V[k]:V[k] = V[m] × 1.609344
-
结果需要从°C 转换回°F:F = 32 + C (9/5)
我们不会将这些纳入这个解决方案。我们将把这留给读者作为一个练习。
创建风冷表的一种方法是创建类似于这样的东西:
import pathlib
def Twc(T, V):
return 13.12 + 0.6215*T - 11.37*V**0.16 + 0.3965*T*V**0.16
def wind_chill(start_T, stop_T, step_T,
start_V, stop_V, step_V, path):
"""Wind Chill Table."""
with path.open('w', newline='') as target:
writer= csv.writer(target)
heading = [None]+list(range(start_T, stop_T, step_T))
writer.writerow(heading)
for V in range(start_V, stop_V, step_V):
row = [V] + [Twc(T, V)
for T in range(start_T, stop_T, step_T)]
writer.writerow(row)
我们使用with
上下文打开了一个输出文件。这遵循了第二章中的使用 with 语句管理上下文配方,语句和语法。在这个上下文中,我们为 CSV 输出文件创建了一个写入。我们将在第九章中更深入地研究这个问题,输入/输出、物理格式、逻辑布局。
我们使用表达式[None]+list(range(start_T, stop_T, step_T)
,创建了一个标题行。这个表达式包括一个列表文字和一个生成器表达式,用于构建一个列表。我们将在第四章中查看列表,内置数据结构-列表、集合、字典。我们将在第八章中查看生成器表达式,函数式和响应式编程特性。
同样,表格的每个单元格都是由一个生成器表达式构建的,[Twc(T, V) for T in range(start_T, stop_T, step_T)]
。这是一个构建列表对象的理解。列表由风冷函数Twc()
计算的值组成。我们根据表中的行提供风速。我们根据表中的列提供温度。
虽然细节涉及前瞻性部分,def
行提出了一个问题。这个def
行非常复杂。
这种设计的问题在于wind_chill()
函数有七个位置参数。当我们尝试使用这个函数时,我们得到以下代码:
import pathlib
p=pathlib.Path('code/wc.csv')
wind_chill(0,-45,-5,0,20,2,p)
所有这些数字是什么?有没有什么可以帮助解释这行代码的意思?
如何做到...
当我们有大量参数时,使用关键字参数而不是位置参数会有所帮助。
在 Python 3 中,我们有一种强制使用关键字参数的技术。我们可以使用*
作为两组参数之间的分隔符:
-
在
*
之前,我们列出可以或按关键字命名的参数值。在这个例子中,我们没有这些参数。 -
在
*
之后,我们列出必须使用关键字给出的参数值。对于我们的示例,这是所有的参数。
对于我们的示例,生成的函数如下:
def wind_chill(*, start_T, stop_T, step_T, start_V, stop_V, step_V, path):
当我们尝试使用令人困惑的位置参数时,我们会看到这个:
**>>> wind_chill(0,-45,-5,0,20,2,p)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: wind_chill() takes 0 positional arguments but 7 were given**
我们必须按以下方式使用该函数:
wind_chill(start_T=0, stop_T=-45, step_T=-5,
start_V=0, stop_V=20, step_V=2,
path=p)
强制使用必填关键字参数的用法迫使我们每次使用这个复杂函数时都写出清晰的语句。
它是如何工作的...
*
字符在函数定义中有两个含义:
-
它作为一个特殊参数的前缀,接收所有未匹配的位置参数。我们经常使用
*args
将所有位置参数收集到一个名为args
的单个参数中。 -
它被单独使用,作为可以按位置应用的参数和必须通过关键字提供的参数之间的分隔符。
print()
函数就是一个例子。它有三个仅限关键字参数,用于输出文件、字段分隔符字符串和行结束字符串。
还有更多...
当然,我们可以将此技术与各种参数的默认值结合使用。例如,我们可以对此进行更改:
import sys
def wind_chill(*, start_T, stop_T, step_T, start_V, stop_V, step_V, output=sys.stdout):
现在我们可以以两种方式使用这个函数:
- 这是在控制台上打印表的方法:
wind_chill(
start_T=0, stop_T=-45, step_T=-5,
start_V=0, stop_V=20, step_V=2)
- 这是写入文件的方法:
path = pathlib.Path("code/wc.csv")
with path.open('w', newline='') as target:
wind_chill(output=target,
start_T=0, stop_T=-45, step_T=-5,
start_V=0, stop_V=20, step_V=2)
我们在这里改变了方法,稍微更加通用。这遵循了设计具有可选参数的函数配方。
另请参阅
- 查看基于部分函数选择参数顺序配方,了解此技术的另一个应用
在函数参数上写明确的类型
Python 语言允许我们编写完全与数据类型相关的函数(和类)。以这个函数为例:
def temperature(*, f_temp=None, c_temp=None):
if c_temp is None:
return {'f_temp': f_temp, 'c_temp': 5*(f_temp-32)/9}
elif f_temp is None:
return {'f_temp': 32+9*c_temp/5, 'c_temp': c_temp}
else:
raise Exception("Logic Design Problem")
这遵循了之前展示的三个配方:使用超灵活的关键字参数,使用本章的分隔符强制关键字参数,以及设计复杂的 if...elif 链来自第二章,语句和语法*。
这个函数将适用于任何数值类型的参数值。实际上,它将适用于任何实现+
、-
、*
和/
运算符的数据结构。
有时我们不希望我们的函数完全通用。在某些情况下,我们希望对数据类型做出更强的断言。虽然我们有时关心数据类型,但我们不想编写大量看起来像这样的代码:
from numbers import Number
def c_temp(f_temp):
assert isinstance(F, Number)
return 5*(f_temp-32)/9
这引入了额外的assert
语句的性能开销。它还会用一个通常应该重申显而易见的语句来使我们的程序混乱。
此外,我们不能依赖文档字符串进行测试。这是推荐的风格:
def temperature(*, f_temp=None, c_temp=None):
"""Convert between Fahrenheit temperature and
Celsius temperature.
:key f_temp: Temperature in °F.
:key c_temp: Temperature in °C.
:returns: dictionary with two keys:
:f_temp: Temperature in °F.
:c_temp: Temperature in °C.
"""
文档字符串不允许进行任何自动化测试来确认文档实际上是否与代码匹配。两者可能不一致。
我们想要的是关于涉及的数据类型的提示,可以用于测试和确认,但不会影响性能。我们如何提供有意义的类型提示?
准备工作
我们将实现temperature()
函数的一个版本。我们将需要两个模块,这些模块将帮助我们提供关于参数和返回值的数据类型的提示:
from typing import *
我们选择从typing
模块导入所有名称。如果我们要提供类型提示,我们希望它们简洁。写typing.List[str]
很尴尬。我们更喜欢省略模块名称。
我们还需要安装最新版本的mypy
。这个项目正在快速发展。与其使用pip
程序从 PyPI 获取副本,最好直接从 GitHub 存储库github.com/JukkaL/mypy
下载最新版本。
说明中说,目前,PyPI 上的 mypy 版本与 Python 3.5 不兼容。如果你使用 Python 3.5,请直接从 git 安装。
**$ pip3 install git+git://github.com/JukkaL/mypy.git**
mypy
工具可用于分析我们的 Python 程序,以确定类型提示是否与实际代码匹配。
如何做...
Python 3.5 引入了语言类型提示。我们可以在三个地方使用它们:函数参数、函数返回和类型提示注释:
- 为各种数字定义一个方便的类型:
from decimal import Decimal
from typing import *
Number = Union[int, float, complex, Decimal]
理想情况下,我们希望在 numbers 模块中使用抽象的Number
类。目前,该模块没有可用的正式类型规范,因此我们将为Number
定义自己的期望。这个定义是几种数字类型的联合。理想情况下,mypy
或 Python 的未来版本将包括所需的定义。
- 像这样注释函数的参数:
def temperature(*,
f_temp: Optional[Number]=None,
c_temp: Optional[Number]=None):
我们在参数的一部分添加了:
和类型提示。在这种情况下,我们使用我们自己的Number
类型定义来声明任何数字都可以在这里。我们将其包装在Optional[]
类型操作中,以声明参数值可以是Number
或None
。
- 函数的返回值可以这样注释:
def temperature(*,
f_temp: Optional[Number]=None,
c_temp: Optional[Number]=None) -> Dict[str, Number]:
我们为此函数的返回值添加了->
和类型提示。在这种情况下,我们声明结果将是一个具有字符串键str
和使用我们的Number
类型定义的数字值的字典对象。
typing
模块引入了类型提示名称,例如Dict
,我们用它来解释函数的结果。这与实际构建对象的dict
类不同。typing.Dict
只是一个提示。
- 如果需要的话,我们可以在赋值和
with
语句中添加类型提示作为注释。这些很少需要,但可能会澄清一长串复杂的语句。如果我们想要添加它们,注释可能看起来像这样:
result = {'c_temp': c_temp,
'f_temp': f_temp} # type: Dict[str, Number]
我们在构建最终字典对象的语句上添加了# type: Dict[str, Number]
。
工作原理...
我们添加的类型信息称为提示。它们不是 Python 编译器以某种方式检查的要求。它们在运行时也不会被检查。
类型提示由一个名为mypy
的独立程序使用。有关更多信息,请参见mypy-lang.org
。
mypy
程序检查 Python 代码,包括类型提示。它应用一些形式推理和推断技术,以确定各种类型提示是否对 Python 程序可以处理的任何数据为“真”。
对于更大更复杂的程序,mypy
的输出将包括描述代码本身或装饰代码的类型提示可能存在问题的警告和错误。
例如,这是一个容易犯的错误。我们假设我们的函数返回一个单一的数字。然而,我们的返回语句与我们的期望不匹配:
def temperature_bad(*,
f_temp: Optional[Number]=None,
c_temp: Optional[Number]=None) -> Number:
if c_temp is None:
c_temp = 5*(f_temp-32)/9
elif f_temp is None:
f_temp = 32+9*c_temp/5
else:
raise Exception( "Logic Design Problem" )
result = {'c_temp': c_temp,
'f_temp': f_temp} # type: Dict[str, Number]
return result
当我们运行mypy
时,我们会看到这个:
ch03_r04.py: note: In function "temperature_bad":
ch03_r04.py:37: error: Incompatible return value type:
expected Union[builtins.int, builtins.float, builtins.complex, decimal.Decimal],
got builtins.dict[builtins.str,
Union[builtins.int, builtins.float, builtins.complex, decimal.Decimal]]
我们可以看到我们的Number
类型名称在错误消息中被扩展为Union[builtins.int, builtins.float, builtins.complex, decimal.Decimal]
。更重要的是,我们可以看到在第 37 行,return
语句与函数定义不匹配。
考虑到这个错误,我们需要修复返回值或定义,以确保期望的类型和实际类型匹配。目前不清楚哪个是“正确”的。以下任一种可能是意图:
-
计算并返回单个值:这意味着需要有两个
return
语句,取决于计算了哪个值。在这种情况下,没有理由构建result
字典对象。 -
返回字典对象:这意味着我们需要更正
def
语句以具有正确的返回类型。更改这可能会对其他期望temperature
返回Number
实例的函数产生连锁变化。
参数和返回值的额外语法对运行时没有真正影响,只有在源代码首次编译成字节码时才会有很小的成本。它们毕竟只是提示。
还有更多...
在使用内置类型时,我们经常可以创建复杂的结构。例如,我们可能有一个字典,将三个整数的元组映射到字符串列表:
a = {(1, 2, 3): ['Poe', 'E'],
(3, 4, 5): ['Near', 'a', 'Raven'],
}
如果这是函数的结果,我们如何描述这个?
我们将创建一个相当复杂的类型表达式,总结每个结构层次:
Dict[Tuple[int, int, int], List[str]]
我们总结了一个将一个类型Tuple[int, int, int]
映射为另一个类型List[str]
的字典。这捕捉了几种内置类型如何组合以构建复杂的数据结构。
在这种情况下,我们将三个整数的元组视为一个匿名元组。在许多情况下,它不仅仅是一个通用元组,它实际上是一个被建模为元组的 RGB 颜色。也许字符串列表实际上是来自更长文档的一行文本,已经根据空格拆分成单词。
在这种情况下,我们应该做如下操作:
Color = Tuple[int, int, int]
Line = List[str]
Dict[Color, Line]
创建我们自己的应用程序特定类型名称可以极大地澄清使用内置集合类型执行的处理。
另请参阅
-
有关类型提示的更多信息,请参见
www.python.org/dev/peps/pep-0484/
。 -
有关当前
mypy
项目,请参见github.com/JukkaL/mypy
。 -
有关
mypy
如何与 Python 3 一起工作的文档,请参见www.mypy-lang.org
。
基于部分函数选择参数顺序
当我们查看复杂的函数时,有时我们会看到我们使用函数的方式有一个模式。例如,我们可能多次评估一个函数,其中一些参数值由上下文固定,而其他参数值随着处理的细节而变化。
如果我们的设计反映了这一点,它可以简化我们的编程。我们希望提供一种使常见参数比不常见参数更容易处理的方法。我们也希望避免重复大上下文中的参数。
准备就绪
我们将看一个 haversine 公式的版本。这计算地球表面上点之间的距离,使用该点的纬度和经度坐标:
c = 2 arc sin(√a)
基本的计算得出了两点之间的中心角c。角度以弧度表示。我们通过将其乘以地球的平均半径来将其转换为距离。如果我们将角度c乘以半径为 3959 英里,距离,我们将角度转换为英里。
这是这个函数的一个实现。我们包括了类型提示:
from math import radians, sin, cos, sqrt, asin
MI= 3959
NM= 3440
KM= 6372
def haversine(lat_1: float, lon_1: float,
lat_2: float, lon_2: float, R: float) -> float:
"""Distance between points.
R is Earth's radius.
R=MI computes in miles. Default is nautical miles.
>>> round(haversine(36.12, -86.67, 33.94, -118.40, R=6372.8), 5)
2887.25995
"""
Δ_lat = radians(lat_2) - radians(lat_1)
Δ_lon = radians(lon_2) - radians(lon_1)
lat_1 = radians(lat_1)
lat_2 = radians(lat_2)
a = sin(Δ_lat/2)**2 + cos(lat_1)*cos(lat_2)*sin(Δ_lon/2)**2
c = 2*asin(sqrt(a))
return R * c
注意
关于 doctest 示例的说明:
示例中的 doctest 使用了一个额外的小数点,这在其他地方没有使用。这样做是为了使这个示例与在线上的其他示例匹配。
地球不是球形的。在赤道附近,更精确的半径是 6378.1370 公里。在极地附近,半径是 6356.7523 公里。我们在常数中使用常见的近似值。
我们经常遇到的问题是,我们通常在一个单一的上下文中工作,并且我们将始终为R
提供相同的值。例如,如果我们在海洋环境中工作,我们将始终使用R = NM
来获得海里。
提供参数的一致值有两种常见的方法。我们将看看两种方法。
如何做...
在某些情况下,一个整体的上下文将为参数建立一个变量。这个值很少改变。提供参数的一致值有几种常见的方法。这涉及将函数包装在另一个函数中。有几种方法:
-
在一个新函数中包装函数。
-
创建一个偏函数。这有两个进一步的改进:
-
我们可以提供关键字参数
-
或者我们可以提供位置参数
我们将在这个配方中分别看看这些不同的变化。
包装一个函数
我们可以通过将一个通用函数包装在一个特定上下文的包装函数中来提供上下文值:
- 使一些参数成为位置参数,一些参数成为关键字参数。我们希望上下文特征——很少改变的特征——成为关键字。更频繁更改的参数应该保持为位置参数。我们可以遵循使用分隔符强制关键字参数*的方法。
我们可能会将基本的 haversine 函数更改为这样:
def haversine(lat_1: float, lon_1: float,
lat_2: float, lon_2: float, *, R: float) -> float:
我们插入了*
来将参数分成两组。第一组可以通过位置或关键字提供参数。第二组,- 在这种情况下是R
- 必须通过关键字给出。
- 然后,我们可以编写一个包装函数,它将应用所有的位置参数而不加修改。它将作为长期上下文的一部分提供额外的关键字参数:
def nm_haversine(*args):
return haversine(*args, R=NM)
我们在函数声明中使用了*args
构造来接受一个单独的元组args
中的所有位置参数值。当评估haversine()
函数时,我们还使用了*args
来将元组扩展为该函数的所有位置参数值。
使用关键字参数创建一个偏函数
偏函数是一个有一些参数值被提供的函数。当我们评估一个偏函数时,我们将之前提供的参数与额外的参数混合在一起。一种方法是使用关键字参数,类似于包装一个函数:
- 我们可以遵循使用分隔符强制关键字参数*的方法。我们可能会将基本的 haversine 函数更改为这样:
def haversine(lat_1: float, lon_1: float,
lat_2: float, lon_2: float, *, R: float) -> float:
- 使用关键字参数创建一个偏函数:
from functools import partial
nm_haversine = partial(haversine, R=NM)
partial()
函数从现有函数和一组具体的参数值中构建一个新函数。nm_haversine()
函数在构建偏函数时提供了R
的特定值。
我们可以像使用任何其他函数一样使用它:
**>>> round(nm_haversine(36.12, -86.67, 33.94, -118.40), 2)
1558.53**
我们得到了一个海里的答案,这样我们就可以进行与船只相关的计算,而不必每次使用haversine()
函数时都要耐心地检查它是否有R=NM
作为参数。
使用位置参数创建一个偏函数
部分函数是一个具有一些参数值的函数。当我们评估部分函数时,我们正在提供额外的参数。另一种方法是使用位置参数。
如果我们尝试使用带有位置参数的partial()
,我们只能在部分定义中提供最左边的参数值。这让我们想到函数的前几个参数可能被部分函数或包装器隐藏。
- 我们可能会将基本的
haversine
函数更改为这样:
def haversine(R: float, lat_1: float, lon_1: float,
lat_2: float, lon_2: float) -> float:
- 使用位置参数创建一个部分函数:
from functools import partial
nm_haversine = partial(haversine, NM)
partial()
函数从现有函数和具体的参数值集构建一个新的函数。nm_haversine()
函数在构建部分时为第一个参数R
提供了一个特定的值。
我们可以像使用其他函数一样使用这个:
**>>> round(nm_haversine(36.12, -86.67, 33.94, -118.40), 2)
1558.53**
我们得到了一个海里的答案,这样我们就可以进行与航海有关的计算,而不必耐心地检查每次使用haversine()
函数时是否有R=NM
作为参数。
它是如何工作的...
部分函数本质上与包装函数相同。虽然它为我们节省了一行代码,但它有一个更重要的目的。我们可以在程序的其他更复杂的部分中自由构建部分函数。我们不需要使用def
语句。
请注意,在查看位置参数的顺序时,创建部分函数会引起一些额外的考虑:
-
当我们使用
*args
时,它必须是最后一个。这是语言要求。这意味着在它前面的参数可以被具体识别,其余的都变成了匿名的,并且可以被一次性传递给包装函数。 -
在创建部分函数时,最左边的位置参数最容易提供一个值。
这两个考虑让我们将最左边的参数视为更多的上下文:这些预计很少改变。最右边的参数提供细节并经常改变。
还有更多...
还有第三种包装函数的方法——我们也可以构建一个lambda
对象。这也可以工作:
nm_haversine = lambda *args: haversine(*args, R=NM)
注意,lambda
对象是一个被剥离了名称和主体的函数。它被简化为只有两个要素:
-
参数列表
-
一个单一的表达式是结果
lambda
不能有任何语句。如果我们需要语句,我们需要使用def
语句来创建一个包含名称和多个语句的定义。
另请参阅
- 我们还将在使用脚本库开关编写可重用脚本的配方中进一步扩展这个设计
使用 RST 标记编写清晰文档字符串
我们如何清楚地记录函数的作用?我们可以提供例子吗?当然可以,而且我们真的应该。在第二章中的包括描述和文档,语句和语法和使用 RST 标记编写清晰文档字符串的配方中,我们看到了一些基本的文档技术。这些配方介绍了ReStructuredText(RST)用于模块文档字符串。
我们将扩展这些技术,为函数文档字符串编写 RST。当我们使用 Sphinx 等工具时,我们函数的文档字符串将成为描述函数作用的优雅文档。
准备工作
在使用分隔符强制关键字参数*的配方中,我们看到了一个具有大量参数的函数和另一个只有两个参数的函数。
这是一个稍微不同版本的Twc()
函数:
**>>> def Twc(T, V):
... """Wind Chill Temperature."""
... if V < 4.8 or T > 10.0:
... raise ValueError("V must be over 4.8 kph, T must be below 10°C")
... return 13.12 + 0.6215*T - 11.37*V**0.16 + 0.3965*T*V**0.16**
我们需要用更完整的文档来注释这个函数。
理想情况下,我们已经安装了 Sphinx 来看我们的劳动成果。请参阅www.sphinx-doc.org
。
如何做...
通常我们会为函数描述写以下内容:
-
概要
-
描述
-
参数
-
返回
-
异常
-
测试案例
-
任何其他看起来有意义的东西
这是我们如何为一个函数创建良好文档的方法。我们可以应用类似的方法来为一个函数,甚至一个模块创建文档:
- 写概要:不需要一个适当的主题——我们不写 这个函数计算... ;我们从 计算... 开始。没有理由过分强调上下文:
def Twc(T, V):
"""Computes the wind chill temperature."""
- 用详细描述写:
def Twc(T, V):
"""Computes the wind chill temperature
The wind-chill, :math:`T_{wc}`, is based on
air temperature, T, and wind speed, V.
"""
在这种情况下,我们在描述中使用了一小块排版数学。:math:
解释文本角色使用 LaTeX 数学排版。如果你安装了 LaTeX,Sphinx 将使用它来准备一个带有数学的小.png
文件。如果你愿意,Sphinx 可以使用 MathJax 或 JSMath 来进行 JavaScript 数学排版,而不是创建一个.png
文件。
- 描述参数:对于位置参数,通常使用
:param name: description
。Sphinx 将容忍许多变化,但这是常见的。
对于必须是关键字的参数,通常使用 :key name: description
。使用 key
而不是 param
显示它是一个仅限关键字的参数:
def Twc(T: float, V: float):
"""Computes the wind chill temperature
The wind-chill, :math:`T_{wc}`, is based on
air temperature, T, and wind speed, V.
:param T: Temperature in °C
:param V: Wind Speed in kph
"""
有两种包含类型信息的方法:
-
使用 Python 3 类型提示
-
使用 RST
:type name:
标记
我们通常不会同时使用这两种技术。类型提示比 RST :type:
标记更好。
- 使用
:returns:
描述返回值:
def Twc(T: float, V: float) -> float:
"""Computes the wind chill temperature
The wind-chill, :math:`T_{wc}`, is based on
air temperature, T, and wind speed, V.
:param T: Temperature in °C
:param V: Wind Speed in kph
:returns: Wind-Chill temperature in °C
"""
有两种包含返回类型信息的方法:
-
使用 Python 3 类型提示
-
使用 RST
:rtype:
标记
我们通常不会同时使用这两种技术。RST :rtype:
标记已被类型提示取代。
- 确定可能引发的重要异常。使用
:raises exception:
原因标记。有几种可能的变化,但:raises exception:
似乎最受欢迎:
def Twc(T: float, V: float) -> float:
"""Computes the wind chill temperature
The wind-chill, :math:`T_{wc}`, is based on
air temperature, T, and wind speed, V.
:param T: Temperature in °C
:param V: Wind Speed in kph
:returns: Wind-Chill temperature in °C
:raises ValueError: for wind speeds under over 4.8 kph or T above 10°C
"""
- 如果可能的话,包括一个 doctest 测试用例:
def Twc(T: float, V: float) -> float:
"""Computes the wind chill temperature
The wind-chill, :math:`T_{wc}`, is based on
air temperature, T, and wind speed, V.
:param T: Temperature in °C
:param V: Wind Speed in kph
:returns: Wind-Chill temperature in °C
:raises ValueError: for wind speeds under over 4.8 kph or T above 10°C
>>> round(Twc(-10, 25), 1)
-18.8
"""
- 写任何其他附加说明和有用信息。我们可以将以下内容添加到文档字符串中:
See https://en.wikipedia.org/wiki/Wind_chill
.. math::
T_{wc}(T_a, V) = 13.12 + 0.6215 T_a - 11.37 V^{0.16} + 0.3965 T_a V^{0.16}
我们已经包含了一个维基百科页面的参考,该页面总结了风冷计算并链接到更详细的信息。
我们还包括了一个带有函数中使用的 LaTeX 公式的 .. math::
指令。这将排版得很好,提供了代码的一个非常可读的版本。
它是如何工作的...
有关文档字符串的更多信息,请参见第二章中的包括描述和文档 配方,语句和语法。虽然 Sphinx 很受欢迎,但它并不是唯一可以从文档字符串注释中创建文档的工具。Python 标准库中的 pydoc 实用程序也可以从文档字符串注释中生成漂亮的文档。
Sphinx 工具依赖于docutils
包中 RST 处理的核心功能。有关更多信息,请参见pypi.python.org/pypi/docutils
。
RST 规则相对简单。这个配方中的大多数附加功能都利用了 RST 的解释文本角色。我们的每个 :param T:
、 :returns:
和 :raises ValueError:
结构都是一个文本角色。RST 处理器可以使用这些信息来决定内容的样式和结构。样式通常包括一个独特的字体。上下文可能是 HTML 定义列表格式。
还有更多...
在许多情况下,我们还需要在函数和类之间包含交叉引用。例如,我们可能有一个准备风冷表的函数。这个函数可能有包含对 Twc()
函数的引用的文档。
Sphinx 将使用特殊的 :func:
文本角色生成这些交叉引用:
def wind_chill_table():
"""Uses :func:`Twc` to produce a wind-chill
table for temperatures from -30°C to 10°C and
wind speeds from 5kph to 50kph.
"""
我们在 RST 文档中使用了 :func:
Twc`` 来交叉引用一个函数。Sphinx 将把这些转换为适当的超链接。
另请参阅
- 有关 RST 工作的其他配方,请参见第二章中的包括描述和文档 和在文档字符串中编写更好的 RST 标记 配方。
围绕 Python 的堆栈限制设计递归函数
一些函数可以使用递归公式清晰而简洁地定义。有两个常见的例子:
阶乘函数:
计算斐波那契数的规则:
其中每个都涉及一个具有简单定义值的情况,以及涉及根据同一函数的其他值计算函数值的情况。
我们面临的问题是,Python 对这种递归函数定义的上限施加了限制。虽然 Python 的整数可以轻松表示1000!,但堆栈限制阻止我们随意这样做。
计算F[n]斐波那契数涉及一个额外的问题。如果我们不小心,我们会计算很多值超过一次:
F[5] = F[4] + F[3]
F[5] = (F[3] + F[2] ) + (F[2] + F[1] )
等等。
要计算F[5],我们将计算F[3]两次,F[2]三次。这是非常昂贵的。
准备工作
许多递归函数定义遵循阶乘函数设定的模式。这有时被称为尾递归,因为递归情况可以写在函数体的尾部:
def fact(n: int) -> int:
if n == 0:
return 1
return n*fact(n-1)
函数中的最后一个表达式引用了具有不同参数值的函数。
我们可以重新陈述这一点,避免 Python 中的递归限制。
如何做...
尾递归也可以被描述为归约。我们将从一组值开始,然后将它们减少到一个单一的值:
- 扩展规则以显示所有细节:
n! = n x (n- 1 ) × (n- 2 ) × (n- 3 )... × 1
- 编写一个循环,枚举所有的值:
N = { n, n- 1 , n- 2 , ..., 1}在 Python 中,它就是这样的:range(1, n+1)
。然而,在某些情况下,我们可能需要对基本值应用一些转换函数:
N = { f(i): 1 ≤ i < n +1}如果我们必须执行某种转换,它在 Python 中可能看起来像这样:
N = (f(i) for i in range(1,n+1))
- 整合归约函数。在这种情况下,我们正在计算一个大的乘积,使用乘法。我们可以使用 x 表示这一点。对于这个例子,我们只对产品中计算的值施加了一个简单的边界:
以下是 Python 中的实现:
def prod(int_iter):
p = 1
for x in int_iter:
p *= x
return p
我们可以将这个重新陈述为这样的解决方案。这使用了更高级的函数:
def fact(n):
return prod(range(1, n+1))
这很好地起作用。我们已经优化了将prod()
和fact()
函数合并为一个函数的第一个解决方案。事实证明,进行这种优化实际上并没有减少操作的时间。
这里是使用timeit
模块运行的比较:
简单 | 4.7766 |
---|---|
优化 | 4.6901 |
这是一个 2%的性能改进。并不是一个显著的改变。
请注意,Python 3 的range
对象是惰性的——它不创建一个大的list
对象,它会在prod()
函数请求时返回值。这与 Python 2 不同,Python 2 中的range()
函数急切地创建一个包含所有值的大的list
对象,而xrange()
函数是惰性的。
它是如何工作的...
尾递归定义很方便,因为它既简短又容易记忆。数学家喜欢这个,因为它可以帮助澄清函数的含义。
许多静态的编译语言都以类似于我们展示的技术进行了优化。这种优化有两个部分:
- 使用相对简单的代数规则重新排列语句,使递归子句实际上是最后一个。
if
子句可以重新组织成不同的物理顺序,以便return fact(n-1) * n
是最后一个。这种重新排列对于这样组织的代码是必要的:
def ugly_fact(n):
if n > 0:
return fact(n-1) * n
elif n == 0:
return 1
else:
raise Exception("Logic Error")
- 将一个特殊指令注入到虚拟机的字节码中 - 或者实际的机器码中 - 重新评估函数,而不创建新的堆栈帧。Python 没有这个特性。实际上,这个特殊指令将递归转换成一种
while
语句:
p = n
while n != 1:
n = n-1
p *= n
这种纯机械的转换会导致相当丑陋的代码。在 Python 中,它也可能非常慢。在其他语言中,特殊的字节码指令的存在将导致代码运行速度快。
我们不喜欢做这种机械优化。首先,它会导致丑陋的代码。更重要的是 - 在 Python 中 - 它往往会创建比上面开发的替代方案更慢的代码。
还有更多...
斐波那契问题涉及两个递归。如果我们将其简单地写成递归,可能会像这样:
def fibo(n):
if n <= 1:
return 1
else:
return fibo(n-1)+fibo(n-2)
将一个简单的机械转换成尾递归是困难的。像这样具有多个递归的问题需要更加仔细的设计。
我们有两种方法来减少这个计算复杂度:
-
使用记忆化
-
重新阐述问题
记忆化技术在 Python 中很容易应用。我们可以使用functools.lru_cache()
作为装饰器。这个函数将缓存先前计算过的值。这意味着我们只计算一次值;每一次,lru_cache
都会返回先前计算过的值。
它看起来像这样:
from functools import lru_cache
@lru_cache(128)
def fibo(n):
if n <= 1:
return 1
else:
return fibo(n-1)+fibo(n-2)
添加一个装饰器是优化更复杂的多路递归的简单方法。
重新阐述问题意味着从新的角度来看待它。在这种情况下,我们可以考虑计算所有斐波那契数,直到F[n]。我们只想要这个序列中的最后一个值。我们计算所有的中间值,因为这样做更有效。这是一个执行此操作的生成器函数:
def fibo_iter():
a = 1
b = 1
yield a
while True:
yield b
a, b = b, a+b
这个函数是斐波那契数的无限迭代。它使用 Python 的yield
,以便以懒惰的方式发出值。当客户函数使用这个迭代器时,每个数字被消耗时,序列中的下一个数字被计算。
这是一个函数,它消耗值,并对否则无限的迭代器施加一个上限:
def fibo(n):
"""
>>> fibo(7)
21
"""
for i, f_i in enumerate(fibo_iter()):
if i == n: break
return f_i
这个函数从fibo_iter()
迭代器中消耗每个值。当达到所需的数字时,break
语句结束for
语句。
当我们回顾第二章中的设计一个正确终止的 while 语句配方时,我们注意到一个带有break
的while
语句可能有多个终止的原因。在这个例子中,结束for
语句只有一种方法。
我们可以始终断言在循环结束时i == n
。这简化了函数的设计。
另请参阅
- 请参阅第二章中的设计一个正确终止的 while 语句配方,语句和语法
使用脚本库开关编写可重用脚本
通常会创建一些小脚本,我们希望将它们组合成一个更大的脚本。我们不想复制和粘贴代码。我们希望将工作代码留在一个文件中,并在多个地方使用它。通常,我们希望从多个文件中组合元素,以创建更复杂的脚本。
我们遇到的问题是,当我们导入一个脚本时,它实际上开始运行。这通常不是我们导入一个脚本以便重用它时的预期行为。
我们如何导入文件中的函数(或类),而不让脚本开始执行某些操作?
准备好
假设我们有一个方便的 haversine 距离函数的实现,名为haversine()
,并且它在一个名为ch03_r08.py
的文件中。
最初,文件可能是这样的:
import csv
import pathlib
from math import radians, sin, cos, sqrt, asin
from functools import partial
MI= 3959
NM= 3440
KM= 6373
def haversine( lat_1: float, lon_1: float,
lat_2: float, lon_2: float, *, R: float ) -> float:
... and more ...
nm_haversine = partial(haversine, R=NM)
source_path = pathlib.Path("waypoints.csv")
with source_path.open() as source_file:
reader= csv.DictReader(source_file)
start = next(reader)
for point in reader:
d = nm_haversine(
float(start['lat']), float(start['lon']),
float(point['lat']), float(point['lon'])
)
print(start, point, d)
start= point
我们省略了haversine()
函数的主体,只显示了...和更多...
,因为它在基于部分函数选择参数顺序的配方中有所展示。我们专注于函数在 Python 脚本中的上下文,该脚本还打开一个名为wapypoints.csv
的文件,并对该文件进行一些处理。
我们如何导入这个模块,而不让它打印出waypoints.csv
文件中航点之间的距离?
如何做...
Python 脚本可以很容易编写。事实上,创建一个可工作的脚本通常太简单了。以下是我们如何将一个简单的脚本转换为可重用的库:
- 识别脚本的工作语句:我们将区分定义和动作。例如
import
,def
和class
等语句显然是定义性的——它们支持工作但并不执行工作。几乎所有其他语句都是执行动作的。
在我们的例子中,有四个赋值语句更多地是定义而不是动作。区别完全是出于意图。所有语句,根据定义,都会执行一个动作。不过,这些动作更像是def
语句的动作,而不像脚本后面的with
语句的动作。
以下是通常的定义性语句:
MI= 3959
NM= 3440
KM= 6373
def haversine( lat_1: float, lon_1: float,
lat_2: float, lon_2: float, *, R: float ) -> float:
... and more ...
nm_haversine = partial(haversine, R=NM)
其余的语句明显是朝着产生打印结果的动作。
- 将动作封装成一个函数:
def analyze():
source_path = pathlib.Path("waypoints.csv")
with source_path.open() as source_file:
reader= csv.DictReader(source_file)
start = next(reader)
for point in reader:
d = nm_haversine(
float(start['lat']), float(start['lon']),
float(point['lat']), float(point['lon'])
)
print(start, point, d)
start= point
- 在可能的情况下,提取文字并将其转换为参数。这通常是将文字移到具有默认值的参数中。
从这里开始:
def analyze():
source_path = pathlib.Path("waypoints.csv")
到这里:
def analyze(source_name="waypoints.csv"):
source_path = pathlib.Path(source_name)
这使得脚本可重用,因为路径现在是一个参数而不是一个假设。
- 将以下内容作为脚本文件中唯一的高级动作语句包括:
if __name__ == "__main__":
analyze()
我们已经将脚本的动作封装为一个函数。顶层动作脚本现在被包裹在一个if
语句中,以便在导入时不被执行。
它是如何工作的...
Python 的最重要规则是,导入模块实质上与运行模块作为脚本是一样的。文件中的语句按顺序从上到下执行。
当我们导入一个文件时,通常我们对执行def
和class
语句感兴趣。我们可能对一些赋值语句感兴趣。
当 Python 运行一个脚本时,它设置了一些内置的特殊变量。其中之一是__name__
。这个变量有两个不同的值,取决于文件被执行的上下文:
-
从命令行执行的顶层脚本:在这种情况下,内置特殊名称
__name__
的值设置为__main__
。 -
由于导入语句而执行的文件:在这种情况下,
__name__
的值是正在创建的模块的名称。
__main__
的标准名称一开始可能有点奇怪。为什么不在所有情况下使用文件名?这个特殊名称是被分配的,因为 Python 脚本可以从多个来源之一读取。它可以是一个文件。Python 也可以从stdin
管道中读取,或者可以在 Python 命令行中使用-c
选项提供。
然而,当一个文件被导入时,__name__
的值被设置为模块的名称。它不会是__main__
。在我们的例子中,import
处理期间__name__
的值将是ch03_r08
。
还有更多...
现在我们可以围绕一个可重用的库构建有用的工作。我们可能会创建几个看起来像这样的文件:
文件trip_1.py
:
from ch03_r08 import analyze
analyze('trip_1.csv')
或者甚至更复杂一些:
文件all_trips.py
:
from ch03_r08 import analyze
for trip in 'trip_1.csv', 'trip_2.csv':
analyze(trip)
目标是将实际解决方案分解为两个特性集合:
-
类和函数的定义
-
一个非常小的面向行动的脚本,使用定义来进行有用的工作
为了达到这个目标,我们经常会从一个混合了两组特性的脚本开始。这种脚本可以被视为一个尖峰解决方案。我们的尖峰解决方案应该在我们确信它有效之后逐渐演变成一个更精细的解决方案。
尖峰或者悬崖钉是一种可移动的登山装备,它并不能让我们在路线上爬得更高,但它能让我们安全地攀登。
另请参阅
- 在第六章中,类和对象的基础,我们将看一下类定义。这是另一种广泛使用的定义性语句。