像计算机科学家一样思考:用 Python 学习 第 2 版/字典
到目前为止,我们详细研究的所有复合数据类型——字符串、列表和元组——都是序列类型,它们使用整数作为索引来访问它们包含的值。
字典是一种不同的复合类型。它们是 Python 内置的 映射类型。它们将 键(可以是任何不可变类型)映射到值,值可以是任何类型,就像列表或元组的值一样。
例如,我们将创建一个字典将英文单词翻译成西班牙语。对于这个字典,键是字符串。
创建字典的一种方法是从空字典开始并添加 键值对。空字典用{}:
第一个赋值创建一个名为eng2sp的字典;其他赋值向字典添加新的键值对。我们可以像往常一样打印字典的当前值
字典的键值对用逗号分隔。每个对包含一个用冒号分隔的键和值。
对的顺序可能不是你期望的。Python 使用复杂算法来确定键值对在字典中的存储位置。就我们而言,我们可以认为这种排序是不可预测的。
创建字典的另一种方法是使用与先前输出相同的语法提供键值对列表
我们写对的顺序无关紧要。字典中的值是通过键访问的,而不是通过索引访问的,因此无需关心排序。
以下是我们使用键查找对应值的示例
键'two'产生值'dos'.
的del语句从字典中删除键值对。例如,以下字典包含各种水果的名称以及库存中每种水果的数量
如果有人买了所有梨,我们可以从字典中删除该条目
或者,如果我们很快会收到更多梨,我们可能只需要更改与梨关联的值
的len函数也适用于字典;它返回键值对的数量
字典有一些有用的内置方法。
的keys方法接受字典并返回其键的列表。
正如我们之前在字符串和列表中看到的,字典方法使用点表示法,它指定方法的名称位于点的右侧,而应用方法的对象的名称位于点的左侧。圆括号表示该方法不接受任何参数。
方法调用称为 调用;在这种情况下,我们会说我们在keys方法上调用eng2sp对象。正如我们将在几章后讨论面向对象编程时看到的那样,方法调用的对象实际上是方法的第一个参数。
的values方法类似;它返回字典中值的列表
的items方法同时返回两者,以元组列表的形式——每个键值对一个。
的has_key方法接受键作为参数并返回True如果键出现在字典中,则返回False否则
此方法非常有用,因为在字典中查找不存在的键会导致运行时错误
由于字典是可变的,因此您需要了解别名。当两个变量引用同一个对象时,对其中一个变量的更改会影响另一个变量。
如果您想修改字典并保留原始字典的副本,请使用copy方法。例如,opposites是一个包含相反词对的字典
alias和opposites引用同一个对象;copy引用同一个字典的新副本。如果我们修改alias, opposites也会发生改变
如果我们修改copy, opposites保持不变
我们之前使用列表的列表来表示矩阵。对于大多数非零值的矩阵来说,这是一个不错的选择,但请考虑以下 稀疏矩阵:
稀疏矩阵 列表表示包含许多零
另一种方法是使用字典。对于键,我们可以使用包含行号和列号的元组。以下是同一矩阵的字典表示
我们只需要三个键值对,每个非零元素一个。每个键都是一个元组,每个值都是一个整数。
要访问矩阵的元素,我们可以使用[]运算符
请注意,字典表示的语法与嵌套列表表示的语法不同。我们不使用两个整数索引,而是使用一个索引,它是一个包含整数的元组。
有一个问题。如果我们指定一个为零的元素,我们会收到错误,因为字典中没有包含该键的条目
的get方法解决了这个问题
第一个参数是键;第二个参数是get如果键不在字典中应该返回的值
get绝对改进了访问稀疏矩阵的语义。语法有点可惜。
如果您使用上一章中的fibonacci函数进行了一些操作,您可能已经注意到,您提供的参数越大,函数运行的时间就越长。此外,运行时间增长得非常快。在我们的一台机器上,fibonacci(20)立即完成,fibonacci(30)大约需要一秒钟,而fibonacci(40)则大约永远不会完成。
为了理解原因,请考虑以下fibonacci的 调用图,其中n = 4:
斐波那契树 调用图显示一组函数帧,用线连接每个帧与其调用的函数的帧。在图的顶部,fibonacci的 调用图,其中n = 4调用fibonacci的 调用图,其中n = 3和n = 2。反过来,fibonacci的 调用图,其中n = 3调用fibonacci的 调用图,其中n = 2和n = 1。依此类推。
统计fibonacci(0)和fibonacci(1)被调用的次数。这是一个低效的解决方案,随着参数的增大,效率会变得更糟。
一个好的解决方案是将已经计算过的值存储在一个字典中,以便跟踪它们。存储起来以便将来使用的先前计算过的值称为提示。以下是用提示的实现方法:fibonacci使用提示
名为previous的字典跟踪我们已经知道的斐波那契数。我们从两个键值对开始:0 映射到 1;1 映射到 1。
无论何时fibonacci被调用,它都会检查字典以确定它是否包含结果。如果在字典中,函数可以立即返回而无需进行任何进一步的递归调用。如果没有,它必须计算新值。新值在函数返回之前添加到字典中。
使用此版本的fibonacci,我们的机器可以在眨眼间计算fibonacci(100)。
的L在数字的末尾表示它是一个long整数。
长整数
[edit | edit source]Python 提供了一种称为long的类型,它可以处理任何大小的整数(仅受计算机内存大小的限制)。
有三种方法可以创建long值。第一种是计算一个太大而无法放入int中的算术表达式。我们已经在上面的fibonacci(100)示例中看到了这一点。另一种方法是在数字末尾写一个大写的L。第三种方法是调用
,并将要转换的值作为参数传递给它。long,就像longfloatint和一样,可以将、intfloat,甚至数字字符串转换为长整数统计字母
[edit | edit source]
在第 7 章中,我们编写了一个函数来统计字符串中字母出现的次数。这个问题的更通用版本是形成字符串中字母的直方图,即每个字母出现的次数。这样的直方图可能对压缩文本文件很有用。由于不同的字母出现的频率不同,我们可以通过对常用字母使用较短的代码,对出现频率较低的字母使用较长的代码来压缩文件。
字典提供了一种优雅的方法来生成直方图
我们从一个空的字典开始。对于字符串中的每个字母,我们找到当前计数(可能是零)并递增它。最后,字典包含字母及其频率的配对。
以字母顺序显示直方图可能更吸引人。我们可以使用
sortitems和方法来做到这一点案例研究:机器人
[edit | edit source]
游戏[edit | edit source]
在这个案例研究中,我们将编写一个经典的基于控制台的游戏 robots 的版本。Robots 是一款回合制游戏,玩家扮演主角,试图在被愚蠢但执着的机器人追捕时生存下来。每次玩家移动时,每个机器人都会向玩家移动一格。如果机器人抓住玩家,玩家就死了,但如果机器人相互碰撞,它们就会死掉,留下死机器人的残骸。如果其他机器人与机器人残骸碰撞,它们也会死掉。
基本策略是让玩家自己处于一个位置,这样机器人就可以在向玩家移动时相互碰撞,以及与机器人残骸碰撞。为了使游戏更具可玩性,玩家还被赋予了将自己传送到屏幕上另一个位置的能力——安全传送 3 次,之后随机传送,这样玩家就不会一直被逼到角落里,然后每次都输掉。
设置世界、玩家和主循环
[edit | edit source]
让我们从一个将玩家放置在屏幕上并具有一个函数来响应按键来移动玩家的程序开始像这样的程序,它们涉及通过事件(例如按键和鼠标点击)与用户交互,被称为事件驱动程序。
此阶段的主要事件循环只是
事件处理是在
move_player函数中完成的。update_when('key_pressed')等待按键按下,然后才执行下一个语句。多路分支语句然后处理与游戏相关的全部按键。按下 Esc 键会导致
返回函数中完成的。,使Truenot finished为 false,从而退出主循环并结束游戏。4、7、8、9、6、3、2 和 1 键都导致玩家在适当的方向移动,前提是她没有被窗口边缘挡住。添加机器人
[edit | edit source]
现在让我们添加一个机器人,每次玩家移动时,它都会朝玩家前进。在
place_player函数之间添加以下place_robot函数。在和函数中完成的。:
place_player函数之后立即添加move_robot函数中完成的。:
函数。我们需要将机器人和玩家都传递给此函数,以便它可以比较它们的位置并将机器人移动到玩家的位置。
现在在程序主体中,在player = place_player()行之后立即添加robot = place_robot()行,并在主循环中,在finished = move_player(player)行之后立即添加move_robot(robot, player).
调用。
检查碰撞[edit | edit source]
现在我们有一个机器人,它不断地向玩家移动,但一旦它抓住玩家,它就会一直跟随玩家,无论玩家去哪里。我们希望发生的事情是在玩家被抓住后立即结束游戏。以下函数将确定这种情况是否已经发生函数中完成的。将这个新函数放在函数的正下方。现在让我们修改play_game
函数来检查碰撞我们将变量finished重命名为defeated,它现在被设置为collided重命名为的结果。主循环只要为 false 就会运行。按下 Esc 键仍然会结束程序,因为我们检查了quit重命名为并退出主循环,如果它为 true。最后,我们在主循环之后立即检查
,如果它为 true,则显示相应的提示信息。
添加更多机器人[edit | edit source]
- 接下来我们可以做几件事
- 让玩家有能力传送到另一个位置来逃脱追捕。
- 为玩家提供安全的放置位置,这样它永远不会从机器人身上开始。
添加更多机器人。
添加将玩家传送至随机位置的能力是最简单的任务,我们已将其留给读者作为练习。
我们如何为玩家提供安全的放置位置,将取决于我们如何表示多个机器人,因此有必要先解决添加更多机器人的问题。要添加第二个机器人,我们可以创建一个名为robot2函数之间添加以下的另一个变量,并在其中调用另一个
。此方法的问题在于,我们很快就会想要很多机器人,而为所有机器人取名将会很麻烦。更优雅的解决方案是将所有机器人放在一个列表中函数之间添加以下现在,我们不再在函数的正下方。现在让我们修改中调用,而是调用place_robots
,它返回一个包含所有机器人的单个列表
place_player在放置了多个机器人之后,我们必须处理每个机器人的移动。但是,我们已经解决了移动单个机器人的问题,因此遍历列表并依次移动每个机器人即可解决问题move_robot函数之后立即添加move_robots函数的正下方。现在让我们修改,并将在放置了多个机器人之后,我们必须处理每个机器人的移动。但是,我们已经解决了移动单个机器人的问题,因此遍历列表并依次移动每个机器人即可解决问题更改为调用函数之后立即添加.
,而不是调用
place_player现在,我们需要检查每个机器人是否与玩家发生了碰撞move_robot,它现在被设置为check_collisions函数的正下方。现在让我们修改,并将重命名为,并将现在,我们需要检查每个机器人是否与玩家发生了碰撞更改为调用,它现在被设置为.
中的行更改为最后,我们需要遍历robots重命名为,如果
变为 true,则依次移除每个机器人。我们已将添加此功能留作练习。
赢得游戏我们游戏中最大的问题是无法获胜。机器人既无情又坚不可摧。通过谨慎的操作和一点幸运的传送,我们可以达到看起来只有一个机器人追逐玩家的点(实际上所有机器人都会叠在一起)。这堆移动的机器人会继续追逐我们倒霉的玩家,直到它被抓住,无论是我们的一次糟糕的移动还是传送将玩家直接传送到机器人身上。
当两个机器人发生碰撞时,它们应该死亡,留下一个垃圾堆。机器人(或玩家)在与垃圾堆碰撞时也应该死亡。实现这个逻辑相当棘手。在玩家和每个机器人移动之后,我们需要
- 检查玩家是否与机器人或垃圾堆发生了碰撞。如果发生碰撞,则将重命名为设置为true,并退出游戏循环。
- 检查每个机器人最后,我们需要遍历列表,看它是否与垃圾堆发生了碰撞。如果发生碰撞,则忽略该机器人(将其从最后,我们需要遍历列表中删除)。
- 检查每个剩余的机器人,看它们是否与另一个机器人发生了碰撞。如果发生碰撞,则丢弃所有发生碰撞的机器人,并在它们所占据的位置放置一个垃圾堆。
- 检查是否还有机器人。如果没有,则结束游戏并标记玩家为获胜者。
让我们依次完成这些任务。
大部分工作将在我们的现在,我们需要检查每个机器人是否与玩家发生了碰撞函数中进行。让我们从修改,它现在被设置为开始,将参数名称更改为反映其更通用的用途
我们现在在调用垃圾之后立即引入一个名为,而是调用:
的新空列表,并修改现在,我们需要检查每个机器人是否与玩家发生了碰撞以包含新列表
请务必修改对现在,我们需要检查每个机器人是否与玩家发生了碰撞(目前为defeated = check_collisions(robots, player))的调用,以包含垃圾作为新的参数。
同样,我们需要修复if defeated之后的逻辑,在显示“They got you!”消息之前将新的垃圾从屏幕上移除
由于此时垃圾始终为空列表,因此我们没有改变程序的行为。为了测试我们的新逻辑是否真正有效,我们可以引入一个垃圾堆,让我们的玩家撞向它,此时游戏应该从屏幕上删除所有项目,并显示结束消息。
为了测试,临时修改我们的程序,将机器人和玩家的随机放置位置更改为预定的位置会很有帮助。我们计划使用实心方块来表示垃圾堆。我们观察到放置机器人与放置垃圾堆非常相似,并修改函数之间添加以下来同时执行这两个操作
注意x和y现在是参数,以及一个新的参数,我们将使用它来设置filled为垃圾堆设置为true。
我们的程序现在出现了问题,因为,而是调用finished函数之间添加以下中的调用没有为x和y传递参数。修复这个问题以及设置程序进行测试留作练习。
为了移除与垃圾堆发生碰撞的机器人,我们在现在,我们需要检查每个机器人是否与玩家发生了碰撞中添加一个嵌套循环,遍历每个机器人和每个垃圾堆。我们第一次尝试这样操作,但它没有成功
运行这个新代码,使用练习 11 中设置的程序,我们发现了一个错误。看起来机器人仍然像以前一样穿透垃圾堆。
实际上,错误更微妙一些。由于我们有两个机器人叠在一起,当检测到第一个机器人的碰撞并将该机器人移除时,我们将第二个机器人移到列表中的第一个位置,并且它被下一个迭代所忽略。在遍历列表时修改列表通常很危险。这样做可能会在程序中引入许多难以查找的错误。
在这种情况下,解决方法是反向遍历最后,我们需要遍历列表,这样当我们从列表中移除一个机器人时,所有由于索引更改而导致列表索引更改的机器人都是我们已经评估过的机器人。
像往常一样,Python 提供了一种优雅的方法来实现这一点。内置函数reversed提供对序列的反向迭代。替换
的 调用图,其中
将使我们的程序按照我们的预期方式工作。
我们现在要检查每个机器人,看它是否与其他任何机器人发生了碰撞。我们将移除所有发生碰撞的机器人,在其身后留下一个垃圾堆。如果我们到达一个不再有机器人的状态,则玩家获胜。
再一次,我们必须小心,不要引入与从我们正在迭代的列表中移除项目相关的错误。
以下是计划
- 检查最后,我们需要遍历中的每个机器人(一个外部循环,向前遍历)。
- 将其与每个后面的机器人进行比较(一个内部循环,向后遍历)。
- 如果两个机器人发生了碰撞,则在它们的位置添加一个垃圾,标记第一个机器人为垃圾,并移除第二个机器人。
- 检查完所有机器人的碰撞之后,再次反向遍历机器人列表,移除所有标记为垃圾的机器人。
- 检查是否还有机器人。如果没有,则宣布玩家获胜。
将以下内容添加到现在,我们需要检查每个机器人是否与玩家发生了碰撞将完成我们需要做的大部分事情
我们使用enumerate函数(我们在第 9 章中见过),在向前遍历时获取每个机器人的索引和值。然后对剩余机器人的切片进行反向遍历reversed(robots[index+1:]),设置碰撞检查。
每当两个机器人发生碰撞时,我们的计划要求在该位置添加一块垃圾,标记第一个机器人以便稍后移除(我们仍然需要它与其他机器人进行比较),并立即移除第二个机器人。的if collided(robot1, robot2)条件语句的主体旨在实现这一点,但如果你仔细观察这行
,你应该注意到一个问题。robot1['junk']会导致语法错误,因为我们的机器人字典还没有包含'junk'键。为了解决这个问题,我们修改函数之间添加以下以适应新的键
随着程序开发的进行,数据结构发生变化并不少见。逐步求精程序数据和逻辑是结构化编程过程的正常组成部分。
在robot1被标记为垃圾之后,我们使用junk.append(place_robot(robot1['x'], robot1['y'], True))将一个垃圾堆添加到垃圾列表中,并与之位于相同位置,然后从游戏中移除要添加第二个机器人,我们可以创建一个名为,方法是从图形窗口中移除其形状,然后从机器人列表中移除它。
下一个循环反向遍历机器人列表,移除之前标记为垃圾的所有机器人。由于当所有机器人死亡时玩家获胜,而当机器人列表不再包含存活的机器人时,它将为空,因此我们可以简单地检查最后,我们需要遍历是否为空来确定玩家是否获胜。
这可以在现在,我们需要检查每个机器人是否与玩家发生了碰撞中完成,方法是在我们完成检查机器人碰撞并移除死亡机器人后立即添加
嗯...我们应该返回什么?在当前状态下,现在,我们需要检查每个机器人是否与玩家发生了碰撞是一个布尔函数,当玩家与某物发生碰撞并输掉游戏时返回true,当玩家没有输掉游戏并且游戏应该继续时返回false。这就是为什么在函数的正下方。现在让我们修改函数中捕获返回值的变量被称为重命名为.
现在我们有三种可能的状态
- 最后,我们需要遍历不为空且玩家没有与任何东西发生碰撞 - 游戏仍在进行中
- 玩家与某物发生了碰撞 - 机器人获胜
- 玩家没有与任何东西发生碰撞,并且最后,我们需要遍历为空 - 玩家获胜
为了尽可能少地更改当前程序来处理这种情况,我们将利用 Python 允许序列类型充当布尔值的双重身份。当游戏应该继续时,我们将返回一个空字符串(这是 false),并返回"robots_win"或"player_wins"来处理其他两种情况。现在,我们需要检查每个机器人是否与玩家发生了碰撞现在应该看起来像这样
需要对函数的正下方。现在让我们修改进行一些相应的更改,以使用新的返回值。这些留作练习。
编写一个程序,从命令行读取一个字符串,并返回一个表格,其中包含字符串中出现的字母按字母顺序排列,以及每个字母出现的次数。不区分大小写。程序的示例运行如下所示
$ python letter_counts.py "ThiS is String with Upper and lower case Letters." a 2 c 1 d 1 e 5 g 1 h 2 i 4 l 2 n 2 o 1 p 2 r 4 s 5 t 5 u 1 w 2 $
从一个连续的解释器会话中,给出 Python 解释器对以下每个输入的响应。
- #.
- #.
- #.
- #.
- #.
- #.
- #.
确保你理解为什么你会得到每个结果。然后应用你学到的知识来填充下面函数的正文。
你的解决方案应该通过 doctests。编写一个名为alice_words.py的程序,创建一个名为alice_words.txt的文本文件,其中包含在 alice_in_wonderland.txt_ 中找到的所有单词的字母排序列表,以及每个单词出现的次数。你的输出文件的首 10 行应该看起来像这样
Word Count ======================= a 631 a-piece 1 abide 1 able 1 about 94 above 3 absence 1 absurd 2
单词alice在书中出现了多少次?- 《爱丽丝梦游仙境》中最长的单词是什么?它有多少个字符?
- 将代码从“设置世界、玩家和主循环”部分复制到一个名为robots.py的文件中并运行它。你应该能够使用数字键盘在屏幕上移动玩家,并通过按下 Esc 键退出程序。
- 笔记本电脑通常比台式电脑的键盘更小,没有单独的数字键盘。修改 robots 程序,使其使用 'a'、'q'、'w'、'e'、'd'、'c'、'x' 和 'z' 来代替 '4'、'7'、'8'、'9'、'6'、'3'、'2' 和 '1',以便它可以在典型的笔记本电脑键盘上工作。
- 将“添加机器人”部分中的所有代码添加到指示的位置。确保程序正常工作,并且现在有一个机器人跟随你的玩家。
- 将“检查碰撞”部分中的所有代码添加到指示的位置。验证程序在机器人抓住玩家后是否结束,并显示“他们抓住了你!”消息 3 秒。
- 修改函数中完成的。函数,为玩家添加每次按下0键时跳到随机位置的功能。(提示:函数。在已经拥有将玩家放置在随机位置所需的逻辑。只需添加另一个条件分支到函数中完成的。中,当key_pressed('0')为真时使用此逻辑。)测试程序以验证你的玩家现在是否可以传送到屏幕上的随机位置以摆脱困境。
- 对你的程序进行所有在“添加更多机器人”中指示的更改。确保循环遍历最后,我们需要遍历列表,在重命名为变为真后,依次删除每个机器人。测试你的程序以验证现在是否有两个机器人追赶你的玩家。让一个机器人抓住你以测试你是否正确处理了所有机器人的删除。将robots = place_robots(2)中的参数从 2 改为 4,并确认你有 4 个机器人。
对你的程序进行所有在“添加 junk”中指示的更改。修复,而是调用,将x和y的值的随机生成移动到适当的位置,并将这些值作为参数传递给函数之间添加以下的调用中。现在,我们准备对我们的程序进行临时修改,以删除随机性,以便我们可以控制它进行测试。我们可以从在游戏板的中心放置一堆垃圾开始。更改
finished
运行程序并确认棋盘中心有一个黑色方块。现在更改函数。在使其看起来像这样
最后,暂时注释掉x和y中的值的随机生成以及,而是调用的创建numbotsrobots。用创建两个固定位置的机器人的代码替换此逻辑
现在当你启动程序时,它应该看起来像这样
当你运行此程序,并且保持静止(通过重复按下5),或者远离垃圾堆,你可以确认机器人完好无损地穿过它。另一方面,当你进入垃圾堆时,你会死掉。对函数的正下方。现在让我们修改进行以下修改,以集成到“将机器人变成垃圾并让玩家获胜”中所做的更改。
- 重命名重命名为finishedwinner并将其初始化为空字符串,而不是False.