零、简介
“我是无意中听到的,可能是因为我在偷听。”
——匿名
如果你可以带着这本书回到 20 世纪 90 年代初,把第 23 章实现 RSA 密码的内容出口到美国以外是非法的。因为用 RSA 加密的信息不可能被破解,像 RSA 这样的加密软件的出口被认为是国家安全问题,需要国务院的批准。事实上,强密码术与坦克、导弹和火焰喷射器处于同一管控水平。
1990 年,加州大学伯克利分校的学生丹尼尔·j·伯恩斯坦(Daniel J. Bernstein)想发表一篇学术论文,介绍他的 Snuffle 加密系统的源代码。美国政府通知他,他需要成为一个有执照的军火商,然后才能在互联网上发布他的源代码。政府还告诉他,如果他申请出口许可证,它将拒绝他,因为他的技术非常保密。
电子前沿基金会,一个年轻的数字公民自由组织,代表伯恩斯坦起诉美国政府。法院有史以来第一次裁定,编写的软件代码受第一修正案的保护,与加密相关的出口控制法,侵犯了伯恩斯坦在第一修正案中的权利。
现在,强大的加密技术是全球经济的基础,保护着拥有每天数百万互联网购物者使用的电子商务网站和企业。情报机构关于加密软件将成为严重威胁国家安全的预测是没有根据的。
但是就在 20 世纪 90 年代,自由传播这些知识(就像这本书所做的那样)会让你因军火武器走私罪而入狱。想要更详细地了解争取加密自由的法律斗争历史,请阅读史蒂文·利维的书《Crypto: How the Code Rebels Beat the Government, Saving Privacy in the Digital Age》(Penguin 出版社,2001 年)。
谁应该读这本书?
许多书教初学者如何用密码写秘密信息。也有一些书教初学者如何破解密码。但是没有书教初学者如何给计算机编程来破解密码。这本书填补了这一空白。
这本书是为那些对加密、黑客或密码学感兴趣的人准备的。这本书里的密码(除了第 23 章和第 24 章里的公钥密码)都是几个世纪前的,但是任何笔记本电脑都有破解它们的计算能力。没有现代组织或个人在继续使用这些密码,但通过学习它们,您将了解密码学的基础,以及黑客如何破解脆弱的加密。
注
你在本书中学到密码的过程将很有趣,但它们并不提供真正的安全性。不要使用本书中的任何加密程序来保护你的实际文件。一般来说,你不应该相信你创造的密码。真实世界的密码在投入使用之前,要经过专业密码学家多年的分析。
这本书也是给以前没有编程基础的人看的。它使用 Python 编程语言教授基本的编程概念,这是初学者最好入手的语言之一。它有一个温和的学习曲线,所有年龄的新手都可以掌握,但它也是专业软件开发人员使用的一种强大的语言。Python 可以运行在 Windows,macOS,Linux,甚至 Raspberry Pi 上,并且可以免费下载和使用。(参见第 xxv 页的下载并安装 Python 获取说明。)
在本书中,我将经常使用术语“黑客”。这个词有两种定义。黑客可以是这样一个人,他研究一个系统(例如一个密码的规则或一个软件)以便很好地理解它,以至于他们不受该系统原始规则的限制,并且可以以创造性的方式修改它。黑客也可以是闯入计算机系统、侵犯他人隐私并造成损害的罪犯。这本书在第一种意义上使用这个术语。黑客很酷。罪犯只是那些认为他们通过破坏东西来变聪明的人。
这本书里有什么?
前几章介绍了基本的 Python 和密码学概念。随后,剩下章节通常在解释密码程序和解释破解该密码的程序之间交替。每章还包括练习题,以帮助您复习所学内容。
- 第一章:制作纸质加密工具 涵盖了一些简单的纸质工具,展示了在计算机出现之前加密是如何完成的。
- 第二章:交互 shell 中的编程 讲解了如何使用 Python 的交互 Shell 一行一行地执行代码。
- 第三章:字符串和编写程序 涵盖了编写完整的程序,并介绍了本书所有程序中使用的字符串数据类型。
- 第四章:反向密码 解释了如何为你的第一个密码编写一个简单的程序。
- 第五章:凯撒密码 涵盖了一个几千年前首次发明的基本密码。
- 第六章:用暴力破解凯撒密码 解释了暴力破解技术,以及如何使用它在没有加密密钥的情况下解密消息。
- 第七章:用换位密码加密 介绍换位密码和用它加密信息的程序。
- 第八章:用换位密码解密 涵盖了换位密码的后半部分:能够用密钥解密消息。
- 第九章:编写一个程序来测试你的程序 介绍了用其它程序测试自己代码的编程技巧。
- 第十章:加密和解密文件 解释了如何编写从硬盘读取文件和向硬盘写入文件的程序。
- 第十一章:程序化检测英语 描述了如何让计算机检测英语句子。
- 第十二章:破解换位密码 结合前几章的概念,破解换位密码。
- 第十三章:仿射密码的模运算模块 解释了仿射密码背后的数学概念。
- 第十四章:仿射密码编程 涵盖了编写一个仿射密码加密程序。
- 第十五章:破解仿射密码 讲解如何写程序破解仿射密码。
- 第十六章:编写简单的替换密码 涵盖了编写一个简单的替换密码加密程序。
- 第十七章:破解简单替换密码 讲解如何编写程序破解简单替换密码。
- 第 18 章:编程维吉尼亚密码 解释了一个维吉尼亚密码的程序,一个更复杂的替换密码。
- 第十九章:频率分析 探讨英语单词的结构,以及如何用它来破解维吉尼亚密码。
- 第二十章:破解维吉尼亚密码 讲述了一个破解维吉尼亚密码的程序。
- 第 21 章:一次性密码本 解释了一次性密码本以及为什么它在数学上不可能被破解。
- 第二十二章:寻找并生成质数 讲述了如何编写一个快速判断一个数是否为质数的程序。
- 第 23 章:生成公钥密码的密钥 描述了公钥密码以及如何编写生成公钥和私钥的程序。
- 第 24 章:公钥密码器编程 解释了如何写一个公钥密码器程序,你不能仅仅用笔记本电脑就能破解它。
- 附录调试 Python 代码,向您展示如何使用 IDLE 的调试器来查找并修复程序中的 bug。
如何使用这本书
用 Python 破解代码不同于其他编程书籍,因为它关注的是完整程序的源代码。这本书向你展示了完整的程序,并解释了它们是如何工作的,而不是教你编程概念,让你自己去弄清楚如何制作自己的程序。
一般来说,你应该按章节顺序阅读这本书。在前几章建立编程概念的基础。然而,Python 是一种可读性很强的语言,在开始前面几章之后,你可以跳到后面的章节,拼凑出代码的功能。如果你在继续往后续章节阅读过程中,感觉迷失了,可以回到前面的章节。
键入源代码
当你通读这本书时,我鼓励你手动将这本书的源代码输入 Python 。这样做肯定会帮助你更好地理解代码。
键入源代码时,不要包含出现在每行开头的行号。这些数字不是实际程序的一部分,我们用它们来指代代码中的特定行。但是除了行号之外,请确保输入的代码与显示的完全一样,包括大写字母和小写字母。
你还会注意到,有些行不是从页面的最左边开始,而是缩进了四个、八个或更多的空格。你需要确保在每行开头输入正确的空格数,以避免发生错误。
但是如果你不想输入代码,你可以从本书的网站www.nostarch.com/crackingcodes
下载源代码文件。
检查代码编写中的错别字
尽管手动输入程序的源代码对学习 Python 很有帮助,但您可能偶尔会犯一些会导致错误的错别字。这些错别字可能很难发现,尤其是当您的源代码非常长的时候。
为了快速方便地检查你键入的源代码中的错误,你可以将文本复制并粘贴到该书网站上的在线比较工具中,网址为www.nostarch.com/crackingcodes
。差异工具显示了书中的源代码和你的源代码之间的任何差异。
本书编码约定
这本书的目的不是设计成一本参考手册;而是作为给初学者的实践指南。出于这个原因,编码风格有时违背了最佳实践,但这是一个有意识的决定,使代码更容易学习。这本书也跳过了理论上的计算机科学概念。
经验丰富的程序员,可能会指出改变本书中的代码实现方法可以提高效率,但本书主要关注的是让程序以最少的努力工作。
在线资源
这本书的网站(www.nostarch.com/crackingcodes
)包括许多有用的资源,包括程序的可下载文件和练习题的示例解答。这本书全面地涵盖了经典密码,但是因为总是有更多的东西需要学习,所以我也包括了对这本书中介绍的许多主题的进一步阅读的建议。
下载安装 Python
在开始编程之前,您需要安装 Python 解释器,这是一个执行您将用 Python 语言编写的指令的软件。从现在开始,我将把“Python 解释器”称为“Python”。
从www.python.org/downloads
免费下载适用于 Windows、macOS 和 Ubuntu 的 Python。如果你下载了最新版本,这本书里的所有程序都可以运行。
注
一定要下载 Python 3 的某个版本(比如 3.6)。本书中的程序是为在 Python 3 上运行而编写的,在 Python 2 上可能无法正确运行。
Windows 安装
在 Windows 上,下载 Python 安装程序,其文件名应该以.msi
结尾。并双击它。按照安装程序在屏幕上显示的说明安装 Python,如下所示:
- 选择立即安装开始安装。
- 安装完成后,点击关闭。
macOS 安装
在 macOS 上,从网站上下载适用于您的 macOS 版本的 dmg* 文件,然后双击它。按照安装程序在屏幕上显示的说明安装 Python,如下所示:
- 当 DMG 包在新窗口中打开时,双击
Python.mpkg
文件。您可能需要输入电脑的管理员密码。 - 点击继续通过欢迎部分,点击同意接受许可。
- 选择 HD Macintosh (或者你的硬盘名称),点击安装。
Ubuntu 安装
如果您运行的是 Ubuntu,请按照以下步骤从 Ubuntu 软件中心安装 Python:
- 打开 Ubuntu 软件中心。
- 在窗口右上角的搜索框中输入 Python 。
- 选择 IDLE(使用 Python 3.6) ,或者随便什么最新版本。
- 点击安装。
您可能需要输入管理员密码才能完成安装。
下载pyperclip.py
本书中几乎每个程序都使用了我编写的一个自定义模块,名为pyperclip.py
。这个模块提供了让你的程序复制和粘贴文本到剪贴板的功能。它不是 Python 内置程序里自带,所以你需要从www.nostarch.com/crackingcodes
下载。
这个文件必须和你写的 Python 程序文件在同一个文件夹里(也叫目录*)。否则,当您尝试运行程序时,会看到以下错误消息:
ImportError: No module named pyperclip
现在您已经下载并安装了 Python 解释器和pyperclip.py
模块,让我们看看您将在哪里编写程序。
启动 IDLE
Python 解释器是运行你的 Python 程序的软件,而*交互式开发环境(IDLE)*软件是你编写程序的地方,很像文字处理器。安装 Python 时会安装 IDLE。如果要开始启动 IDLE,请按照下列步骤操作:
- 在 Windows 7 或更新版本上,点击屏幕左下角的开始图标,在搜索框中输入
IDLE
,选择 IDLE (Python 3.6 64 位)。 - 在 macOS 上,打开 Finder,点击应用,点击 Python 3.6 ,然后点击
IDLE
图标。 - 在 Ubuntu 上选择应用 -> 工具 -> 终端然后进入 idle3 。(您也可以点击屏幕顶部的应用,选择应用,然后点击IDLE 3 。)
无论你运行的是哪种操作系统,IDLE 窗口看起来都应该类似于图 1。根据 Python 的具体版本,标题文本可能略有不同。
图 1:IDLE 窗口
这个窗口叫做交互 Shell。shell 是一种程序,它可以让你在计算机中输入指令,很像 macOS 上的终端或 Windows 命令提示符。有时你会想运行一小段代码,而不是编写一个完整的程序。Python 的交互式 shell 允许您输入 Python 解释器软件的指令,计算机会立即读取并运行这些指令。
例如,在交互式 shell 中的>>>
提示符旁边键入以下内容:
>>> print('Hello, world!')
按下Enter
键,交互 shell 应该显示以下响应内容:
Hello, world!
总结
在计算机的引入带来了现代密码学之前,仅仅使用铅笔和纸是不可能破解许多密码的。尽管许多古老的经典密码容易受到计算机的攻击,但学习它们仍然很有趣。编写破解这些密码的密码分析程序是学习如何编程的好方法。
在第 1 章中,我们将从一些基本的加密工具开始,在没有计算机帮助的情况下加密和解密信息。
让我们开始破解吧。
一、制作纸质加密工具
“加密精灵已经从瓶子里出来了。”
——简·库姆,WhatsApp 创始人
在我们开始写密码程序之前,让我们看一下只用铅笔和纸实现加密和解密的过程。这将帮助你理解密码是如何工作的,以及产生秘密信息的数学原理。在这一章中,你将了解我们所说的密码是什么,以及代码与密码有何不同。然后,您将使用一种称为凯撒密码的简单密码,用纸和笔对消息进行加密和解密。
本章涵盖的主题
- 什么是密码学?
- 代码和密码
- 凯撒密码
- 密码轮
- 用算术实现密码学
- 双重加密
什么是密码学?
历史上,任何需要与他人共享秘密的人,如间谍、士兵、黑客、海盗、商人、暴君和政治活动家,都依赖密码学来确保他们的秘密保密和不被公开。密码学是使用秘密代码的科学。要理解加密是什么样子的,请看下面两段文字:
左边的文字是一条经过加密的秘密信息,或者说变成了一个秘密代码。对于不知道如何解密它,或者把它变回原始的英文信息的人来说,它是完全不可读的。右边的消息是没有隐藏含义的随机乱码。加密使信息对其他无法解密的人保密,即使他们得到了加密后的信息。一条加密的信息看起来完全像随机的胡说八道。
一个密码学家使用并研究秘密代码。当然,这些秘密信息并不总是保密的。一个密码分析专家,也被称为密码破译者或黑客,可以破解密码并读取他人的加密信息。这本书教你如何使用各种技术加密和解密信息。但不幸的是或者说幸运的是,你将在本书中学到的黑客行为不会让你陷入法律问题的危险。
代码与密码
与密码不同的是,代码可以被理解和公开获得。代码用符号代替信息,任何人都可以通过查找来翻译成信息。
在 19 世纪早期,一个众所周知的代码来自于电报的发展,它允许通过电线在各大洲之间进行近乎即时的通信。通过电报发送信息比以前派一个骑马的人携带一袋字母要快得多。然而,电报不能直接发送写在纸上的字母。相反,它只能发送两种类型的电脉冲:一种称为“点”的短脉冲和一种称为“划”的长脉冲。
要将字母表中的字母转换成这些点和破折号,你需要一个编码系统将英语转换成电脉冲。将英语转换成点和破折号来发送电报的过程称为编码,接收到消息时将电脉冲翻译成英语的过程称为解码。用于编码和解码电报(以及后来的无线电)信息的代码被称为莫尔斯电码,如表 1-1 所示。塞缪尔·莫尔斯和阿尔弗雷德·维尔发明了莫尔斯电码。
表 1-1: 国际莫尔斯电码编码
字母 | 编码 | 字母 | 编码 | 数字 | 编码 |
A |
N |
1 |
|||
B |
O |
2 |
|||
C |
P |
3 |
|||
D |
Q |
4 |
|||
E |
R |
5 |
|||
F |
S |
6 |
|||
G |
T |
7 |
|||
H |
U |
8 |
|||
I |
V |
9 |
|||
J |
W |
0 |
|||
K |
X |
||||
L |
Y |
||||
M |
Z |
通过用一键电报机点击点和破折号,报务员几乎可以立即将英语信息传达给世界另一端的人!(要了解更多关于莫尔斯电码的信息,请访问/www.nostarch.com/crackingcodes
。)
与代码不同的是,密码是一种特殊类型的代码,旨在使信息保密。你可以使用一种密码将可理解的英文文本,称为明文*,转换成隐藏秘密信息的乱码,称为密文。密码是一组在明文和密文之间转换的规则。这些规则通常使用只有通信者知道的秘密密钥来加密或解密。在这本书里,你将学习几种密码,并编写程序使用这些密码来加密和解密文本。但是首先,让我们使用简单的纸质工具手工加密消息。
凯撒密码
你将学习的第一个密码是凯撒密码,它是以 2000 年前使用它的朱利叶斯·凯撒命名的。好消息是它简单易学。坏消息是因为它太简单了,所以密码分析者也很容易破解。然而,这仍然是一个有用的学习练习。
凯撒密码的工作原理是,在移动字母表后,用一个新字母替换信息中的每个字母。例如,朱利叶斯·凯撒通过将字母表中的字母下移三位,然后用移位后的字母表中的字母替换每个字母,来替换他信息中的字母。
例如,消息中的每个 A 都将被替换为 D,每个 B 都将是 E,依此类推。当 Caesar 需要移动字母表末尾的字母(如 Y(时,他会绕到字母表的开头,并将三个位置移动到 b。在本节中,我们将使用凯撒密码手动加密消息。
密码轮
为了使使用凯撒密码将明文转换为密文更容易,我们将使用一个密码轮,也称为密码盘。密码轮由两个字母环组成;每个环被分成 26 个槽(26 个字母的字母表)。外环代表明文字母表,内环代表密文中对应的字母。内环也是从 0 到 25 的数字。这些数字代表加密密钥,在这种情况下,它是从 A 移动到内环上相应字母所需的字母数。因为移位是循环的,用大于 25 的密钥移位会使字母绕回,所以移位 26 就等于移位 0,移位 27 就等于移位 1,依此类推。
您可以在www.nostarch.com/crackingcodes
在线访问虚拟密码轮。图 1-1 显示了它的样子。要旋转滚轮,请单击它,然后四处移动鼠标光标,直到您想要的配置就位。然后再次单击鼠标以停止滚轮旋转。
图 1-1:在线密码轮
这本书的网站上还提供了一个可打印的纸质密码轮。剪下两个圆,将它们叠放在一起,将较小的一个放在较大的一个的中间。通过两个圆圈的中心插入一个大头针或圆头钉,这样你就可以在适当的位置旋转它们。
使用纸张或虚拟转轮,你可以手工加密秘密信息。
用密码轮加密
要开始加密,在一张纸上用英语写下你的信息。对于这个例子,我们将加密消息,密码是 ROSEBUD。接下来,旋转密码轮的内轮,直到其插槽与外轮的插槽匹配。注意外轮字母 A 旁边的点。注意这个点旁边的内轮上的数字。这是加密密钥。
例如,在图 1-1 中,外圆的 A 在内圆的数字 8 之上。在我们的例子中,我们将使用这个加密密钥来加密消息,如图 1-2 所示。
图 1-2:用 8 的凯撒密钥加密信息
对于消息中的每个字母,在外圈找到它,并用内圈的相应字母替换它。在这个例子中,消息中的第一个字母是 T(“THE SECRET…”中的第一个 T),所以在外圆中找到字母 T,然后在内圆中找到相应的字母,也就是字母 b。因此,秘密消息总是用 b 替换 T。(如果您使用不同的加密密钥,明文中的每个 T 将被替换为不同的字母。)消息中的下一个字母是 H,变成 p,字母 E 变成 m,外轮上的每个字母总是加密到内轮上的同一个字母。为了节省时间,在您查找“THE SECRET…”中的第一个 T 并看到它加密到 B 后,您可以将消息中的每个 T 替换为 B,因此您只需要查找一个字母一次。
在您加密整个消息之后,原始消息(秘密密码是 ROSEBUD)就变成了 BPM AMKZMB XIAAEWZL QA ZWAMJCL。请注意,非字母字符(如空格)不会改变。
现在你可以把这封加密的邮件发送给别人(或者自己保存),除非你告诉他们秘密的加密密钥,否则没有人能够阅读它。一定要对加密密钥保密;任何知道消息是用密钥 8 加密的人都可以读取密文。
用密码轮解密
要解密密文,从密码轮的内圈开始,然后移动到外圈。例如,假设您收到了密文 IWT CTL EPHHLDGS XH HLDGSUXHW。除非你知道密钥(或者除非你是一个聪明的黑客),否则你无法解密这条消息。幸运的是,你的朋友已经告诉你他们用 15 密钥来传递信息。该密钥的密码轮如图 1-3 所示。
图 1-3:设置在 15 密钥的密码轮
现在,您可以将外圆上的字母 A(下面有圆点的那个)排列在内圆上有数字 15 的字母(也就是字母 P(上。然后,在内圈上找到秘密消息中的第一个字母,就是 I,再看外圈上对应的字母,就是 t,密文中的第二个字母 W 解密成字母 h,将密文中的其余字母解密回明文,就得到消息,新密码是剑鱼,如图 1-4 所示。
图 1-4:用 15 的凯撒密钥解密一条消息
如果你使用了一个不正确的密钥,比如 16,解密后的信息将是 SGD MDV OZRRVNQC HR RVNQCEHRG,这是不可读的。除非使用正确的密钥,否则无法理解解密的消息。
用算术加密和解密
密码轮是使用凯撒密码加密和解密的方便工具,但是您也可以使用算术加密和解密。要做到这一点,从 A 到 Z 写字母表的字母,在每个字母下面写下从 0 到 25 的数字。从 A 下面的 0 开始,B 下面的 1,依此类推,直到 z 下面的 25。
图 1-5:从 0 到 25 对字母表进行编号
你可以用这个字母到数字的代码来代表字母。这是一个强大的概念,因为它允许你对字母进行数学运算。例如,如果将字母CAT
表示为数字 2、0 和 19,则可以将 3 相加得到数字 5、3 和 22。这些新数字代表字母FDW
,如图 1-5 所示。你刚刚给CAT
这个词“加”了 3!稍后,我们将能够编程一台计算机来为我们做这些数学运算。
要使用凯撒密码算法进行加密,请在您要加密的字母下找到数字,然后将密钥号添加到该数字中。得到的总和就是加密字母下的数字。比如,我们来加密 HELLO。你好吗?使用密钥 13。(您可以使用 1 到 25 之间的任何数字作为密钥。)首先找到 H 下面的数字,是 7。然后在这个数上加 13:7+13 = 20。因为数字 20 在字母 U 下面,字母 H 加密到 U。
同样,要加密字母 E (4(,加 4 + 13 = 17。17 以上的数是 R,所以 E 被加密成 R,以此类推。
这个过程一直进行到字母 O,O 下面的数字是 14。但是 14 加 13 是 27,数字列表最多只有 25。如果字母的数字和密钥的和是 26 或更多,你需要从中减去 26。在这种情况下,27–26 = 1。数字 1 上面的字母是 B,所以 O 用密钥 13 加密到 B。当您加密邮件中的每个字母时,密文将为 URYYB。UBJ·NER·LBH?
要解密密文,减去密钥而不是加上密钥。密文字母 B 的数目是 1。1 减去 13 得到–12。像我们加密的“减 26”法则,解密时结果小于 0,就需要加 26。因为–12+26 = 14,密文字母 B 解密为 o。
注
如果你不知道如何用负数加减,你可以在
www.nostarch.com/crackingcodes
了解一下。
如你所见,使用凯撒密码不需要密码轮。你所需要的只是一支铅笔,一张纸,和一些简单的算术!
为什么双重加密不起作用
您可能认为使用两个不同的密钥对一条消息加密两次会使加密的强度加倍。但凯撒密码(以及大多数其他密码)并非如此。事实上,双重加密的结果和普通加密的结果是一样的。让我们尝试对消息进行双重加密,看看为什么。
例如,如果您使用密钥 3 对单词 KITTEN 进行加密,您将在明文字母的数字上加上 3,得到的密文将是 NLWWHQ。如果您随后加密 NLWWHQ,这次使用密钥 4,得到的密文将是 RPAALU,因为您在明文字母的数字上加了 4。但这和用密钥 7 加密一次小猫这个词是一样的。
对于大多数密码,多次加密不会提供额外的强度。事实上,如果你用两个加起来是 26 的密钥对一些明文进行加密,得到的密文将和原始明文一样!
总结
几个世纪以来,凯撒密码和其他类似的密码被用来加密秘密信息。但是如果你想加密一条长消息——比如说,一整本书——手工加密可能需要几天或几周的时间。这就是编程可以发挥作用的地方。计算机可以在不到一秒的时间内加密和解密大量文本!
要使用计算机进行加密,你需要学习如何编写程序,或者使用计算机能够理解的语言来指导计算机执行我们刚刚执行的相同步骤。幸运的是,学习像 Python 这样的编程语言并不像学习像日语或西班牙语这样的外语那样困难。除了加法、减法和乘法,你也不需要知道很多数学知识。你需要的只是一台电脑和这本书!
让我们继续第 2 章,在这里我们将学习如何使用 Python 的交互式 shell 一次一行地探索代码。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 用给定的密钥加密比尔斯的《魔鬼字典》中的以下条目:
- 密钥 4:
AMBIDEXTROUS: Able to pick with equal skill a right-hand pocket or a left.
- 密钥 17:
GUILLOTINE: A machine which makes a Frenchman shrug his shoulders with good reason.
- 密钥 21:
IMPIETY: Your irreverence toward my deity.
- 用给定的密钥解密下列密文:
- 密钥 15:
ZXAI: P RDHIJBT HDBTIXBTH LDGC QN HRDIRWBTC XC PBTGXRP PCS PBTGXRPCH XC HRDIAPCS.
- 密钥 4:
MQTSWXSV: E VMZEP EWTMVERX XS TYFPMG LSRSVW.
- 用密钥 0 加密下面的句子:
This is a silly example.
- 以下是一些单词及其加密。每个单词用了哪个密钥?
ROSEBUD – LIMYVOX
YAMAMOTO – PRDRDFKF
ASTRONOMY – HZAYVUVTF
- 用密钥 8 加密的这句话用密钥 9 解密成什么?" ummsmaa:Cvkwuuwv xibqmvkm qv xtivvqvo I zmdmvom bpib QA ewzbp epqtm . "*
二、交互式 SHELL 中的编程
“分析引擎并不能自命地创造任何东西。它可以做任何我们知道如何命令它执行的事情。”
——阿达·洛芙莱斯,1842 年 10 月
在编写加密程序之前,您需要学习一些基本的编程概念。这些概念包括值、运算符、表达式和变量。
本章涵盖的主题
- 运算符
- 值
- 整数和浮点数
- 表达式
- 计算表达式
- 变量值
- 覆盖变量
让我们从探索如何在 Python 的交互式 shell 中进行一些简单的数学运算开始。请务必在您的计算机旁边阅读这本书,这样您就可以输入简短的代码示例并了解它们的功能。从输入程序开发肌肉记忆将帮助你记住 Python 代码是如何构造的。
一些简单的数学表达式
通过打开 IDLE 启动(参见第二十七页上的启动 IDLE(。您将看到交互式 Sheel 和在>>>
提示符旁边闪烁的光标。交互式 Shell 可以像计算器一样工作。在 shell 中键入2 + 2
,然后在键盘上按enter
。(在某些键盘上,这是回车键
。)计算机应显示数字4
做出响应,如图 2-1 所示。
图 2-1:将2 + 2
键入 shell 。
在图 2-1 的例子中,+
符号告诉计算机将数字2
和2
相加,但是 Python 也可以做其他计算,比如用减号(–
(减去数字,用星号(*
(乘以数字,或者用正斜杠(/
(除以数字。当以这种方式使用时,+
、-
、*
和/
被称为运算符,因为它们告诉计算机对它们周围的数字执行运算。表 2-1 总结了 Python 数学运算符。这些2
s(或其他数字)被称为值。
表 2-1:Python 中的数学运算符
运算符 | 操作 |
+ |
加法 |
- |
减法 |
* |
乘法 |
/ |
除法 |
本身,2 + 2
不是程序;它只是一个简单的指令。程序由许多这样的指令组成。
整数和浮点值
在编程中,整数如4
、0
、99
称为整数。带小数点的数字(3.5
、42.1
、5.0
(称为浮点数。在 Python 中,数字5
是一个整数,但是如果你把它写成5.0
,它将是一个浮点数。
整数和浮点是数据类型。值42
是整型或int
数据类型的值。值7.5
是浮点或float
数据类型的值。
每个值都有一个数据类型。你将学习一些其他的数据类型(比如第三章的中的字符串),但是现在只要记住,任何时候我们谈论一个值,这个值是一个特定的数据类型。通常只需查看值是如何书写的,就可以很容易地识别数据类型。整数是没有小数点的数字。浮点数是带小数点的数字。所以42
是 int,但是42.0
是 float。
表达式
您已经看到 Python 解决了一个数学问题,但是 Python 可以做更多的事情。尝试在 shell 中键入以下数学问题,在每个问题之后按下enter
键:
>>> 2+2+2+2+2 # ➊ 10 >>> 8*6 48 >>> 10-5+6 # ➋ 11 >>> 2 + 2 # ➌ 4
图 2-2:一个表达式由值(如 2 (和运算符(如 + (组成。
这些数学题叫做表达式。计算机可以在几秒钟内解决数百万个这样的问题。表达式由运算符(数学符号)和连接的值(数字)组成,如图 2-2 所示。一个表达式中可以有任意多的数字 ➊,只要它们由运算符连接;你甚至可以在一个表达式中使用多种类型的操作符 ➋。您还可以在整数和这些运算符 ➌ 之间输入任意数量的空格。但是一定要确保表达式总是在行首开始,前面没有空格,因为行首的空格会改变 Python 解释指令的方式。您将在第 45 页的的块中了解更多关于行首空格的信息。
运算顺序
你可能还记得数学课上的短语“运算顺序”。比如先做乘法再做加法。表达式2 + 4 * 3
的计算结果为14
,因为先进行乘法运算来计算4 * 3
,然后再加上2
。圆括号可以让不同的运算符先行。在表达式(2 + 4) * 3
中,首先进行加法计算(2 + 4)
,然后将总和乘以3
。括号使得表达式的计算结果为18
而不是14
。Python 数学运算符的运算顺序(也称为优先(类似于数学。首先计算括号内的运算;接下来从左到右执行*
和/
运算符;然后从左到右执行+
和-
运算符。
计算表达式
当计算机求解表达式10 + 5
并得到值15
时,我们说它已经对表达式求值。对表达式求值会将表达式简化为一个值,就像解决数学问题会将问题简化为一个数字:答案。
表达式10 + 5
和10 + 3 + 2
具有相同的值,因为它们的计算结果都是15
。甚至单个值也被认为是表达式:表达式15
的计算结果是值15
。
Python 会继续计算表达式,直到它变成单个值,如下所示:
Python 从最里面最左边的括号开始计算表达式。即使圆括号相互嵌套,圆括号内的表达式部分的计算规则也与任何其他表达式相同。所以当 Python 遇到((7 + 1) / (3 - 1))
时,它首先求解最左边的内括号中的表达式(7 + 1)
,然后求解右边的表达式(3 - 1)
。当内括号中的每个表达式都被简化为单个值时,外括号中的表达式将被求值。请注意,除法运算的结果是浮点值。最后,当括号中没有更多的表达式时,Python 按照运算符的顺序执行任何剩余的计算。
在一个表达式中,可以有两个或多个由操作符连接的值,也可以只有一个值,但是如果在交互式 shell 中输入一个值和一个操作符,就会得到一条错误消息:
>>> 5 + SyntaxError: invalid syntax
发生这个错误是因为5 +
不是一个表达式。具有多个值的表达式需要操作符来连接这些值,而在 Python 语言中,+
操作符期望连接两个值。一个语法错误意味着计算机不理解你给它的指令,因为你打错了。这似乎并不重要,但计算机编程不仅仅是告诉计算机做什么——它还涉及知道如何正确地给计算机发出它可以遵循的指令。
错误是可以接受的!
犯错误是完全可以的!您不会因为输入错误的代码而导致损坏您的计算机。Python 会简单地告诉你发生了一个错误,然后再次显示>>>提示符。您可以继续在交互式 shell 中输入新代码。
在您获得更多编程经验之前,错误消息可能对您没有太大意义。但是,您总是可以在 google 上搜索错误消息文本,以找到解释该特定错误的网页。你也可以去www.nostarch.com/crackingcodes
查看常见 Python 错误消息列表及其含义。
变量值
程序通常需要存储值,以便在后续程序中使用。您可以通过使用=
符号(称为赋值操作符(将值存储在变量中。例如,要将值15
存储在名为spam
的变量中,在 shell 中输入spam = 15
:
>>> spam = 15
你可以把变量想象成一个盒子,里面有值15
(如图 2-3 (。变量名spam
是盒子上的标签(这样我们就可以区分一个变量和另一个变量),存储在里面的值就像盒子里面的一张纸条。
当你按下Enter
键时,你将不会看到任何东西,除了一个空行作为回应。除非你看到错误消息,否则你可以认为指令执行成功。出现下一个>>>
提示,以便您可以输入下一条指令。
这个带有=
赋值操作符的指令(称为赋值语句(创建变量spam
并将值15
存储在其中。与表达式不同,声明语句是不计算任何值的指令;相反,他们只是执行一个动作。这就是 shell 中的下一行不显示任何值的原因。
弄清楚哪些指令是表达式,哪些是语句可能会令人困惑。请记住,如果 Python 指令的计算结果是单个值,那么它就是一个表达式。如果没有,那就是声明语句。
图 2-3:变量就像有名字的盒子,可以装值。
赋值声明语句写成变量,后面是=
运算符,再后面是表达式,如图 2-4 所示。表达式计算的值存储在变量中。
图 2-4:赋值语句的组成部分
请记住,变量存储的是单个值,而不是分配给它们的表达式。例如,如果您输入语句spam = 10 + 5
,表达式10 + 5
首先被计算为15
,然后值15
被存储在变量spam
中,我们可以通过在 shell 中输入变量名看到:
>>> spam = 10 + 5 >>> spam 15
变量本身是一个由表达式计算的结果并存储在变量中的值。值本身也可以是一个对自身求值的表达式:
>>> 15 15
这里有一个有趣的转折。如果您现在在 shell 中输入spam + 5
,您将得到整数20
:
>>> spam = 15 >>> spam + 5 20
如你所见,变量可以像值一样用在表达式中。因为spam
的值是15
,所以表达式spam + 5
计算为表达式15 + 5
,然后表达式15 + 5
计算为20
。
覆盖变量
您可以通过输入另一个赋值声明语句来更改存储在变量中的值。例如,输入以下内容:
>>> spam = 15 >>> spam + 5 # ➊ 20 # ➋ >>> spam = 3 # ➌ >>> spam + 5 # ➍ ➎ 8
第一次输入spam + 5
➊ 时,表达式的计算结果为20
➋,因为你在变量spam
中存储了值15
。但当你输入spam = 3
➌ 时,数值15
被数值3
覆盖(即替换),如图 2-5 所示。现在当你输入spam + 5
➍ 时,表达式计算结果为8
➎ ,因为spam + 5
计算结果为3 + 5
。spam
中的旧值被遗忘。
图 2-5:变量spam
中的值 15 被值 3 覆盖。
您甚至可以使用spam
变量中的值给spam
分配一个新值:
>>> spam = 15 >>> spam = spam + 5 >>> spam 20
赋值语句spam = spam + 5
告诉计算机“变量spam
的新值是当前值spam
加五。”=
符号左侧的变量被赋予右侧表达式的值。您可以将spam
中的值变大通过几次5
的加法运算:
>>> spam = 15 >>> spam = spam + 5 >>> spam = spam + 5 >>> spam = spam + 5 >>> spam 30
每次执行spam = spam + 5
时,spam
中的值都会改变。存储在spam
中的值最终是30
。
变量名
尽管计算机不关心你给变量取什么名字,但你应该这样做。给变量起一个能反映它们所包含的数据类型的名字,这样更容易理解程序是做什么的。你可以给你的变量取像abrahamLincoln
或monkey
这样的名字,即使你的程序与亚伯拉罕·林肯或猴子无关——计算机仍然会运行程序(只要你一直使用abrahamLincoln
或monkey
(。但是当你在很长一段时间没有看到一个程序时,你可能不记得每个变量是做什么的。
一个好的变量名描述了它包含的数据。想象一下,你搬到了一所新房子,并给你所有的搬家箱子贴上标签东西。你永远找不到任何东西!变量名spam
、eggs
、bacon
等等(受《Monty Python》的“Spam”草图的启发)在本书和 Python 的大部分文档中用作示例的通用名称,但是在您的程序中,描述性名称有助于使您的代码更具可读性。
变量名(以及 Python 中的其他东西)是区分大小写的。区分大小写意味着不同大小写的相同变量名被认为是完全不同的变量。例如,spam
、SPAM
、Spam
和sPAM
在 Python 中被认为是四个不同的变量。它们都可以包含各自独立的值,不能互换使用。
总结
那么我们什么时候开始制作加密程序呢?很快。但是在你破解密码之前,你需要学习一些基本的编程概念,所以你还需要读几章编程的章节。
在本章中,您学习了在交互式 shell 中编写 Python 指令的基础。Python 需要你准确地告诉它以它期望的方式做什么,因为计算机只理解非常简单的指令。您了解了 Python 可以对表达式求值(即将表达式简化为单个值),表达式是值(如2
或5
(与运算符(如+
或-
(的组合。您还了解了可以将值存储在变量中,这样您的程序就可以记住它们以便以后使用。
交互式 shell 是学习 Python 指令的有用工具,因为它允许你一次输入一个指令并查看结果。在第 3 章中,你将创建包含许多指令的程序,这些指令是按顺序执行的,而不是一次执行一个。我们将讨论一些更基本的概念,你将编写你的第一个程序!
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 哪个是除法运算符,
/
还是\
? - 下面哪个是整数值,哪个是浮点数值?
42 3.141592
- 以下哪几行不是表达式?
4 x 10 + 2 3 * 7 + 1 2 + 42 2 + 2 spam = 42
- 如果你在交互式 shell 中输入下面几行代码,➊和➋会打印出什么?
spam = 20 ➊ spam + 20 SPAM = 30 ➊ spam
三、字符串和编写程序
“亲手编写程序代码是学习一门新编程语言的唯一方法。”
——布莱恩·克尼根和丹尼斯·里奇,C 语言
第 2 章给了你足够的整数和数学知识,但是 Python 不仅仅是一个实现计算器功能。因为加密就是通过将明文转换成密文来处理文本值,所以在本章中,您将学习如何存储、组合和在屏幕上显示文本。您还将编写第一个程序,用文本"Hello, world!"
来问候用户,并让用户输入他们的名字。
本章涵盖的主题
- 字符串
- 字符串连接和复制
- 索引和切片
print()
函数- 用 IDLE 写源代码
- 在 IDLE 中保存和运行程序
- 注释
input()
函数
使用字符串值处理文本
在 Python 中,我们处理被称为字符串值的小段文本(或者简称为字符串(。我们所有的加密和破解程序都处理字符串值,将类似于'One if by land, two if by space'
的明文转换成类似于'
b1rJvsJo
!Jyn1q,J702JvsJo!J63nprM'
的密文。在我们的程序中,明文和密文在我们的程序中都被表示为字符串值,Python 代码可以通过多种方式操作这些值。
您可以将字符串值存储在变量中,就像存储整数和浮点值一样。当您键入一个字符串时,将它放在两个单引号('
(之间,以显示该字符串的开始和结束位置。在交互式 shell 中输入以下内容:
>>> spam = 'hello'
单引号不是字符串值的一部分。Python 知道'hello'
是一个字符串,spam
是一个变量,因为字符串用引号括起来,而变量名没有。
如果您在 shell 中输入spam
,您将看到spam
变量的内容('hello'
字符串):
>>> spam = 'hello' >>> spam 'hello'
这是因为 Python 的变量赋值是将值存储在变量中:在本例中,是字符串'hello'
。字符串中几乎可以包含任何键盘字符。这些都是字符串的例子:
>>> 'hello' 'hello' >>> 'KITTENS' 'KITTENS' >>> '' '' >>> '7 apples, 14 oranges, 3 lemons' '7 apples, 14 oranges, 3 lemons' >>> 'Anything not pertaining to elephants is irrelephant.' 'Anything not pertaining to elephants is irrelephant.' >>> 'O*&#wY%*&OcfsdYO*&gfC%YO*&%3yc8r2' 'O*&#wY%*&OcfsdYO*&gfC%YO*&%3yc8r2'
请注意,''
字符串中没有字符;单引号之间没有任何内容。这被称为空串或空字符串。
用+
运算符连接字符串
您可以使用+
操作符把两个字符串值连接起来创建一个新字符串。这样做被称为字符串连接。在 Shell 中输入'Hello,' + 'world!'
:
>>> 'Hello,' + 'world!' 'Hello,world!'
Python 准确地把你需要连接的字符串连接起来,所以当你连接它们时,它不会在字符串之间加一个空格。如果希望结果字符串中有一个空格,那么必须在两个原始字符串中有一个空格。要在'Hello,'
和'world!'
之间加一个空格,可以在'Hello,'
字符串的末尾和第二个单引号之前加一个空格,如下所示:
>>> 'Hello, ' + 'world!' 'Hello, world!'
+
操作符可以将两个字符串值连接成一个新的字符串值('
Hello, ' + 'world!'
到'Hello, world!'
(,就像它可以将两个整数值相加得到一个新的整数值(2 + 2
到4
(。由于值的数据类型,Python 知道+
操作符应该做什么。正如你在第 2 章中学到的,一个值的数据类型告诉我们(和计算机)这个值是什么类型的数据。
只要数据类型匹配,就可以在带有两个或更多字符串或整数的表达式中使用+
运算符。如果你尝试使用一个字符串和一个整数的操作符,你会得到一个错误。在交互式 shell 中输入以下代码:
>>> 'Hello' + 42 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: must be str, not int >>> 'Hello' + '42' 'Hello42'
第一行代码导致错误,因为'Hello'
是一个字符串,而42
是一个整数。但是在第二行代码中,'42'
是一个字符串,所以 Python 将其连接起来。
用*
运算符复制字符串
你也可以在一个字符串和一个整数之间使用*
操作符来做字符串复制。这会将字符串复制(即重复)整数值的任意倍。在交互式 shell 中输入以下内容:
>>> 'Hello' * 3 # ➊ 'HelloHelloHello' >>> spam = 'Abcdef' >>> spam = spam * 3 # ➋ >>> spam 'AbcdefAbcdefAbcdef'
要复制一个字符串,输入字符串,然后输入*
操作符,然后输入您希望字符串重复的次数 ➊。你也可以存储一个字符串,就像我们对spam
变量所做的那样,然后复制这个变量来代替 ➋。您甚至可以将复制的字符串存储回同一个变量或新变量中。
正如你在第 2 章的中看到的,*
操作符可以处理两个整数值,将它们相乘。但是它不能处理两个字符串值,这将导致如下错误:
>>> 'Hello' * 'world!' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't multiply sequence by non-int of type 'str'
字符串连接和字符串复制表明,Python 中的运算符可以根据它们所操作的值的数据类型执行不同的任务。+
操作符可以做加法或字符串连接。*
操作符可以做乘法或字符串复制。
使用索引从字符串中获取字符
您的加密程序经常需要从字符串中获取单个字符,这可以通过索引来完成。使用索引,您可以在字符串值(或包含字符串的变量)的末尾添加方括号[
和]
,它们之间有一个数字,以访问一个字符。这个数字被称为索引,它告诉 Python 字符串中的哪个位置有你想要的字符。Python 索引从0
开始,所以字符串中第一个字符的索引是0
。索引1
用于第二个字符,索引2
用于第三个字符,依此类推。
在交互式 shell 中输入以下内容:
>>> spam = 'Hello' >>> spam[0] 'H' >>> spam[1] 'e' >>> spam[2] 'l'
图 3-1:字符串hello
及其索引
请注意,表达式spam[0]
的计算结果是字符串值'H'
,因为H
是字符串'Hello'
的第一个字符,索引从0
开始,而不是1
(参见图 3-1)。
您可以对包含字符串值的变量使用索引,就像我们在前面的示例中所做的那样,或者对字符串值本身使用索引,如下所示:
>>> 'Zophie'[2] 'p'
表达式'Zophie'[2]
计算出第三个字符串值,即'p'
。这个'p'
字符串就像任何其他字符串值一样,可以存储在变量中。在交互式 shell 中输入以下内容:
>>> eggs = 'Zophie'[2] >>> eggs 'p'
如果输入的索引对于字符串来说太大,Python 会显示一条"index out of range"
错误消息,如下面的代码所示:
>>> 'Hello'[10] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: string index out of range
字符串'Hello'
中有五个字符,所以如果您试图使用索引10
,Python 会显示一个错误。
图 3-2:字符串hello
及其负索引
负索引
负索引从一个字符串的末尾开始,向后。负索引-1
是字符串中最后一个字符的索引。索引-2
是倒数第二个字符的索引,以此类推,如图 3-2 所示。
在交互式 shell 中输入以下内容:
>>> 'Hello'[-1] 'o' >>> 'Hello'[-2] 'l' >>> 'Hello'[-3] 'l' >>> 'Hello'[-4] 'e' >>> 'Hello'[-5] 'H' >>> 'Hello'[0] 'H'
注意-5
和0
是同一字符的索引。大多数情况下,您的代码将使用正索引,但有时使用负索引更容易。
使用切片从字符串中获取多个字符
如果你想从一个字符串中得到多个字符,你可以使用切片来代替索引。切片也使用[
和]
方括号,但是有两个整数索引而不是一个。这两个索引由冒号(:
(分隔,并告诉 Python 切片中第一个和最后一个字符的索引。在交互式 shell 中输入以下内容:
>>> 'Howdy'[0:3] 'How'
切片计算的字符串从第一个索引值开始,一直到第二个索引值,但不包括第二个索引值。字符串值'Howdy'
的索引0
为H
,索引3
为d
。因为切片向上但不包括第二个索引,所以切片'Howdy'[0:3]
的结果为字符串值'How'
。
在交互式 shell 中输入以下内容:
>>> 'Hello, world!'[0:5] 'Hello' >>> 'Hello, world!'[7:13] 'world!' >>> 'Hello, world!'[-6:-1] 'world' >>> 'Hello, world!'[7:13][2] 'r'
请注意,表达式'Hello, world!'[7:13][2]
首先将列表片求值为'world!'[2]
,然后进一步求值为'r'
。
与索引不同,如果您为字符串提供太大的索引值,切片仍然不会出错。它将尽可能返回最宽的匹配切片:
>>> 'Hello'[0:999] 'Hello' >>> 'Hello'[2:999] 'llo' >>> 'Hello'[1000:2000] ''
表达式'Hello'[1000:2000]
返回一个空字符串,因为索引1000
在该字符串的末尾,所以这个切片不可能包含任何字符。虽然我们的例子没有显示这一点,但是您也可以对存储在变量中的字符串进行切片。
空白切片索引
如果省略一个切片中的第一个索引值,Python 将自动使用索引0
作为第一个索引值。表达式'Howdy'[0:3]
和'Howdy'[:3]
计算出相同的字符串:
>>> 'Howdy'[:3] 'How' >>> 'Howdy'[0:3] 'How'
如果省略第二个索引值,Python 将从第一个索引值开始自动使用字符串的其余部分:
>>> 'Howdy'[2:] 'wdy'
您可以通过许多不同的方式使用空白索引。在 shell 中输入以下内容:
>>> myName = 'Zophie the Fat Cat' >>> myName[-7:] 'Fat Cat' >>> myName[:10] 'Zophie the' >>> myName[7:] 'the Fat Cat'
正如您所看到的,您甚至可以在空白索引中使用负索引。因为在第一个例子中-7
是起始索引,Python 从末尾向后计数 7 个字符,并将其作为起始索引。然后,由于第二个空索引,它返回从该索引到字符串末尾的所有内容。
用print()
函数打印数值
让我们尝试另一种类型的 Python 指令:一个print()
函数调用。在交互式 shell 中输入以下内容:
>>> print('Hello!') Hello! >>> print(42) 42
一个函数(就像本例中的print()
)内部有执行任务的代码,比如在屏幕上打印数值。Python 附带了许多不同的函数,可以为您执行有用的任务。调用一个函数意味着执行函数内部的代码。
本例中的指令将一个值传递给圆括号之间的print()
,而print()
函数将该值打印到屏幕上。调用函数时传递的值是参数。当你写程序时,你会用print()
让文本出现在屏幕上。
您可以向print()
传递一个表达式,而不是单个值。这是因为实际传递给print()
的值是该表达式的计算值。在交互 shell 中输入以下字符串连接表达式:
>>> spam = 'Al' >>> print('Hello, ' + spam) Hello, Al
'Hello, ' + spam
表达式计算出'Hello, ' + 'Al'
,然后再计算出字符串值'Hello, Al'
。这个字符串值就是传递给print()
调用的内容。
打印转义字符
您可能希望在字符串值中使用一个会使 Python 混淆的字符。例如,您可能希望使用单引号字符作为字符串的一部分。但是你会得到一个错误消息,因为 Python 认为单引号是结束字符串值和文本的引号,而不是字符串的其余部分。在交互式 shell 中输入以下内容,查看操作中的错误:
>>> print('Al's cat is named Zophie.') SyntaxError: invalid syntax
要在字符串中使用单引号,需要使用一个转义字符。转义字符是反斜杠字符后跟另一个字符,例如,\t
、\n
或\'
。斜杠告诉 Python 斜杠后面的字符有特殊的含义。在交互式 shell 中输入以下内容。
>>> print('Al\'s cat is named Zophie.') Al's cat is named Zophie.
现在 Python 知道撇号是字符串值中的一个字符,而不是标记字符串结尾的 Python 代码。
表 3-1 显示了一些你可以在 Python 中使用的转义字符。
表 3-1: 转义字符
转义字符 | 打印结果 |
\\ |
反斜杠(\ ) |
\' |
单引号(' ) |
\" |
双引号(" ) |
\n |
新行 |
\t |
制表符 |
反斜杠总是在转义字符之前。即使你只想在字符串中加一个反斜杠,也不能只加一个反斜杠,因为 Python 会把下一个字符解释为转义字符。例如,这行代码不能正常工作:
>>> print('It is a green\teal color.') It is a green eal color.
'teal'
中的't'
被识别为转义字符,因为它跟在反斜杠后面。转义字符\t
模拟按下键盘上的Tab
键。
相反,请输入以下代码:
>>> print('It is a green\\teal color.') It is a green\teal color.
这一次,字符串将按照您的意图打印,因为在字符串中添加第二个反斜杠会使反斜杠成为转义字符。
引号和双引号
在 Python 中,字符串不必总是在两个单引号之间。你可以用双引号来代替。这两行打印了相同的内容:
>>> print('Hello, world!') Hello, world! >>> print("Hello, world!") Hello, world!
但是不能把单引号和双引号混在一起。这一行给你一个错误:
>>> print('Hello, world!") SyntaxError: EOL while scanning string literal
我更喜欢使用单引号,因为它们比双引号更容易输入,而且 Python 不在乎这两种方式。
但是就像你必须使用转义符\'
在单引号包围的字符串中使用单引号一样,你需要转义符\"
在双引号包围的字符串中使用双引号。例如,看这两行:
>>> print('Al\'s cat is Zophie. She says, "Meow."') Al's cat is Zophie. She says, "Meow." >>> print("Zophie said, \"I can say things other than 'Meow' you know.\"") Zophie said, "I can say things other than 'Meow' you know."
单引号字符串中不需要转义双引号,双引号字符串中也不需要转义单引号。Python 解释器非常聪明,它知道如果一个字符串以一种引号开始,另一种引号并不意味着该字符串结束。
在 IDLE 的文件编辑器中编写程序
到目前为止,您一直在交互 shell 中一次输入一条指令。但是当你写程序的时候,你会输入几个指令并让它们运行,而不用等你下一个指令。是时候写你的第一个程序了!
提供交互 shell 的软件程序的名字叫做 IDLE(集成开发环境)。除了交互 shell,IDLE 还有一个文件编辑器,我们现在就打开。
在 Python shell 窗口顶部,选择文件 -> 新建窗口。将出现一个新的空白窗口,即文件编辑器,供您输入程序,如图 3-3 所示。文件编辑器窗口的右下角显示了光标当前所在的行和列。
图 3-3:光标位于文件编辑器窗口的第 1 行第 0 列
您可以通过寻找>>>
提示符来区分文件编辑器窗口和交互式 shell 窗口。交互式 shell 总是显示提示,而文件编辑器不显示。
'Hello, world!'
程序的源代码
传统上,正在学习一门新语言的程序员让他们的第一个程序在屏幕上显示文本"Hello, world!"
。我们将创造我们自己的"Hello, world!"
通过在新的文件编辑器窗口中输入文本来进行下一步编程。我们称这个文本为程序的源代码,因为它包含了 Python 将遵循的指令,以确定程序应该如何运行。
你可以下载“你好,世界!”源代码来自www.nostarch.com/crackingcodes
。如果输入代码后出现错误,使用在线比较工具将它与书中的代码进行比较(参见“使用在线比较工具检查源代码”下一节)。记住不要输入行号;它们出现在本书中只是为了帮助解释。
hello.py
# This program says hello and asks for my name. print('Hello, world!') print('What is your name?') myName = input() print('It is good to meet you, ' + myName)
IDLE 将以不同的颜色显示不同类型的指令。当你输入完这段代码后,窗口应该看起来像图 3-4 。
图 3-4:文件编辑器窗口在你输入代码后会是这个样子。
使用在线比较工具检查源代码
即使你可以从这本书的网站上复制粘贴或下载hello.py
代码,你仍然应该手动输入这个程序。这样做会让你更熟悉程序中的代码。但是,在将它输入文件编辑器时,您可能会犯一些错误。
要将您键入的代码与本书中的代码进行比较,请使用图 3-5 所示的在线比较工具。复制你的代码文本,然后在本书网站的www.nostarch.com/crackingcodes
导航到差异工具。从下拉菜单中选择hello.py
程序。将您的代码粘贴到该网页的文本字段中,然后单击比较按钮。差异工具显示您的代码和本书中的代码之间的任何差异。这是找到程序中任何导致错误的打字错误的简单方法。
*
图 3-5:在线比较工具
稍后使用 IDLE 打开您的程序
当你写程序时,你可能想保存它们,以后再来看,特别是在你输入了一个很长的程序之后。IDLE 具有保存和打开程序的功能,就像文字处理器具有保存和重新打开文档的功能一样。
保存你的程序
输入源代码后,请保存它,这样您就不必在每次想要运行它时重新键入它。从文件编辑器窗口顶部的菜单中选择文件 -> 另存为。另存为对话框应该打开,如图 3-6 中的所示。在文件名字段输入hello.py
,点击保存。
图 3-6:保存程序
你应该在打字的时候经常保存你的程序,这样你就不会在电脑崩溃或者意外退出 IDLE 时丢失你的工作程序。作为一种快捷方式,你可以在 Windows 和 Linux 上按下ctrl+S
或者在 macOS 上按下Cmd+S
来保存你的文件。
运行你的程序
现在是时候运行你的程序了。选择运行 -> 运行模块或者直接按键盘上的 F5 键。你的程序应该在你第一次启动 IDLE 出现的 shell 窗口中运行。请记住,您必须在文件编辑器的窗口中按F5
键,而不是在交互式 shell 的窗口中。
当程序询问你的名字时,输入你的名字,如图 3-7 所示。
图 3-7:运行'Hello, world!'
程序。
现在当你按下enter,
的时候,程序应该会点名问候你(用户,也就是使用程序的那个人)。恭喜你!你写了你的第一个程序。你现在是一名初级电脑程序员。(如果您愿意,可以通过再次按F5
再次运行该程序。)
如果您得到如下所示的错误,这意味着您正在使用 Python 2 而不是 Python 3 运行程序:
Hello, world! What is your name? Albert Traceback (most recent call last): File "C:/Python27/hello.py", line 4, in <module> myName = input() File "<string>", line 1, in <module> NameError: name 'Albert' is not defined
该错误是由input()
函数调用引起的,它在 Python 2 和 3 中的执行方式不同。在继续之前,按照第 xxv 页的下载并安装 Python 中的说明安装 Python 3。
打开你保存的程序
单击右上角的 X 关闭文件编辑器。要重新加载已保存的程序,从菜单中选择文件 -> 打开。现在就这样做,在出现的窗口中,选择hello.py
。然后点击打开按钮。您保存的hello.py
程序应该会在文件编辑器窗口中打开。
'Hello, world!'
程序是怎样工作的
'Hello, world!'
程序中的每一行是一个指令,它确切地告诉 Python 该做什么。计算机程序很像一个食谱。先做第一步,再做第二步,以此类推,直到最后。当程序一步一步地遵循指令时,我们称之为程序执行,或简称为执行。
每条指令都是按顺序执行的,从程序的顶部开始,沿着指令列表向下。执行从第一行代码开始,然后向下移动。但是执行也可以跳过,而不只是从上到下;你将在第 4 章中找到如何做到这一点。
让我们看看'Hello, world!'
从第 1 行开始,一次编执行一行程序,看看它在做什么。
注释
任何跟在井号(#
)后面的文本都是注释:
# This program says hello and asks for my name.
注释不是给计算机的,而是给你这个程序员的。电脑会忽略它们。它们用来提醒你程序是做什么的,或者告诉其他可能看你代码的人你的代码是做什么的。
程序员通常会在代码的顶部加上注释,给程序起一个标题。IDLE 程序用红色文本显示注释,以帮助它们突出出来。有时候,程序员会在一行代码前加一个#
,在测试程序时临时跳过。这被称为注释代码,当你试图找出一个程序不工作的原因时,这很有用。当您准备好将被注释的代码行放回原处时,可以移除#
。
给用户打印提示
接下来的两行用print()
函数向用户显示提示。函数就像程序中的迷你程序。使用函数的最大好处是,我们只需要知道函数做什么,而不需要知道它是如何做的。例如,您需要知道print()
在屏幕上显示文本,但是您不需要知道函数内部的确切代码。函数调用是一段代码,它告诉程序在函数内部运行代码。
hello.py
的第 2 行是对print()
的调用(括号内是要打印的字符串)。3 号线是另一个print()
调用。这次程序显示'What is your name?'
print('Hello, world!') print('What is your name?')
我们在函数名的末尾添加括号,以清楚地表明我们引用的是名为print()
的函数,而不是名为print
的变量。函数末尾的括号告诉 Python 我们正在使用一个函数,就像数字'42'
周围的引号告诉 Python 我们正在使用字符串'42'
,而不是整数42
。
获取用户的输入
第 4 行有一个带有变量(myName
)和新函数调用input()
的赋值语句:
myName = input()
当input()
被调用时,程序等待用户输入一些文本并按下enter
。用户输入的文本字符串(他们的名字)成为存储在myName
中的字符串值。
像表达式一样,函数调用赋予为单个值。调用赋予的值被称为返回值。(事实上,我们也可以用“返回”这个词来表示函数调用的“求值”。)在这种情况下,input()
的返回值是用户输入的字符串,应该是他们的名字。如果用户输入了Albert
,那么input()
调用将计算(即返回)字符串'Albert'
。
与print()
不同,input()
函数不需要任何参数,这就是圆括号之间没有任何内容的原因。
hello.py
中的最后一行代码是另一个print()
调用:
print('It is good to meet you, ' + myName)
对于第 5 行的print()
调用,我们使用加号运算符(+
)连接字符串'It is good to meet you, '
和存储在myName
变量中的字符串,这是用户输入到程序中的名称。这就是我们如何让程序通过名字问候用户。
结束程序
当程序执行最后一行时,它就停止了。此时它已经终止或者退出,所有的变量都被计算机遗忘,包括存储在myName
中的字符串。如果您尝试再次运行该程序并输入不同的名称,它将打印该名称。
Hello, world! What is your name? Zophie It is good to meet you, Zophie
请记住,计算机只做你通过编程让它做的事情。在这个程序中,它会询问您的姓名,让您输入一个字符串,然后打招呼并显示您输入的字符串。
但是电脑是愚蠢的。这个程序不在乎你输入你的名字,别人的名字,或者只是一些愚蠢的东西。你可以输入任何你想输入的东西,计算机会以同样的方式处理它:
Hello, world! What is your name? poop It is good to meet you, poop
总结
写程序只是学习如何说计算机的语言。在第 2 章中,您已经了解了一些如何做这件事,现在您已经将几个 Python 指令组合在一起,制作了一个完整的程序,它会询问用户的名字并问候用户。
在这一章中,你学习了一些操作字符串的新技术,比如使用+
操作符来连接字符串。您还可以使用索引和切片从不同字符串的一部分创建新字符串。
本书中的其余程序将更加复杂和精密,但它们都将被逐行解释。你总是可以在交互式 shell 中输入指令,在你把它们放进一个完整的程序之前,看看它们做了什么。
接下来,我们将开始编写我们的第一个加密程序:反向密码。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 如果赋值
spam = 'Cats'
,下面几行打印什么?
spam + spam + spam spam * 3
- 下面几行打印的是什么?
print("Dear Alice,\nHow are you?\nSincerely,\nBob") print('Hello' + 'Hello')
- 如果你赋值
spam = 'Four score and seven years is eighty seven years.'
,下面的每一行会打印什么?
print(spam[5]) print(spam[-3]) print(spam[0:4] + spam[5]) print(spam[-3:-1]) print(spam[:10]) print(spam[-5:]) print(spam[:])
- 哪个窗口显示
>>>
提示符,交互式 shell 还是文件编辑器? - 下面一行打印了什么?
#print('Hello, world!')
四、反向密码
“每个人都被周边一群志愿间谍包围着。”
——简·奥斯汀,诺桑觉寺
反向密码通过以相反的顺序打印消息来实现消息的加密。所以'Hello, world!'
加密成"!dlrow ,olleH"
。要解密或获取原始消息,只需反转加密的消息即可。加密和解密步骤是相同的。
然而,这种反向密码很弱,很容易就能猜出明文。只要看一看密文,你就可以知道消息是逆序的。
.syas ti tahw tuo erugif llits ylbaborp nac uoy ,detpyrcne si siht hguoht neve ,elpmaxe roF
但是反向密码程序的代码很容易解释,所以我们将把它作为我们的第一个加密程序。
本章涵盖的主题
len()
函数while
循环- 布尔数据类型
- 比较运算符
- 条件判断
- 块
反向密码程序的源代码
在 IDLE 中,点击文件 -> 新建窗口创建一个新的文件编辑窗口。输入下面的代码,保存为reverseCipher.py
,按F5
运行它,但是记住不要在每行之前键入数字:
reverseCipher.py
# Reverse Cipher # https://www.nostarch.com/crackingcodes/ (BSD Licensed) message = 'Three can keep a secret, if two of them are dead.' translated = '' i = len(message) - 1 while i >= 0: translated = translated + message[i] i = i - 1 print(translated)
反向密码程序的运行示例
当您运行reverseCipher.py
程序时,输出如下:
.daed era meht fo owt fi ,terces a peek nac eerhT
要解密此消息,通过高亮显示消息并按下 Windows 和 Linux 上的ctrl + C
或 macOS 上的Cmd + C
,将.daed era meht fo owt fi ,terces a peek nac eerhT
文本复制到剪贴板。然后将其粘贴(在 Windows 和 Linux 上使用ctrl + V
或者在 macOS 上使用Cmd+V
为第 4 行message
中存储的字符串值。确保在字符串的开头和结尾保留单引号。新的第 4 行看起来像这样(用粗体表示的变化):
message = '.daed era meht fo owt fi ,terces a peek nac eerhT'
现在,当您运行reverseCipher.py
程序时,输出解密后的原始消息:
Three can keep a secret, if two of them are dead.
设置注释和变量
reverseCipher.py
中的前两行是注释,解释了程序是什么以及可以找到它的网站。
# Reverse Cipher # https://www.nostarch.com/crackingcodes/ (BSD Licensed)
BSD Licensed
部分意味着这个程序可以被任何人自由复制和修改,只要这个程序保留了原作者的版权(在这种情况下,这本书的网站在第二行的https://www.nostarch.com/crackingcodes/
)。我喜欢把这些信息放在文件里,这样如果它在网上被复制,下载它的人总是知道去哪里找原始资料。他们还会知道这个程序是开源软件,可以免费分发给其他人。
第 3 行只是一个空行,Python 跳过了。第 4 行将我们想要加密的字符串存储在一个名为message
的变量中:
message = 'Three can keep a secret, if two of them are dead.'
每当我们想要加密或解密一个新的字符串时,我们只需将该字符串直接输入到第 4 行的代码中。
第 5 行的translated
变量是我们的程序存储反转字符串的地方:
translated = ''
在程序开始时,translated
变量包含这个空白字符串。(请记住,空白字符串是两个单引号字符,而不是一个双引号字符。)
获取字符串的长度
第 7 行是在名为i
的变量中存储一个值的赋值语句:
i = len(message) - 1
变量中计算和存储的表达式是len(message) - 1
。这个表达式的第一部分len(message)
,是对len()
函数的函数调用,它接受一个字符串参数,就像print()
一样,并返回一个字符串中有多少字符的整数值(即字符串的长度)。在这种情况下,我们将message
变量传递给len()
,因此len(message)
返回存储在message
中的字符串值中有多少个字符。
让我们在交互式 shell 中试验一下len()
函数。在交互式 shell 中输入以下内容:
>>> len('Hello') 5 >>> len('') 0 >>> spam = 'Al' >>> len(spam) 2 >>> len('Hello,' + ' ' + 'world!') 13
从len()
的返回值中,我们知道字符串'Hello'
中有五个字符,空白字符串中没有字符。如果我们将字符串'Al'
存储在一个变量中,然后将该变量传递给len()
,该函数将返回2
。如果我们将表达式'Hello,' + ' ' + 'world!'
传递给len()
函数,它将返回13
。原因是'Hello,' + ' ' + 'world!'
的计算结果是字符串值'Hello, world!'
,其中有 13 个字符。(空格和感叹号算作字符。)
既然你已经理解了len()
函数是如何工作的,让我们回到reverseCipher.py
程序的第 7 行。第 7 行通过从len(message)
中减去 1 找到message
中最后一个字符的索引。它必须减去 1,因为例如像'Hello'
这样的 5 个字符长度的字符串的索引是从 0 到 4。这个整数然后被存储在i
变量中。
while
循环介绍
第 8 行是一种 Python 指令,称为while
循环或while
语句:
while i >= 0:
一个while
循环由四部分组成(如图 4-1 中所示)。
图 4-1:while
循环部分
一个条件是一个用在while
语句中的表达式。只要条件为真,while
语句中的代码块就会执行。
为了理解while
循环,你首先需要了解布尔、比较运算符和块。
布尔数据类型
布尔数据类型只有两个值:True
或False
。这些布尔值,或者说bools
,是区分大小写的(你总是需要大写T
和F
,而让其余的小写)。它们不是字符串值,所以不要用引号将True
或False
括起来。
通过在交互式 shell 中输入以下内容来尝试一些布尔值:
>>> spam = True >>> spam True >>> spam = False >>> spam False
像任何其他数据类型的值一样,布尔值可以存储在变量中。
比较运算符
在reverseCipher.py
程序的第 8 行,看看while
关键字后的表达式:
while i >= 0:
跟在while
关键字后面的表达式(i >= 0
部分)包含由>=
符号连接的两个值(变量i
中的值和整数值0
),称为“大于或等于”运算符。>=
运算符是一个比较运算符。
我们使用比较运算符来比较两个值,并计算出一个True
或False
布尔值。表 4-1 列出了比较运算符。
表 4-1: 比较运算符
运算符标识 | 运算符名称 |
< |
小于 |
> |
大于 |
<= |
小于或等于 |
>= |
大于或等于 |
== |
等于 |
!= |
不等于 |
在交互式 shell 中输入以下表达式,查看它们计算出的布尔值:
>>> 0 < 6 True >>> 6 < 0 False >>> 50 < 10.5 False >>> 10.5 < 11.3 True >>> 10 < 10 False
表达式0 < 6
返回布尔值True
,因为数字0
小于数字6
。但是因为6
不小于0
,所以表达式6 < 0
的计算结果为False
。表达式50 < 10.5
是False
,因为50
不小于10.5
。表达式10 < 11.3
的计算结果为True
,因为10.5
小于11.3
。
再看10 < 10
。因为数字10
不小于数字10
,所以是False
。他们完全一样。(如果爱丽丝和鲍勃一样高,你就不会说爱丽丝比鲍勃矮了。这种说法是错误的。)
使用<=
(小于或等于)和>=
(大于或等于)运算符输入一些表达式:
>>> 10 <= 20 True >>> 10 <= 10 True >>> 10 >= 20 False >>> 20 >= 20 True
注意10 <= 10
是True
,因为运算符检查 10 是否小于或等于 10。请记住,对于“小于或等于”和“大于或等于”运算符,<
或>
符号总是在=
符号之前。
现在在 shell 中输入一些使用了==
(等于)和!=
(不等于)运算符的表达式,看看它们是如何工作的:
>>> 10 == 10 True >>> 10 == 11 False >>> 11 == 10 False >>> 10 != 10 False >>> 10 != 11 True
这些运算符的工作方式和你对整数的预期一样。用==
运算符比较彼此相等的整数,结果为True
,不相等的值为False
。当你和!=
运算符比较的时候,就相反了。
字符串比较的工作方式类似:
>>> 'Hello' == 'Hello' True >>> 'Hello' == 'Goodbye' False >>> 'Hello' == 'HELLO' False >>> 'Goodbye' != 'Hello' True
大小写对 Python 很重要,所以大小写不完全匹配的字符串值不是同一个字符串。例如,字符串 ’ Hello
’ 和 ’ HELLO
’ 彼此不相等,因此将它们与==
进行比较得出False
。
注意赋值运算符(=
)和“等于”比较运算符(==
)之间的区别。单等号(=
)用于给变量赋值,双等号(==
)用于表达式中检查两个值是否相同。如果你问 Python 两个东西是否相等,用==
。如果你告诉 Python 给一个变量设置一个值,使用=
。
在 Python 中,字符串和整数值总是被认为是不同的值,永远不会彼此相等。例如,在交互式 shell 中输入以下内容:
>>> 42 == 'Hello' False >>> 42 == '42' False >>> 10 == 10.0 True
尽管它们看起来很像,但整数42
和字符串'42'
并不相等,因为字符串和数字不一样。整数和浮点数可以彼此相等,因为它们都是数字。
当您使用比较运算符时,请记住每个表达式总是计算出一个True
或False
值。
代码块
一个块是一行或多行代码,用相同的最小缩进量(即该行前面的空格数)组合在一起。
当一行缩进四个空格时,一个块开始。任何后面的行也缩进至少四个空格,是块的一部分。当一行缩进另外四个空格时(该行前面总共有八个空格),一个新的块从第一块开始。当有一行代码的缩进与块开始前的缩进相同时,块结束。
让我们看一些假想的代码(代码是什么并不重要,因为我们只关注每行的缩进)。缩进的空格在这里用灰点代替,以便于计数。
可以看到第 1 行没有缩进;也就是说,代码行前面没有空格。但是第二行有四个缩进空间。因为这比前一行缩进量大,所以我们知道一个新的块已经开始。第 3 行也有四个缩进空间,所以我们知道这个块在第 3 行继续。
第 4 行有更多的缩进(八个空格),所以一个新的块已经开始。这个代码块在另一个代码块的里面。在 Python 中,代码块中可以有代码块。
在第 5 行,缩进量减少到了 4,所以我们知道上一行的块已经结束了。第 4 行是那个代码块唯一的一条线。因为第 5 行与第 2 行和第 3 行中的块具有相同的缩进量,所以它仍然是原始外部块的一部分,即它不是第 4 行中的块的一部分。
第 6 行是空行,我们直接跳过;它不影响代码块。
第 7 行有四个缩进空间,所以我们知道从第 2 行开始的代码块一直延续到第 7 行。
第 8 行有零个缩进空间,比前一行缩进量少。缩进的减少告诉我们前一个块,从第 2 行开始的块,已经结束了。
这段代码显示了两个块。第一个块从第 2 行到第 7 行。第二个块只包含第 4 行(在另一个块的内部)。
注
代码块不一定要用四个空格来描述。块可以使用任意数量的空格,但是惯例是每个缩进使用四个空格。
####while
循环语句
让我们看看完整的while
语句,从reverseCipher.py
的第 8 行开始:
while i >= 0: translated = translated + message[i] i = i - 1 print(translated)
一个while
语句告诉 Python 首先检查条件的计算结果,在第 8 行是i >= 0
。你可以把while
语句while i >= 0:
理解为“当变量i
大于或等于零时,继续执行下面块中的代码。”如果条件判断结果为True
,程序执行进入while
语句之后的块。通过查看缩进,您可以看到这个块由第 9 行和第 10 行组成。当它到达块的底部时,程序执行跳回到第 8 行的while
语句并再次检查条件。如果还是True
,执行跳转到块的开始,再次运行块中的代码。
如果while
语句的条件求值为False
,程序执行将跳过下一个块中的代码,并跳转到该块之后的第一行(第 12 行)。
使字符串变长
请记住,在第 7 行上,i
变量首先被设置为message
的长度减 1,第 8 行上的while
循环继续执行下一个块中的行,直到条件i >= 0
为False
:
i = len(message) - 1 while i >= 0: translated = translated + message[i] i = i – 1 print(translated)
第 9 行是一个赋值语句,它在translated
变量中存储了一个值。存储的值是与message
中索引i
处的字符连接translated
的当前值。结果,存储在translated
中的字符串值一次“增长”一个字符,直到它成为完全加密的字符串。
第 10 行也是一个赋值语句。它获取当前的整数值i
并从中减去 1(这被称为递减变量)。然后它将这个值存储为i
的新值。
下一行是 12,但是因为这一行缩进较少,Python 知道while
语句的块已经结束。因此,程序执行没有继续到第 12 行,而是跳回到第 8 行,在那里再次检查while
循环的条件。如果条件是True
,块内的行(第 9 行和第 10 行)被再次执行。这种情况一直持续到条件为False
(即i
小于0
),在这种情况下,程序执行到程序块后的第一行(第 12 行)。
让我们考虑一下这个循环的行为,以了解它在块中运行代码的次数。变量i
以最后一个索引message
的值开始,translated
变量以空白的字符串开始。然后在循环内部,message[i]
的值(它是message
字符串中的最后一个字符,因为i
将具有最后一个索引的值)被添加到translated
字符串的末尾。
然后i
中的值减少(即减少)了1
,这意味着message[i]
将是倒数第二个字符。因此,当i
作为一个索引从message
中的字符串的后面一直移动到前面时,字符串message[i]
被添加到translated
的末尾。这就是translated
如何实现message
的反向。当i
最终设置为-1
时,这发生在我们到达消息的索引0
时,while
循环的条件为False
,执行跳转到第 12 行:
print(translated)
在第 12 行程序的末尾,我们将变量translated
的内容(即字符串'.daed era meht fo owt fi ,terces a peek nac eerhT'
)打印到屏幕上。这向用户显示了反转后的字符串是什么样子。
如果您仍然无法理解while
循环中的代码如何反转字符串,请尝试将新行(以粗体显示)添加到循环的块中:
while i >= 0: translated = translated + message[i] print('i is', i, ', message[i] is', message[i], ', translated is', translated) i = i - 1 print(translated)
第 10 行打印i
、message[i]
和translated
的值,以及每次执行循环时的字符串标签(即,在循环的每次迭代)。这一次,我们没有使用字符串连接,而是使用了一些新的东西。逗号告诉print()
函数我们正在打印六个独立的东西,所以函数在它们之间添加了一个空格。现在当你运行程序时,你可以看到translated
变量是如何“增长”的输出如下所示:
i is 48 , message[i] is . , translated is . i is 47 , message[i] is d , translated is .d i is 46 , message[i] is a , translated is .da i is 45 , message[i] is e , translated is .dae i is 44 , message[i] is d , translated is .daed i is 43 , message[i] is , translated is .daed i is 42 , message[i] is e , translated is .daed e i is 41 , message[i] is r , translated is .daed er i is 40 , message[i] is a , translated is .daed era i is 39 , message[i] is , translated is .daed era i is 38 , message[i] is m , translated is .daed era m i is 37 , message[i] is e , translated is .daed era me i is 36 , message[i] is h , translated is .daed era meh i is 35 , message[i] is t , translated is .daed era meht i is 34 , message[i] is , translated is .daed era meht i is 33 , message[i] is f , translated is .daed era meht f i is 32 , message[i] is o , translated is .daed era meht fo i is 31 , message[i] is , translated is .daed era meht fo i is 30 , message[i] is o , translated is .daed era meht fo o i is 29 , message[i] is w , translated is .daed era meht fo ow i is 28 , message[i] is t , translated is .daed era meht fo owt i is 27 , message[i] is , translated is .daed era meht fo owt i is 26 , message[i] is f , translated is .daed era meht fo owt f i is 25 , message[i] is i , translated is .daed era meht fo owt fi i is 24 , message[i] is , translated is .daed era meht fo owt fi i is 23 , message[i] is , , translated is .daed era meht fo owt fi , i is 22 , message[i] is t , translated is .daed era meht fo owt fi ,t i is 21 , message[i] is e , translated is .daed era meht fo owt fi ,te i is 20 , message[i] is r , translated is .daed era meht fo owt fi ,ter i is 19 , message[i] is c , translated is .daed era meht fo owt fi ,terc i is 18 , message[i] is e , translated is .daed era meht fo owt fi ,terce i is 17 , message[i] is s , translated is .daed era meht fo owt fi ,terces i is 16 , message[i] is , translated is .daed era meht fo owt fi ,terces i is 15 , message[i] is a , translated is .daed era meht fo owt fi ,terces a i is 14 , message[i] is , translated is .daed era meht fo owt fi ,terces a i is 13 , message[i] is p , translated is .daed era meht fo owt fi ,terces a p i is 12 , message[i] is e , translated is .daed era meht fo owt fi ,terces a pe i is 11 , message[i] is e , translated is .daed era meht fo owt fi ,terces a pee i is 10 , message[i] is k , translated is .daed era meht fo owt fi ,terces a peek i is 9 , message[i] is , translated is .daed era meht fo owt fi ,terces a peek i is 8 , message[i] is n , translated is .daed era meht fo owt fi ,terces a peek n i is 7 , message[i] is a , translated is .daed era meht fo owt fi ,terces a peek na i is 6 , message[i] is c , translated is .daed era meht fo owt fi ,terces a peek nac i is 5 , message[i] is , translated is .daed era meht fo owt fi ,terces a peek nac i is 4 , message[i] is e , translated is .daed era meht fo owt fi ,terces a peek nac e i is 3 , message[i] is e , translated is .daed era meht fo owt fi ,terces a peek nac ee i is 2 , message[i] is r , translated is .daed era meht fo owt fi ,terces a peek nac eer i is 1 , message[i] is h , translated is .daed era meht fo owt fi ,terces a peek nac eerh i is 0 , message[i] is T , translated is .daed era meht fo owt fi ,terces a peek nac eerhT
输出行"i is 48 , message[i] is . , translated is ."
显示了在字符串message[i]
被添加到translated
的末尾之后,但在i
被递减之前,表达式i
、message[i]
和translated
的计算结果。可以看到程序执行第一次经过循环时,i
被设置为48
,所以message[i]
(也就是message[48]
)就是字符串'.'
。translated
变量开始是一个空字符串,但是当message[i]
被添加到第 9 行的末尾时,它变成了字符串值'.'
。
在循环的下一次迭代中,输出是"i is 47 , message[i] is d , translated is .d"
。你可以看到i
已经从48
递减到47
,所以现在message[i]
就是message[47]
,也就是'd'
字符。(那是dead'
中的第二个'd'
。)这个'd'
被加到translated
的末尾,所以translated
现在是值'.d'
。
现在你可以看到translated
变量的字符串是如何从一个空白字符串慢慢“成长”到反向的message
。
用input()
提示符改进程序
本书中的程序都是这样设计的,被加密或解密的字符串作为赋值语句直接输入源代码。这在我们开发程序的时候很方便,但是你不应该期望用户自己去修改源代码。为了使程序更容易使用和共享,您可以修改赋值语句,使它们调用input()
函数。您还可以向input()
传递一个字符串,这样它将显示一个提示,让用户输入一个要加密的字符串。例如,将reverseCipher.py
中的第 4 行改为:
message = input('Enter message: ')
当您运行该程序时,它会将提示打印到屏幕上,并等待用户输入消息。用户输入的消息将是存储在message
变量中的字符串值。当你现在运行程序时,你可以输入任何你想要的字符串,并得到如下输出:
Enter message: Hello, world! !dlrow ,olleH
总结
我们刚刚完成了第二个程序,它使用第三章中的技术将一个字符串转换成一个新的字符串,比如索引和连接。该程序的一个关键部分是len()
函数,它接受一个字符串参数并返回该字符串中有多少个字符的整数。
您还了解了布尔数据类型,它只有两个值,True
和False
。比较运算符==
、!=
、<
、>
、<=
和>=
可以比较两个值并计算出一个布尔值。
条件表达式是使用比较运算符并计算为布尔数据类型的表达式。它们在while
循环中使用,循环将执行while
语句后的代码块中的代码,直到条件判断结果为False
。代码块是由具有相同缩进级别的行组成,包括它们内部的任何块。
现在你已经学会了如何操作文本,你可以开始编写用户可以运行和交互的程序了。这很重要,因为文本是用户和计算机相互交流的主要方式。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 下面这段代码在屏幕上显示了什么?
print(len('Hello') + len('Hello'))
- 这段代码打印了什么?
i = 0 while i < 3: print('Hello') i = i + 1
- 这个代码怎么样?
i = 0 spam = 'Hello' while i < 5: spam = spam + spam[i] i = i + 1 print(spam)
- 这个呢?
i = 0 while i < 4: while i < 6: i = i + 2 print(i)