阿尔伯塔问题解决-Python-编程-电子游戏笔记-全-
阿尔伯塔问题解决、Python 编程、电子游戏笔记(全)
001:课程主题 🎮

欢迎来到《问题解决、编程与电子游戏》课程。我们是PVG课程的教学团队。
我叫Dwayne。我是阿尔伯塔大学计算机科学系的教授。我教授入门编程语言课程已有大半生。闲暇时,我喜欢玩电子游戏、看棒球和做木工。
我叫Paul。我也是一名计算机科学教授。这么多年过去,当学生理解一个新的编程概念时,我仍然会感到兴奋。闲暇时,我喜欢看电影和带我的孩子去看航展。
大家好,我叫Elise。我是阿尔伯塔大学的计算机科学毕业生。我之前参与开发过一个应用程序,旨在让编程初学者能更容易地与Arduino机器人交互并创建游戏。我很高兴向大家介绍我们的课程,并希望你们能享受编程的乐趣。
我叫Emma。我拥有数学和计算机科学学位。过去几年,我一直在阿尔伯塔大学担任入门计算机科学课程的教学团队成员。我喜欢指导编程新手,并希望这门课程能让计算机科学对你来说变得触手可及。
在本课程中,你将学习四件事:
- 计算机科学如何将计算应用于信息。
- 如何使用计算思维(或问题解决)来解决计算问题。
- 如何用Python编程。
- 如何创建电子游戏及其他应用。
以下是你在本课程中将创建的两款游戏的简要介绍。


我们将第一款游戏称为“黑客”。它基于Bethesda Soft Works公司广受欢迎的《辐射》系列中出现的一个迷你游戏。这是一段该迷你游戏在《辐射:新维加斯》中出现的视频片段。游戏目标是选择屏幕上显示的一个单词作为正确密码。你最多有四次猜测机会。每次猜测后,会得到一些反馈。这是你将在本课程中创建的“黑客”游戏的视频。
我们将第二款游戏称为“点点碰撞”。游戏目标是防止两个点发生碰撞。如果你点击窗口,两个点会传送到新的随机位置。你的得分是两点碰撞前所经过的时间。
在本课程中,我们使用“电子游戏”这个术语来指代可以为了娱乐而进行的交互式图形应用程序。
学习创建游戏很酷,但这并非本课程的真正目标。你用来创建游戏的过程同样适用于大多数其他类型的软件应用程序。我们选择电子游戏有三个原因:
- 首先,它们有趣且好玩。
- 其次,尽管游戏很有趣,但它们足够简单,可以在本课程期间完成。
- 第三,电子游戏是图形化和交互式的。这意味着在课程结束后,你将能够创建交互式图形软件应用程序。例如,你可以创建一个管理音乐播放列表的应用程序,或者一个演示化学反应的应用程序。
要创建电子游戏,你必须学习用某种编程语言编写程序。你将学习Python编程语言,以便用它来编写游戏代码。在本课程中,我们将使用Python 3版本。这是“点点碰撞”游戏的部分Python代码。如果你现在还不理解,不用担心,课程结束时你就会明白。
学习编写Python程序既有趣又有用。然而,编程语言有很多种。如果你过于狭隘地专注于Python语言,以后学习其他编程语言可能会遇到困难。在本课程中,你将学习通用的计算机科学概念,例如语法、语义、命名空间和控制结构。虽然你将学习如何应用这些通用概念来学习Python,但你以后也能利用这些概念来学习其他编程语言。


在理解代码必须做什么之前,你无法用编程语言编写出有意义的代码。代码是问题解决过程的一个最终产物或成果,这个过程始于一个想法。
将一个想法转化为可行的解决方案是一个基于问题解决的过程。例如,如果你的想法是一个新设备,其成果可能包括物理对象、设计文档、内含的软件和用户手册。创建这些成果中的每一项都需要使用不同的问题解决技术。
当成果是一个软件应用程序时,就需要进行计算问题解决。在本课程中,你将学习使用计算问题解决(也称为计算思维)将游戏想法转化为游戏软件。然而,同样的问题解决过程可以用来创建任何类型的软件,无论它是否是游戏。
计算机科学是对不同类型信息以及计算可以应用于这些信息的不同方式的系统性研究。也许“计算科学”这个术语比“计算机科学”更准确。例如,我们将我们的大学院系称为“计算科学系”,以更准确地反映该学科的性质。这是一门研究计算和信息,以及计算机和计算机编程的科学学科。下一课将介绍计算机科学。
我们使用游戏让课程更有趣,我们使用Python,因为它是一门目前流行的优秀语言。然而,你在本课程中学到的计算问题解决过程,无论软件是否是游戏,无论编程语言是Python还是其他语言,都可以使用。完成本课程后,你将能够运用所学的问题解决过程和计算机科学概念,使用大多数编程语言将大多数软件想法转化为实际的软件。
有些课程只专注于Python编程,或只专注于为游戏编写代码。本课程专注于通用的计算机科学概念和通用的问题解决技术。是的,它使用了电子游戏和Python。但它所做的远不止于此。
本节课总结

在本节课中,我们一起学习了《问题解决、Python编程与电子游戏》课程的核心主题。我们认识了教学团队,了解了课程将涵盖的四个主要方面:计算机科学基础、计算思维、Python编程以及电子游戏开发。我们预览了课程中将创建的两款游戏——“黑客”和“点点碰撞”,并理解了选择游戏作为教学载体的原因。最重要的是,我们明确了本课程的终极目标:掌握通用的计算问题解决方法和计算机科学概念,而不仅仅是学习Python语法或游戏制作。这些技能将使你能够应对更广泛的编程挑战,并为学习其他编程语言打下坚实基础。
002:计算机科学入门 🖥️

在本节课中,我们将要学习计算机科学、计算以及信息的基本概念。我们将了解计算机科学的研究范围,认识不同类型的信息,并初步探索算法是如何驱动计算的。
什么是计算机科学?🔍
计算机科学是研究计算与信息的学科。它探究那些可用于获取、表示、生成、转换和传递知识的计算算法,以解决计算问题。
信息的类型 📊
信息有多种类型。数字是其中一种类型,例如 4 和 10.5。对数字进行的计算通常被称为计算或运算。换句话说,计算是一种获取、表示、生成、转换和传递数字的过程。你可以用大脑进行加法或乘法等运算,也可以使用计算器处理更复杂的计算。
然而,数字只是信息的一种类型,计算也只是处理数字的一种方式。计算同样可以应用于其他类型的信息。
字符串与文本 📝
字符串是另一种信息类型,它由有限的字符序列构成。例如,"four" 是一个字符串,而不是数字。你可以对字符串执行计算,例如生成一个所有字母都变为大写的新字符串。同样,计算机也可以对字符串进行计算,例如计算它包含的字符数量。
以下是字符串处理的一些例子:
- 你可以或计算机可以统计在戏剧《罗密欧与朱丽叶》的台词中,“Romeo”一词出现的总次数。
- 计算机执行简单计算的速度远快于人类,它能在远小于一秒的时间内给出答案
128。
算法:计算的步骤 📐

算法是执行计算所遵循的步骤序列。例如,统计《罗密欧与朱丽叶》中“Romeo”出现次数的算法可以是:
- 将计数器初始化为
0。 - 扫描文本字符串,寻找大写字符
‘R’。 - 如果这个
‘R’后面紧跟着小写字符‘o’,‘m’,‘e’,‘o’,则将计数器加1。

计算是你想要执行的任务(例如,统计“Romeo”的出现次数),而算法是你执行该任务的方式(例如,上述的步骤序列)。
更多信息类型:文本、图像与视频 🎨
文本是另一种信息类型。它类似于字符串,但除了字符内容外,还具有字体、颜色和大小等属性。例如,一个带有特定字体、颜色和大小的“4”是文本,而不仅仅是字符串。计算机可以通过改变其字体、颜色和大小等属性来处理文本。
图像是另一种信息。例如,一张梅花4扑克牌的图片就是图像信息。计算机可以通过改变其大小或分辨率等属性来转换图像。
视频是我们目前要介绍的最后一种信息类型。本课程中的视频就是视频信息的例子。视频算法的两个例子是插入转场效果(如划像)和添加动画。本课程视频的制作就运用了这些以及更多的计算过程。

本课程中的应用 🎮
在本课程中,你将遇到许多不同类型的信息以及针对每种信息的多种算法。例如:
- 在第一个游戏“Hacking”中,你将创建算法来生成密码字符串。
- 在第二个游戏“Poke the Dots”中,你将创建自己的“点”类型,并编写一个在两个点碰撞时结束游戏的算法。该算法将检查两个点中心之间的距离是否小于或等于它们的半径之和。
你将使用 Python 3 编程语言来实现你的算法,并在计算机上运行这些程序。
计算的用途 🛠️
计算可以用于许多目的,但本课程将聚焦于一个目标:通过计算解决问题来创建电子游戏。计算的其他常见用途包括通信、信息存储与检索、设备监控与控制、信息加密与解密以及模拟仿真。
你可以在未来的课程中学习计算的其他用途。本课程将为理解这些主题打下坚实的基础,特别是类型和算法,它们是所有其他计算应用的核心组成部分。

总结 📚

本节课中,我们一起学习了计算机科学是研究计算与信息的学科。我们了解到,计算能够获取、表示、生成、转换和传递多种不同类型的信息。算法是执行这些计算的明确步骤序列。在本课程中,我们将运用这些概念,通过Python编程来创造我们自己的电子游戏。
编程语言:P03:编程语言简介

在本节课中,我们将学习编程语言的基本概念,了解计算机如何通过编程语言来执行计算,以及翻译器(特别是解释器)在其中扮演的角色。

计算被用于解决计算问题。创建一个电子游戏或其他应用程序就是一种计算问题解决。我们希望计算机能为应用程序执行适当的计算。
那么,如何让计算机执行这些计算呢?在应用程序编程过程的最后步骤之一,是编写一个计算机程序,供计算机用来执行应用程序所需的计算。一名或多名程序员使用一种编程语言来表达这个计算机程序。
然而,现代编程语言并不包含可以直接被计算机执行的机器指令。相反,计算机程序是计算机必须执行步骤的一种高级表示。一个翻译器必须将计算机程序翻译成机器语言指令,然后计算机才能使用这些指令来执行计算。对于本课程,你不需要了解关于机器语言的其他知识。


上一节我们介绍了编程语言需要翻译成机器指令。本节中,我们来看看翻译器的两种类型。翻译器有两种:编译器和解释器。本课程使用的Python编程语言是解释型的。
一个解释器通过解释程序的一行语句,将其翻译成机器语言。然后,计算机执行这几条机器指令来完成部分计算。解释器和计算机会继续这个“解释-执行”循环,直到程序的每一条语句都被翻译,并且所有生成的机器指令都被执行完毕。
我们可以用一个类比来更好地理解解释过程。《牛津英语词典》对“interpreter”的定义是:进行解释的人,尤指口头或用手语翻译讲话的人。考虑在自然语言(如英语和手语)之间进行翻译的问题。解释是实时发生的,因为解释员在讲话者说话时,一次翻译一个短语或句子。这种翻译有时被称为同声传译,它与计算机语言的解释过程类似。
对于本课程,你只需要理解翻译过程会发生,而不需要理解它是如何完成的。你已经看到,计算机程序代表了一个解决计算问题的计算过程。然后,一个解释器将这个表示翻译成机器语言指令,由计算机执行以解决问题。


在本节课中,我们一起学习了编程语言的核心概念。我们了解到,编程语言是人与计算机沟通的桥梁,它用高级的、人类可读的代码描述计算任务。为了让计算机理解并执行这些任务,需要一个翻译器(如Python的解释器)将代码逐行转换成底层的机器指令。这个过程是自动且实时的,使得程序员能够专注于用编程语言表达逻辑,而无需直接操作复杂的机器语言。
Python编程与问题解决:P04:学习目标与基于问题的学习

在本节课程中,我们将明确本课程的学习目标,并介绍课程将采用的“基于问题的学习”方法。理解这些内容将帮助你更好地利用课程资源,达成学习目的。
学习目标概述
学习目标描述了一个学习者在学习过程中获得、并在学习结束后仍能保持的知识、技能和价值观。
本课程总体的学习目标概括如下:
完成本课程后,你将能够使用适当的工具和资源来解决计算问题。
这个总体目标包含了以下几项具体的知识与技能目标:
- 你将能够描述计算问题。
- 创建并应用功能测试计划来评估问题解决方案。
- 创建能代表问题解决方案的算法,这些算法可以用多种编程语言实现。
- 编写、测试和调试交互式图形化Python程序,这些程序使用广泛的数据类型和控制结构来实现一系列算法。
- 评估Python代码的质量。
- 反思问题解决方案,以识别问题并解决或缓解这些问题。
- 在设计、测试和编码过程的所有层面使用适当的抽象,以创建可重用、可靠、健壮且高效的程序。
- 评估你在课程进行中获得的知识、技能和价值观。
这个总体目标同样包含以下几项具体的价值观目标:
- 重视“编码前设计”的好处,包括清晰的描述、完整的功能测试计划和有效的算法。
- 重视团队设计的好处。
- 重视软件质量。
- 重视通过解决方案反思来引入改进。
基于问题的学习方法介绍
上一节我们明确了学习目标,本节中我们来看看本课程将采用的核心教学方法:基于问题的学习。
本课程采用基于问题的学习方法。你将从一个问题开始,在解决问题的过程中,学习解决它所需的所有知识和技能。
这种方法与传统学习方式形成对比。传统方式是先通过聚焦的例子学习每一项知识和技能,然后将所有新学到的知识和技能应用于解决一个综合性问题。
理解基于问题的学习对你非常重要,这能帮助你更好地利用课程资源。以下是该方法应用于学习使用某些建筑工具的描述:
首先,我会向你提出一个问题。
例如,你将建造一个鸟屋。
其次,我会在你需要时,帮助你识别、发现并学习解决问题所需的每一项知识或技能。
例如,我们查看鸟屋的图片或实物,以识别其组成部分。然后,我帮助你为这些组件创建一个组装计划,并确定计划的每一步需要哪些工具和技术。要制作鸟屋,你需要学习如何使用这些工具:卷尺、锯子、胶水、钉子、钻头和油漆。
第三,你只在需要时才学习每一项知识或技能,并通过将其应用于你正在尝试解决的问题来学习它。
例如,鸟屋计划的第一步是制作底板:从一块木板开始,切下8英寸来制作地板。为此,你需要使用卷尺和锯子。如果你需要帮助,我会向你展示如何测量和标记木板,以及如何锯开它。然后你测量并锯开木板,制作出鸟屋的地板。鸟屋完成后,你将知道如何使用这六种工具。除了知道如何使用这些工具,你还将知道如何创建计划以及如何遵循计划来建造一个鸟屋。
第四,基于问题的学习的下一步是解决一个或多个额外的问题,以学习如何在不同的情境中应用已学的知识和技能,并根据需要学习新的知识和技能。
例如,在建造了鸟屋之后,我可能会让你建造一个栅栏。除了在不同情境中应用你已经知道的六种工具外,你还将学习使用水平仪和螺旋钻。
基于问题学习的优势与挑战
基于问题的学习有其优点和缺点。以下是三个主要优点:
- 学习者可能更有动力去学习相关技术和工具。
- 除了技术和工具,学习者还能学习到问题解决的方法。
- 所学内容可能更容易迁移到其他问题领域。
同时,也存在两个潜在的挑战:
- 一些学习者在掌握必要知识之前尝试解决问题可能会感到不适应。
- 除了学习技术和工具,学习问题解决方法可能需要额外的时间。
如果你的目标不仅是学习正确使用编程语言语句,还包括学习解决计算问题,那么花在问题解决上的额外时间将是值得的。
本节总结

本节课中,我们一起学习了本课程明确的学习目标,并深入介绍了“基于问题的学习”这一教学方法。理解这些将为你后续的学习旅程奠定坚实的基础,帮助你以更有效的方式掌握Python编程和问题解决的技能。
005:如何从本课程中获得最大收益

在本节课中,我们将介绍本课程(PVG)的组织结构,并学习如何从中获得最大收益。了解课程的设计理念和最佳学习方法,将帮助你更有效地掌握问题解决、Python编程和电子游戏开发的核心技能。
课程结构与内容
本课程包含12个模块。每个模块都包含教学视频、设计与编码活动、A类活动、阅读材料和拓展内容。
教学视频主要分为两类。第一类专注于通过设计和编程电子游戏来解决问题。第二类被称为Python编程语言视频(简称Python视频),它提供与电脑游戏无关的、关于Python编程语言的通用教学。
基于问题的学习方法
本课程采用基于问题的学习方法。因此,Python视频会按照创建电子游戏所需的顺序来介绍Python概念。
Python视频使用一个可用于理解任何编程语言的概念框架来呈现内容。这个框架基于几个通用的编程语言概念,你将在课程中学习到它们:词法、语法、语义、命名空间和控制结构。你现在不需要识别或理解这些概念,它们将在编程语言视频中进行解释。
这些通用的编程语言概念较为复杂,因此你可能需要多次观看每个编程语言视频,并在观看时经常暂停。在课程后期,当你第二次或第三次需要使用某个语言结构时,你可能还需要回看之前的视频。
其他学习编程语言的方法可能更快,但它们依赖于模仿简单示例而缺乏充分理解。许多方法更狭隘地专注于单一编程语言,或只关注编程语言中最简单的几个方面。如果你接受本课程所使用的方法,你将获得对编程语言更深入的理解,并为在课程结束后学习额外的Python语言特性及其他编程语言做好准备。
模拟真实世界的游戏开发流程
在现实世界中,创建游戏是一项团队工作。优秀的设计是一种社交活动,而非孤立的。在游戏创作开始时,一些团队成员会通过语言、声音、概念艺术和故事板向团队其他成员演示游戏。
在本课程中,你将观看完整游戏的玩法视频并亲自试玩游戏,而不是仅仅看到游戏的想法。换句话说,我们已经创建了游戏的一个版本,而你的目标是自己创建相同的游戏。
创建电子游戏是一个复杂的问题。因此,游戏通常首先创建一个简化版本,然后创建一系列逐渐复杂的版本,最后一个版本才是完整的游戏。每个版本都会增加一些新功能。
在现实世界中,在确定了游戏功能后,团队中一些经验丰富的设计师会将游戏分解为多个版本,可以按功能或其他标准划分。在本课程中,我们将扮演你团队中经验丰富的设计师角色,决定你创建的每个游戏版本应包含哪些功能。
团队将每个版本分解为多个组件,团队成员共同为每个游戏组件创建描述、测试计划、算法和代码。
互动学习对象
在本课程中,除了在课程论坛上的互动,你将独自学习。然而,为了模拟在团队中工作的真实世界游戏创作体验,你将使用三个互动学习对象:描述构建器、功能测试计划构建器和算法构建器。
这三个互动学习对象通过为三项游戏设计活动提供一个明确的设计伙伴和导师,来模拟真实世界的设计过程。
最佳学习实践
在游戏创作过程的每个任务中,你都需要创建或完善一个构成游戏组成部分的“工件”。为了从本课程中获得最大收益,你应该严格遵循游戏创作流程,并按顺序完成每项活动。每项活动都使用前一项活动的工件。因此,在进入下一项活动之前,你应该诚实地努力创建或组装每个目标工件。

本课程就像一个故事,请确保在进入下一章之前完成每一章。

为了确保你不会卡住,在游戏创作过程的每项活动中,都会使用前一项活动的正确工件副本作为起点。在设计活动中,你应该从你的互动学习对象伙伴和导师那里获得指导,但要避免盲目猜测以迫使你的伙伴揭示正确答案。总的来说,猜测不是通向理解的好途径。
总结

本节课中,我们一起学习了本课程的组织结构以及如何从中获得最大收益。我们了解了课程采用基于问题的学习方法,通过模拟真实团队开发流程来教授Python和游戏设计,并强调了按顺序完成活动、利用互动学习对象指导的重要性。遵循这些方法,你将能更深入地理解编程语言,并为未来的学习打下坚实基础。
006:学习者成功建议 🎯


在本节课中,我们将探讨如何成为一名成功的编程学习者。我们将讨论学习编程的正确心态、有效方法以及如何克服常见的挑战。
编程如同任何一项技能。没有人天生就会编程。只要付出适当的努力,你就能掌握它。这就像学习演奏乐器一样。你不能指望第一次拿起小提琴就能完美演奏。因此,你也不应期望第一次接触键盘就能完全理解编程。
上一节我们提到了编程学习的普遍性,本节中我们来看看如何正确衡量自己的进步。
不要通过与他人比较来衡量自己的成功。你的一些同学可能比你更有经验。我知道不进行比较很困难,看到同学轻松完成编程练习可能会让人非常沮丧。如果你遇到困难,最重要的是不要放弃。衡量你在这门课程中成功的标准,应该是你自身所获得的成长。
你可能会在课程讨论中,感觉别人都明白是怎么回事,而你不明白。不要犹豫,积极参与进来。有些学习者可能课程进度更快。其他人可能在本课程之外有编程经验。无论如何,请参与进来。这会更有趣,你也会学到更多。
正如Elise所说,编程是一项技能。然而,你不能仅仅通过阅读来学会编程。仅仅观看课程视频或观看他人编程是不够的。这就像试图仅通过听来学习拉小提琴。你需要练习、练习、再练习,这需要时间。
我曾担任业余棒球教练超过20年,学习编程就像学习击打棒球。我花了很多时间讲解和演示击球姿势、重心转移以及其他许多要点。然而,我花了更多时间投出成千上万次击球练习。
我的建议是,在本课程中尽可能多地编写代码。你通常不会在第一次在程序中使用某个概念时就理解它。通常你需要多次使用它才能真正理解。你不可能在几天内真正学会编程。这门课程需要更多时间。
此外,如果你是编程新手,不要将别人花费数百小时学习编程的成果与你仅有的几个小时进行比较。你可能只看到了那数百小时的结果,而没有看到背后的努力。如果你想变得更好,你需要练习、练习、再练习。
在编写每个游戏版本之后,思考你可以对程序进行哪些小的修改。并实现它们。测试你的修改并调试程序,直到它们正确工作。这些修改不需要很大或很重要。你可以添加一些文本、改变一种颜色,或者改变显示结果的条件。你做的每一次修改都会给你更多练习机会。
学习编程时,能够追踪程序的执行过程是一项重要技能。我们将在整个课程中使用的调试器是学习和练习追踪的绝佳方式。花时间使用调试器逐行追踪你的程序是值得的。你应该预测接下来将执行哪一行代码,以及该行代码将如何改变程序数据对象。像数据对象的名称和程序语句的顺序这样的小细节非常重要。追踪是学会关注最重要细节的最佳方式。
想象一下,你正在学习如何击打棒球。如果你的教练有一台高速摄像机,她可以分解你挥棒的每一个步骤,找出你做错了什么。这就像调试器以及冻结视频每一帧或程序每一步的能力。
不要害怕犯错。在这门课程中,犯错是好事。你犯的每一个错误都是一个学习和成长的机会。如果你害怕犯错,你就会因为害怕而不敢练习和实验。事实上,错误非常重要,以至于本课程使用的游戏创作过程就包括了测试以发现错误和调试以修复错误。
在犯错这件事上,你并不孤单。我们四个人都已经犯过你将犯的每一个错误,而且我们仍然在犯错。如果你备份了你的代码,就可以用它进行实验,即使犯了错。你不会永久性地破坏任何东西。你总是可以回到你原始的代码。我最喜欢编程的一点是,如果我犯了错误,我不会浪费任何东西。如果我在烘焙或木工中犯错,我会浪费食物或木材。编程资源是虚拟的,几乎是无限的。尝试事物和探索想法的自由是赋予人力量的。
这是一个很好的观点。在编程时,请牢记“只管尝试”的态度。每当你思考“我能那样做吗?”或“如果我尝试这个会发生什么?”时,你就应该去尝试。如果你将“只管尝试”、不害怕犯错、练习和追踪结合起来,你将在学习编程的道路上取得成功。

本节课中我们一起学习了成功学习编程的关键要素:保持成长心态、积极实践、善用调试工具追踪代码、勇于尝试并从错误中学习。记住,编程是一项通过持续练习和探索才能掌握的技能。
007:游戏创作流程 🎮

在本节课中,我们将学习游戏创作流程。这是一个用于解决复杂计算问题的结构化方法,尤其适用于游戏开发。我们将重点介绍计算问题解决的核心技术,并详细分解游戏创作流程的第一步。
概述:什么是游戏创作流程?
游戏创作流程是一个算法或过程,它是一系列用于解决问题或创造产物(我们称之为制品)的动作序列。就像遵循食谱烘焙饼干或策划生日派对一样,使用一个经过验证的流程能更高效、更成功地完成任务。


在游戏开发中,由于涉及艺术、音乐、编程、设计等多个专业领域,流程变得至关重要。本课程的游戏将专注于计算问题,简化了艺术和音乐等部分,但创作过程本身依然复杂,因此我们需要一个清晰的步骤来指导。
计算问题解决技术
在深入流程之前,我们需要了解支撑它的核心技术:计算思维。它主要运用三种技术:


- 问题分解:将大问题拆分为更小、更易管理的子任务。
- 算法:定义解决问题的明确步骤序列。
- 抽象:忽略不必要的细节,专注于核心概念。
本节我们将重点运用问题分解和算法来启动我们的第一个游戏《Hacking》的创作。
为什么需要流程?
我们可以通过一个对比来理解流程的重要性。
- 使用流程(如遵循食谱):需要前期准备,但通常能产出可预测的成功结果(美味的饼干)。
- 不使用流程(即兴发挥):看似省事,但可能导致结果不一致、重复或失败(烤焦或没熟的饼干)。
游戏创作同理。一个清晰的流程能帮助我们系统化工作,避免混乱,并确保最终产品的质量。
游戏创作流程分解
游戏创作本身是一个复杂的任务。通过问题分解,我们可以将其划分为多个子任务。下图展示了完整的游戏创作流程:

如图所示,即使专注于计算部分,“创作游戏”也包含许多环节。我们将按顺序介绍每个子任务。首先,我们从第一个也是基础性的任务开始。
第一步:理解游戏
在解决任何问题之前,你必须先理解它。对于游戏创作,“理解游戏”意味着你需要知道游戏中发生了什么(What)、在哪里发生(Where)、何时发生(When)以及为何发生(Why)。这“四个W”将最终指引你“如何”(How)创建这个游戏。

这类似于写一篇关于蜜蜂差异的论文前,你需要研究不同蜜蜂的外观、栖息地、行为和时间以及原因。

“理解游戏”本身也是一个复杂任务,因此我们可以对其再次进行分解。
分解“理解游戏”:体验式分解
为了深入理解游戏,我们采用一种称为体验式分解的方法。它通过三个层次的参与来逐步深化认知:
以下是三个层次的详细说明:
- 观察:这是被动的第一层次。通过观看他人进行游戏,你可以获得对游戏规则和行为的广泛但浅显的初步印象。例如,观看别人打网球。
- 游玩:这是主动的第二层次。通过亲自参与游戏,你将个人经验与新知识结合,获得更深层次的理解和技能。例如,亲自上场打网球。
- 描述:这是互动的第三层次。通过向他人解释游戏,你需要在不同情境下组织你的知识,这常常能带来最深刻的理解,并迫使你调整观点以适应他人。例如,教练网球或教左撇子球员。
这三个层次有时也被概括为:观看、实践、教授。
在接下来的课程中,你将通过观察和游玩《Hacking》游戏来完成“理解游戏”这一步。“描述”这一步将在创作流程的后续阶段学习。
总结与展望


本节课我们一起学习了游戏创作流程的概貌及其重要性。我们了解到:
- 使用流程(算法)是系统化解决复杂问题(如游戏开发)的关键。
- 问题分解是将大任务拆解为可管理子任务的核心技术。
- 理解游戏是创作的第一步,而体验式分解(观察、游玩、描述)是达成深度理解的有效方法。

在接下来的两节课中,你将应用这些知识,亲自观察和游玩《Hacking》游戏,为后续的创作打下坚实的基础。你所学的这些计算问题解决技术,不仅适用于游戏开发,也能应用于更广泛的编程和问题解决场景中。
008:观察黑客游戏 🎮

在本节课中,我们将通过观察一个完整的“黑客游戏”来理解其运行机制。我们将关注游戏窗口中的各种元素、它们的出现时机以及变化规律。

游戏初始界面 🖥️
游戏开始时,会打开一个窗口。窗口标题为“hacking”,背景为黑色。窗口左侧显示绿色的文本行,模拟老式计算机终端的效果。
文本内容显示计算机处于调试模式,并且剩余尝试次数为4次。这似乎意味着调试模式允许玩家进行最多四次某种尝试。
在这段文本下方,是一个嵌入在符号字符中的单词列表。在该列表下方,系统提示玩家输入密码。
根据目前观察,可以得知这是一个在窗口中进行的密码猜测游戏。文本以绿色显示在黑色背景上,并逐行呈现。玩家将有四次机会来猜测密码。


游戏过程与反馈 🔍
接下来,我们看看输入密码时会发生什么。我首先猜测单词“survive”,因为它是窗口中出现的单词之一。
在窗口右侧,可以看到一个两行的提示信息:“Sur is incorrect, but two of the seven letters in the guess match letters in the same positions of the secret password.” 同时,剩余尝试次数减少了一次。
然后我尝试猜测密码“pudding”。这个猜测也不正确,但提示变为“five of seven letters are in matching positions”。
接着我尝试“hunting”。此时,窗口内容被清空,并在窗口中央逐行显示四行文本。
文本显示了我输入的密码,然后提示游戏正在退出调试模式,登录成功,并告知玩家按回车键继续。当我按下回车键后,窗口关闭。
观察总结与思考 💡
至此,我们完成了对游戏的观察。我们观察到:
- 游戏要求玩家猜测一个密码。
- 玩家最多有四次猜测机会。
- 游戏会检查猜测密码与秘密密码在相同位置上的字母匹配情况,并显示提示。
- 如果猜测成功,游戏会祝贺玩家。
除了视频中展示的情况,你还能想到哪些游戏中可能发生但视频未演示的场景?例如,如果玩家进行了太多次错误猜测,会发生什么?

现在轮到你来玩游戏了。请多次试玩,以获得对“黑客游戏”更深入的理解。
009:2.3.1 游戏版本


在本节课中,我们将学习如何将复杂的游戏开发问题分解为更小、更易管理的版本。我们将介绍一种新的问题分解方法——特征选择,并探讨如何通过问题细化来规划每个版本的实现过程。
已完成的任务
上一节我们完成了“理解游戏”任务中的“观察游戏”和“试玩游戏”两个子任务。现在,让我们揭示下一个任务和游戏创建过程。
🎮 创建版本
“创建版本”旁边的循环箭头表示这个任务可能需要执行一次或多次。
我们需要将游戏划分为多个版本,因为大问题很难一次性解决。因此,我们将一个大问题分解成几个更小、更容易解决的问题,这些更小的问题就称为版本。

之前,我们使用经验分解,根据“参与程度”这一标准,将“理解游戏”分解为“观察游戏”和“试玩游戏”两个子任务。参与程度衡量的是一个人与问题的互动程度:观察的互动最少,试玩互动更多,而描述或解释的互动最多。

🔍 特征选择分解法
现在,我们将使用一种新的问题分解方法——特征选择,来将我们的游戏分解为多个版本。
完整的黑客游戏包含一系列特征,例如:
- 显示密码
- 猜测密码
- 使用窗口
- 支持多次尝试
- 提供提示
一次性尝试开发所有特征是很困难的。相反,创建一个只支持部分特征的版本会更容易。第一个版本从少量特征开始,后续的每个版本再添加一些新特征,直到最后一个版本拥有所有必需的特征。这使得每个版本都更容易创建,因为我们每次只需要关注少量的改动。


当你学习大多数新技能时,都会用到特征选择。例如,学习一门新语言时,你不会从写论文或发表演讲开始,因为这些活动需要许多复杂的语言特征,如难词和复杂语法。相反,你会从学习简单的单词和短句开始,然后再添加更复杂的语言特征。
特征选择决定了每个版本必须实现的特征集。
⚖️ 特征选择的标准
各版本的实现工作量应该保持平衡。因此,特征选择的标准之一是平衡特征集。你应该为每个版本选择那些需要大致相同工作量来实现的特征。
例如,单独实现“使用窗口”这一特征的工作量,可能与实现“显示密码”和“猜测密码”两个特征加起来的工作量相当。
有时,将一个单一特征拆分成多个特征有助于平衡特征集。例如,“使用窗口”既包括打开窗口,也包括关闭窗口。
然而,特征选择的第二个标准是特征依赖性,因为某些特征依赖于其他特征。例如,如果窗口没有被打开,它就无法被关闭。
不幸的是,在没有经验的情况下,很难准确估计特征的工作量。因此,在本课程中,我们将为你选择版本的特征集。随着你的使用,你将学会识别好的特征集。在本课程之后,你就可以运用自己的经验,为自己的项目创建特征集了。
🛠️ 创建版本与问题细化
让我们回到“创建版本”和游戏创建过程。“创建版本”作为一个单一任务过于复杂。因此,我将应用第三种问题分解形式——问题细化,来创建合适的子任务。
问题细化通过将一个完整问题分解为一系列细节逐渐增加的解决方案来解决问题。其标准是细节层次,也称为精确度。
第一个解决方案是完整的,但细节最少。例如,画一个笑脸只有很少的细节,但它对于“画一张脸”这个问题来说是一个完整的解决方案。
其余的解决方案精确度逐渐提高。最后一个解决方案和第一个解决方案一样完整,但包含了所有细节。例如,可以向笑脸添加细节,直到最终的草图成为最详细的。
这是问题细化的另一个例子:让我们规划一次从阿尔伯塔大学到不列颠哥伦比亚大学的旅行。
我可以写一份详细的路线说明,从阿尔伯塔大学开始,到不列颠哥伦比亚大学结束。这就像从开始到结束逐行编写程序一样。仅仅从阿尔伯塔大学到埃德蒙顿机场,就有超过10条详细的指令。
然而,在创建了这些详细的指示后,我可能会决定改乘火车而不是飞机。那么所有详细的指令现在都没用了。我因为过早关注细节而浪费了时间。
相反,我会使用问题细化来创建一个不精确但完整的计划:
- 从阿尔伯塔大学开车到埃德蒙顿机场。
- 从埃德蒙顿机场飞往温哥华机场。
- 从温哥华机场开车到不列颠哥伦比亚大学校园。
然后,我们可以在决定采用这个计划之前,通过研究成本和时间来测试它。如果我们想尝试不同的计划,比如从埃德蒙顿坐火车到温哥华,修改这个简短的计划比修改一长串详细的指示要容易得多。在研究完所需成本和时间后,我可以通过这个计划来制定更精确的(乘飞机或火车的)路线指示。
同样地,在知道解决方案的大致形式之前就编写代码是不明智的。你应该在将细节编写为代码之前,先规划好你的解决方案。在软件开发中,这种计划被称为设计。
因此,我将把“创建版本”分解为三个任务:
- 创建设计:产生一个不精确的解决方案,即一个版本的黑客游戏大纲。
- 创建程序:将这个不精确的大纲转换为该版本黑客游戏的精确程序。
- 反思:评估你对所应用的编程语言技术的了解,评估你的解决方案,并为下一个版本确定改进之处。
“反思”本身就是一种细化,因为它将你对解决方案、知识和技能的理解,提炼到当前版本所需的最高细节层次。此外,反思类似于经验分解中的“解释或描述”部分,因为你的反思就像向自己解释这些想法。这种最高层次的参与度会带来对本版本最详细的理解。
这是最后一次细化,因为在反思之后,你将拥有对解决方案最精确的设计、代码和理解。
📝 深入“创建设计”任务
现在让我们回到“创建设计”任务。我们应该如何完成这个任务?尝试一些新方法怎么样?我们可以把它分解成子任务。我知道这并不新鲜,但它很有效。我们的目标是不断细分任务,直到最终的子任务变得非常简单,解决方案显而易见。
让我们使用问题细化来创建三个子任务:
- 理解版本
- 创建版本测试计划
- 创建算法
“理解版本”任务看起来可能很熟悉,因为你已经见过“理解游戏”任务。事实上,如果我们展开“理解版本”任务:
- “观察游戏”对应“观察游戏版本”。
- “试玩游戏”对应“试玩游戏版本”。
请记住,经验分解有三个组成部分:观察、试玩以及描述或解释。“创建版本描述”对应“描述”。这是一个新任务,它产生游戏版本的书面描述。这个描述是一个工件,解释了游戏的功能,包括其特征和玩家操作。这个描述是不精确的,因为它提供了我们实现的目标,但没有提供实现本身的细节。
“创建设计”中的下一个任务是“创建版本测试计划”。这个任务创建一个测试计划,用于检查特征集是否正确实现。这个工件比游戏描述更精确,因为它更明确地考虑了多种游戏场景。描述描述了所有可能的结果,但测试计划规定了哪一系列玩家操作会导致哪种游戏结果。例如,描述指出可能会显示成功或失败的结果,但测试计划精确规定了必须如何玩游戏才能体验到每种结果。


“创建算法”任务产生设计中最精确的工件。算法是一系列步骤,规定了代码应如何编写。它比测试计划更精确,因为算法更接近代码。每个步骤对应几行代码。然而,算法应该独立于任何特定的编程语言,因此算法不如实现它的代码精确。




🚀 开始实践
现在,“创建设计”中的所有任务都已介绍完毕,是时候让你回到游戏创建过程了。

首先,从“理解版本”开始。观看第一个版本黑客游戏的试玩视频。问自己观看完整游戏时间样的问题:游戏看起来怎么样?事物何时在游戏中出现?玩家可以采取哪些操作?你需要这些问题的答案来创建你的版本描述。


总结

本节课中,我们一起学习了如何通过特征选择将复杂游戏分解为多个循序渐进的版本,以及如何运用问题细化的方法,将“创建版本”这个大任务逐步细化为“创建设计”、“创建程序”和“反思”等可操作的具体步骤。我们了解到,良好的规划(设计)应先于具体的编码,并且平衡的工作量和考虑特征依赖性是成功分解的关键。现在,你已经准备好开始为第一个游戏版本创建详细的设计了。
010:观察黑客小游戏版本1 🎮

在本节中,我们将观察并分析“黑客小游戏”的第一个版本。我们将重点关注这个初始版本的游戏界面、玩家交互方式以及它与最终版本之间的核心差异。
游戏界面观察 👀
上一节我们了解了游戏的最终形态,本节中我们来看看它的初始版本。当程序启动时,游戏会使用操作系统默认字体显示文本。
- 文本逐行显示,每一行都位于上一行的下方。
- 这与最终版本不同。最终版本的游戏窗口拥有独立标题,并在窗口内的不同位置显示绿底黑字的文本。
游戏规则与交互 🔍
这个初始版本的游戏规则也有所简化。以下是游戏界面中呈现的关键信息:
- 游戏标题头显示只允许1次尝试机会,而最终版本允许4次。
- 显示的密码列表不包含任何象征性字符(如
*或#)。 - 游戏仍然会提示玩家输入密码。
示例游玩过程 🕹️
接下来,我们通过一个具体操作来观察游戏的反馈机制。我输入了密码 hunting,这个密码在最终版本中是正确答案。
然而,程序在我输入后打印了一条信息,指出我的猜测是不正确的,即使我输入了正确的答案。最后,我按下回车键结束了游戏。
总结与练习 📝
本节课中我们一起观察了“黑客小游戏”版本1的基本运行情况。我们看到了它在文本显示、尝试次数和反馈机制上与最终版本的区别。

以上就是对黑客小游戏版本1的观察。现在,轮到你亲自尝试游玩这个版本了。你可以多次游玩,以更深入地理解这个初始版本的游戏逻辑和设计。
011:描述黑客游戏版本1 🎮

在本节课中,我们将学习如何为“黑客游戏”的第一个版本创建一份正式的游戏描述。这份描述将准确说明游戏运行时发生的一切,包括显示的内容、玩家的操作以及游戏的反应。
概述
游戏描述是“理解版本”阶段的最后一项子任务。它解释了游戏进行时发生的一切,包括游戏的外观、变化方式以及对玩家操作的响应。本质上,描述规定了游戏中必须包含的功能以及这些功能如何交互。一份好的描述能让读者完全了解如何玩游戏,但它不涉及游戏是如何实现的。
上一节我们观察并试玩了黑客游戏版本1,本节中我们来看看如何系统地描述它。
使用描述构建器
我们将使用“描述构建器”工具来创建描述。这个工具模拟了真实的团队协作环境。在真实环境中,每个成员观察游戏并提出描述性属性,其他成员提供反馈和指导,最终共同完成游戏描述。构建器提供了描述当前版本所有方面的短语,你的任务是将这些短语组合成一份准确、全面的描述。
描述构建器窗口的左上角是当前活动标题“Hacking version 1 description”。旁边是菜单按钮,你可以保存描述、打开已保存的描述、下载当前描述的PDF、重置练习或查看使用教程。点击右上角的按钮可以观看当前版本的游戏视频。
构建器包含两个主要面板。左侧面板有两个标签页,每个标签页包含描述短语:
- “对象和操作”标签页:包含游戏中显示的对象(如标题、消息)以及玩家或游戏执行的操作(如输入猜测)。
- “属性”标签页:包含对象的属性(如位置、内容)。
右侧面板是你构建描述的地方。你必须将所有相关的对象、操作和属性描述添加到右侧面板,以创建准确的描述。
构建描述的原则
以下是构建描述时需要遵循的关键原则:
- 顺序原则:对象和操作可以按任何顺序添加,但必须根据它们在游戏中发生的时间顺序进行正确放置。新添加的对象或操作必须放在游戏中发生在它之前的所有内容之下,以及发生在它之后的所有内容之上。
- 属性依附原则:属性不能独立存在,它必须依附于一个对象或操作。在描述中,属性必须作为其描述对象的子项添加。
- 反馈机制:构建器会提供反馈来指导你。反馈信息会出现在右下角。
开始构建描述:一个示例
我将演示如何添加前几个短语,并展示你可能遇到的不同类型的反馈。观看完这个示例后,将由你完成剩下的描述。


第一步:确定首先发生什么?
在黑客版本1中,首先显示的是“debug mode”这行文本。在“对象和操作”标签页中,没有直接描述这行文本的短语。我切换到“属性”标签页,找到了短语“The first content line indicates debug mode”。我尝试将它添加到描述中。
反馈一:属性必须依附于对象
添加失败,属性回到了左侧面板。构建器给出了反馈:“an attribute must belong to an object or action”。这是第一种反馈,说明了属性和对象之间的关系。属性不能脱离对象存在。


第二步:先添加对象
我需要先添加一个表示“有对象被显示”的短语。在“对象和操作”标签页中,有六个可用短语。其中四个是对象:密码列表、标题、失败结果、猜测提示。两个是操作:玩家按回车键结束程序、玩家输入猜测并按回车键。
“debug mode”属性是游戏启动时前两行文本的一部分,我们称这两行为“标题”。因此,我选择“The game displays a header”并将其添加到右侧面板。添加成功。
第三步:为对象添加属性
现在我可以尝试将“The first content line indicates debug mode”作为标题对象的属性添加。添加成功。
每个显示对象都有五个属性:内容、位置、大小、颜色、时间(或时序)。标题的内容是其主题。标题由两行内容和一个空行组成,所以我添加属性“It consists of two content lines followed by a blank line”。我已经添加了描述第一行内容的属性。要完成内容描述,我还必须找到描述“attempts left”内容行的属性并添加到标题对象。
标题的位置是顶部输出行。所以我添加属性“It is the top line of game output”。
由于此版本使用纯文本,我们使用默认的操作系统文本大小和颜色,因此不为每个对象指定这些属性。
对象的时序属性描述它何时显示、更改或删除。此版本中的所有对象都按我们从上到下描述的顺序显示,且不会被擦除。由于是文本版本,空间顺序也定义了时间顺序。在此版本中,“下方”一词既指空间上的下方,也指时间上的之后。
第四步:添加下一个对象
游戏中接下来显示的是密码。所以下一个要添加到描述中的对象是“The game displays a password list”。它必须放在标题下方,因为它在标题之后显示。
如果我错误地把它放在标题上方,会得到第二种反馈:“this concept should be placed higher or lower in the description”。这描述了概念在描述中的位置。新添加的对象或操作必须根据其在游戏中的时间顺序,正确放置在已存在内容的上方或下方。
如果我错误地尝试将密码列表作为标题的属性添加,构建器不会允许。因为密码列表本身是一个显示对象,不是其他显示对象的属性。
正确地将密码列表放在标题下方后,添加成功。
第五步:为密码列表添加属性
属性可以按任何顺序添加。
首先是内容属性。我添加“It consists of 13 content lines followed by a blank line”。密码列表还有第二个内容属性,我将留给你来完成。
其次是位置属性。我添加“It is directly below the header”。
反馈三:属性不属于此概念
为了展示第三种反馈,我尝试将属性“It indicates the player should enter a password”添加到密码列表。这是错误的,构建器显示新反馈:“this attribute does not belong to this concept”。“输入密码”属性实际上是密码提示的属性,而不是密码列表的属性。
问题分解的新方法
描述构建器使用了两种新的问题分解方法:时间分解和空间分解。它们允许我们将创建描述的任务分解为对象、操作和属性描述。
- 时间分解:根据任务必须执行的顺序来划分任务。我们在旅行规划示例中实际上就使用了时间分解,因为每个行车指令都必须遵循前一个指令的时间顺序。在创建版本描述时,通过按游戏中发生的顺序描述对象、操作和属性来应用时间分解。
- 空间分解:根据任务的空间位置来划分任务。所有影响单个对象的细节被分组在一起。当我们开始描述一个特定的游戏对象时,我们将其所有属性放在一起描述。
你的任务
现在轮到你了。请完成以下步骤:
- 为密码列表添加另一个内容属性。
- 自行完成版本描述的其余部分。
如果你改变主意,不想将某个对象或操作添加到描述中,只需将其拖回“对象和操作”面板即可。如果描述构建器中出现了重复的短语,则必须多次使用它们。
你完成的描述必须按游戏中的发生顺序列出所有对象和操作,并且必须为每个对象添加所有适当的属性(属性可以按任何顺序添加)。


总结

本节课中,我们一起学习了如何为游戏创建正式描述。我们介绍了描述构建器的使用方法,理解了描述中对象、操作和属性的组织原则,特别是时间顺序和属性依附关系。我们还接触了时间分解和空间分解这两种新的问题分解策略。通过实践,你现在应该能够为黑客游戏版本1构建一份完整、准确的描述了。
012:为黑客游戏版本1创建测试计划

概述
在本节课中,我们将学习如何为“黑客游戏”的第一个版本创建功能测试计划。测试计划是确保我们编写的程序行为符合预期的重要工具。我们将了解测试计划的结构、创建步骤以及它在游戏开发过程中的作用。
什么是测试计划?
上一节我们完成了游戏版本1的描述设计,本节中我们来看看如何为它创建测试计划。
一个测试计划是一组对程序执行的测试,目的是确保其表现符合预期。最初,我们的测试计划将只包含功能测试,非功能测试将在后续引入。
每个功能测试包含一个动作和一组问题。测试者手动执行动作,并使用这组问题来评估游戏对该动作的响应是否符合游戏描述中的规定。每个测试都以“是”或“否”的问题形式提出。所有答案都必须为“是”,程序才算完全正确。
我们测试游戏是为了确保我们创建的游戏与我们预期创建的游戏相匹配。
为何在编程前创建测试?
我们在编写程序之前创建测试,是为了在描述和代码之间提供一个额外的精确度层级。在编程前创建测试,迫使你在编码之前规划游戏的行为。
在工业界,在编写代码之前创建测试通常被称为测试驱动开发或TDD。然而,TDD通常使用一个测试程序来自动测试应用程序,而不是使用手动测试者动作。
问题细化原则建议我们逐步提高精确度,作为简化问题的有效方法。测试允许我们在需要编写逻辑代码之前,通过明确描述玩家可能采取的动作以及游戏应对每个动作的方式,将逻辑引入我们的设计中。
测试还能指出可能出错的地方。在开始编写代码之前了解这些信息非常有用,这样代码就可以处理这些潜在的出错情况。
测试计划的演进
每个版本都会修改前一个版本的测试计划。到最终版本时,你的测试计划将测试游戏的所有功能和大部分逻辑。
随着游戏复杂度的增加,对所有可能的玩家动作的所有组合进行完整测试变得不切实际。例如,考虑一个有八个任务、玩家可以按任意顺序完成的电子游戏。这会导致8的阶乘,即超过40,000种不同的游戏路径。为每一种任务完成顺序进行测试是不切实际的。
测试计划的好处
一个测试计划允许任何人快速发现游戏中的错误或缺失的功能,不需要具备程序的专业知识。事实上,在商业游戏中,负责测试的质量保证团队通常与编程团队不同。
由于我们的游戏并不复杂,你将能够为所有具有代表性的游戏路径创建测试。
功能测试计划构建器介绍
以下是功能测试计划构建器的使用指南。
构建器左上角是活动标题“黑客版本1功能测试计划”。与描述构建器类似,你可以按右侧的按钮查看游戏玩法视频。如果你想保存、加载或重启练习,可以使用左侧的菜单按钮。

构建器有三个面板:
- 左侧面板包含两个标签页:“动作”和“问题”。
- 中间面板是你通过添加动作和问题来创建功能测试计划的地方。
- 右侧面板包含当前版本的描述。

构建测试计划的步骤
以下是创建测试计划的具体步骤。
-
添加动作:功能测试必须按照测试者应执行动作的顺序列出。这些动作必须构成一个或多个从开始到结束的完整游戏路径。在测试计划中定位动作就像在描述中定位对象和动作。第一个动作必须是“启动程序”。
-
添加问题块:问题按对象分组,称为“问题块”。你将选择这些块,而不是单个问题。每个问题块必须属于它所测试的那个动作。例如,“游戏是否显示标题?”这个块测试的是“启动程序”这个动作的结果。
-
处理反馈:构建器会提供反馈以确保计划有效。例如,不能将动作放在错误的时间顺序上,也不能将问题块放在动作之外。

测试不同游戏路径

测试计划与描述不同。测试要求多次运行游戏,以检查在所有可能游戏条件下的正确行为。
版本1的描述指出,在玩家输入猜测后,游戏会显示失败结果。无论玩家输入什么猜测,都会发生这种情况。因此,在这个版本中,正确和错误的密码都必须导致相同的失败行为。
因此,测试计划必须测试这两个动作,这需要我玩两次游戏。描述没有区分输入正确和错误的猜测。测试计划更精确,因为它明确测试了两个特定的游戏场景。
虽然在这个版本中有许多不同类型的错误猜测,但你只需要一个错误和一个正确的猜测来代表两种最常见的游戏路径。
关于重复项和额外测试的说明
以下是关于测试计划中重复项和额外测试点的说明。


注意,有些动作(如“按回车键结束程序”)和问题块是重复的。如果一个动作或问题块必须在测试计划中使用多次,它就会被复制。
然而,标题、密码列表或猜测提示的测试没有重复,即使你将测试程序两次。这是因为在玩家做出猜测之前,你不需要重新测试任何内容,因为在此之前游戏的行为方式总是相同的。
你必须重新测试失败结果和结束提示,因为输入错误的密码可能会影响该不同玩家动作之后的所有游戏过程。
通常,描述中的每个属性都会在测试计划中生成一个问题。此外,可能还有额外的问题来测试游戏玩法,但并不对应于某个特定属性。例如,“游戏提示进行猜测”有两个必须测试的属性。然而,还有一个额外的问题是“游戏是否等待按下回车键”。测试这种描述中没有属性代表的额外等待功能非常重要。
总结

本节课中我们一起学习了如何为“黑客游戏”版本1创建功能测试计划。我们了解了测试计划的目的、结构以及创建步骤。关键点包括:在编程前创建测试以增加精确度,测试计划需要覆盖不同的游戏路径(如正确和错误密码),以及如何处理重复的测试项和描述之外的额外功能点。在接下来的活动中,你将通过添加缺失的动作和问题来完成这个功能测试计划。
013:创建算法(黑客游戏版本1)

在本节课中,我们将学习如何为“黑客”游戏版本1创建算法。这是“创建设计”阶段的最后一个子任务。我们将了解算法的定义、特性,并使用算法构建器工具来构建一个具体的算法。
什么是算法?
上一节我们完成了版本测试计划,现在我们来创建算法。算法是解决问题的步骤序列。它比测试计划更精确,因为它定义了解决方案的逻辑。这种逻辑需要考虑到问题中可能出现的所有潜在情况,并规定如何处理它们。
算法是对测试计划的细化。由于我们的问题是创建游戏,因此每种情况都是玩家操作可能引发的一系列游戏事件。因此,算法步骤必须能适应所有这些游戏事件序列。
虽然算法比测试计划更精确,但它比代码的精确度低。它之所以不那么精确,是因为它比代码更易于人类阅读,并且忽略了不重要的细节。
例如,让我们为“吃苹果”这个任务创建一个不精确的步骤列表:
- 拿起苹果。
- 吃苹果。
- 丢弃果核。
算法步骤按从第一到最后的顺序执行。这意味着算法步骤必须按照它们应该执行的顺序列出。算法使用时间分解将问题分解为多个步骤。存在一些机制允许算法选择性地跳过或重复某些步骤,你将在版本3中学习到第一个这样的机制。
顺序在算法中很重要,否则算法可能导致错误或无法解决问题。如果“吃苹果”和“丢弃果核”这两个步骤颠倒,算法就无法解决问题,因为你不能在丢弃苹果后再吃它。
在本课程中,所有算法都是编程语言无关的。这意味着你创建的算法是一个不依赖于特定编程语言的通用解决方案。你稍后将编写一个实现该算法的Python程序。同样的算法也可以用其他编程语言(如Java或C++)实现。
使用算法构建器
你将使用算法构建器来创建你的算法。这个工具与你目前见过的其他构建器不同,因为它基于连接各种图形形状,而不是排列文本行。


每个形状将包含一个算法步骤。步骤之间用箭头连接,以指示算法执行时的步骤顺序。当你编写程序时,算法中的每个步骤最终将转化为几行代码。
让我们通过一个示例算法来探索算法构建器。在左上角,你可以看到游戏标题和版本号:“制作蛋糕 版本1 算法”。
算法构建器有一个主面板,你将在其中创建对问题的完整但不精确的解决方案。
以下是制作蛋糕算法的步骤:
- 制作面糊
- 烘烤蛋糕
- 装饰蛋糕
主面板左侧的组件面板由各种形状组成,每种形状代表你可以添加到算法中的一种步骤类型。在后续版本中,你会看到其他形状,但目前面板中只有一个形状:一个标记为“步骤”的矩形。矩形代表一个或多个精确的程序语句序列。
矩形中的每个步骤由一个动作和一个对象组成。例如,“制作”、“烘烤”和“装饰”是动作;“面糊”和“蛋糕”是对象。当你添加一个步骤时,必须从词汇表中选择合适的动作和对象词来命名该步骤。
主算法已经完成。现在,我将通过添加细节来增加其中一个步骤的精确度。“装饰蛋糕”这个步骤的具体做法并不明确。
你可以通过单击其形状来查看和编辑主面板中某个步骤的细节。这将创建一个新面板,将单个步骤扩展为更精确的步骤。这些步骤也由连接的形状表示。例如,展开“装饰蛋糕”步骤后,会看到其详细步骤:制作糖霜、涂抹糖霜、添加蜡烛、点燃蜡烛。
所有这些步骤都使用额外的动作(涂抹、添加、点燃)和对象(糖霜、蜡烛),并遵循从算法词汇表中组装的“动作-对象”格式。
构建黑客游戏版本1的算法


我将清空算法构建器,开始构建黑客游戏版本1的算法。
版本1描述的第一句话是“游戏显示一个标题”,因此我将为此部分游戏创建一个算法步骤。
我将在主面板中添加一个步骤来完成此操作。我必须从提供的词汇表中选择一个动作和一个对象。词汇表包含你在版本1描述和测试计划中使用过的对象(如“游戏”、“猜测”)和动作(如“显示”、“提示输入”)。你应该参考你的描述,为每个矩形从词汇表中选择合适的动作和对象。
我将从动作列表中选择“显示”,从对象列表中选择“标题”,然后单击“添加步骤”按钮来完成此步骤。描述中包含许多关于标题的细节,这些细节将在代码中体现,但不会包含在算法中。对象的大多数属性在算法中不予描述。
描述中的下一个对象是密码列表。你将在下一个活动中添加一个步骤来显示密码列表。
描述接着指出游戏会提示输入猜测,因此我将为提示添加一个步骤。我将选择动作“提示输入”和对象“猜测”。
你可以随时按下“检查算法”按钮来检查你的算法是否正确和完整。反馈信息表明主程序缺少两个组件。你已经知道必须添加一个显示密码的步骤,所以还有一个步骤缺失。
描述中的下一个动作是“玩家输入猜测并按回车键”。这个动作不会成为算法步骤,因为算法无法规定玩家的操作。有些算法步骤是对玩家操作的响应,但没有哪个算法步骤会转化为迫使玩家采取行动的代码。


因此,最后一个算法步骤应该显示失败结果。我将使用动作“显示”和对象“失败结果”来添加一个步骤。
完善算法步骤
除了显示密码列表的步骤外,算法应该已经完成。然而,“显示失败结果”这个步骤有两个重要部分:显示结果和提示玩家结束程序。
我将展开“显示失败结果”步骤并添加这两个步骤:显示失败结果和提示结束。但是,如果我在主面板中使用相同的名称来表示通用的“显示失败结果”步骤和它内部包含的更具体的“显示失败结果”步骤,将会导致错误。
为了避免这个错误,我将重命名其中一个步骤。一般来说,不要对执行不同操作的两个步骤使用相同的名称。在主面板中,我将删除“显示失败结果”步骤,并用一个名为“结束游戏”的新步骤替换它。删除一个步骤也会删除其面板内的所有步骤。“结束游戏”是一个很好的通用名称,因为显示结果和提示结束都是结束游戏的一部分。
一般来说,正确分解算法的方法有很多种。例如,可以将“显示失败结果”和“提示结束”都从“结束游戏”面板移到主面板,并保持正确的步骤顺序。然而,最好将这两个步骤保留在“结束游戏”面板中。随着算法规模变大,如果每个面板只包含少量步骤,算法将更容易理解。分解的目的就是将每个任务分解为少量可管理的子任务,而这正是算法面板允许你做的事情。如果我们不使用面板,黑客游戏最终版本的算法可能会看起来非常庞大和复杂。
由于分解问题的方法有很多,你制作的每个算法只是众多可能正确算法中的一种。在本课程中,算法构建器将引导你为每个版本找到一个特定的算法。你可能需要进行实验才能发现每个版本的具体算法,算法构建器的反馈将引导你走向这个特定的算法。
现在轮到你了。你必须通过添加缺失的步骤来修复当前算法中的错误。你还必须展开“结束游戏”步骤,并添加结束游戏所需的那两个步骤。
完成此活动后,在将算法转化为Python代码之前,你将完成几个关于Python的课程。

总结
本节课中,我们一起学习了算法的概念,它是一种解决问题的精确步骤序列。我们了解了算法比测试计划更精确但比代码更抽象的特性,并掌握了使用算法构建器为“黑客”游戏版本1创建具体算法的过程。我们学会了如何通过“动作-对象”格式定义步骤,以及如何通过分解和细化步骤来构建完整的算法逻辑。
014:Python表达式求值示例 🐍

在本节课中,我们将学习如何评估两种简单的Python表达式:字面量字符串和函数调用。我们将通过Python交互式环境来观察表达式的求值过程,并理解在此过程中可能出现的不同错误类型。
表达式求值过程
上一节我们了解了Python的基本概念,本节中我们来看看Python解释器如何处理我们输入的代码。

我将在Python交互式环境中输入一些Python表达式并观察结果。
第一个表达式是 ‘hello’。我输入后按回车键,Python解释器显示了我输入的相同字符。
第二个表达式是 len(‘hello’)。这次,解释器显示了数字 5。Python进行了一次计算。
以下是这两个过程中实际发生的情况:
- 在每种情况下,我输入了一个简单的Python表达式。
- Python解释器对表达式进行求值,得到一个结果对象。
- 解释器在交互式环境中以人类可读的形式显示这个结果对象。
- 随后,解释器在下一行显示
>>>提示符,表示它已准备好评估下一个Python表达式。
字面量字符串
表达式 ‘hello’ 被称为字面量字符串。字面量字符串可以用单引号或双引号括起来。在本课程中,为简洁起见,我们使用单引号。我们所说的“引号”即指单引号。
函数调用
表达式 len(‘hello’) 在计算机科学中被称为函数调用。
一个函数就像一台有输入和输出的机器。len 函数有一个输入(或称为参数)对象,并产生一个输出(或称为结果)对象。它的参数是一个字符串对象,结果是一个整数对象,表示该字符串中有多少个字符。
可以类比在汽车餐厅点餐:输入是你的口头订单,输出是你收到的餐食。
数学中也使用函数,例如平方函数和平方根函数。3的平方是9,4的平方根是2。
我之前使用的 len 函数是一个内置函数,len 是 length(长度)的缩写。字面量字符串 ‘hello’ 被用作函数的参数。len 函数计算其参数对象中的元素数量。由于 ‘hello’ 有五个字符,len 计算出了 5。
探索内置函数
你如何知道内置函数 len 的作用?还有哪些其他内置函数可以使用?
Python的官方文档位于Python官网上。Python有一个包含内置函数的标准库,标准库文档描述了有哪些内置函数可用以及这些函数的功能。
以下是关于 len 函数的说明:
len函数返回一个对象的长度(项目的数量)。- 参数可以是序列(如字符串、字节、元组、列表或范围)或集合(如字典、集合或冻结集合)。
- 到目前为止,你见过的唯一序列类型是字符串。
我将 len 函数应用于另一个字面量字符串 ‘goodbye’。按回车后,出现了整数 7。这是合理的,因为字符串 ‘goodbye’ 中有7个元素。
通过类比,如果你在同一家餐厅点不同的餐,你会收到不同的食物。输出取决于输入。此外,如果你在不同的餐厅点相同的餐,你也会收到不同的食物。输出不仅取决于输入,还取决于函数本身。
例如,当你对相同的输入对象 ‘hello’ 应用另一个内置函数 id 时,你会得到一个不同的结果对象。id 函数的含义将在后面讨论。这里的重点是,不同的函数会产生不同的结果。
错误类型分析
Python能识别你输入的每一个字符序列吗?
我输入 len? 并按回车求值。Python交互式环境报告了一个语法错误。Python识别 len 为一个有效的词法单元,但 ? 不属于任何有效的Python词法单元。这实际上被称为词法错误,但这个Python解释器将所有词法错误都报告为语法错误。词法单元、词法错误和语法错误将在未来的课程中解释。
为什么需要括号?如果我漏掉括号,输入 len ‘’ 会发生什么?虽然这个表达式包含两个有效的Python词法单元,但它们没有构成一个有效的Python表达式。要理解这个错误发生的原因,你必须理解Python的语法。在定义Python语法之前,看更多例子是有用的。
为什么 ‘hello’ 有引号而 len 没有?回想一下,‘hello’ 被称为字面量字符串,而没有引号的 len 被称为标识符或名称。
我把括号加回来,但通过去掉引号将 hello 从字面量字符串改为标识符。这次,Python交互式环境报告了一个名称错误,这是一种语义错误。这个错误表明名称 hello 没有被定义。要理解这个错误发生的原因,你必须理解Python的语义。
课程总结
本节课中我们一起学习了:
- 你刚刚看到了一些成功的Python求值示例和三种不同类型的错误:词法错误、语法错误和语义错误。
- 识别这三种错误非常重要,因为修复每种错误的方法是不同的。
- 你将在下一课开始学习关于词法单元、词法分析、语法和语义的知识。


015:Python解释过程

概述
在本节课中,我们将要学习Python解释器评估代码时所采用的三步解释过程。这个过程包括词法分析、语法分析和语义分析,是理解Python如何“读懂”并执行代码的基础。
Python解释过程简介
Python程序由语句构成。最简单的语句是表达式语句,它在一行中包含一个或多个表达式。例如,len("hello") 就是一个包含单个表达式 len("hello") 的表达式语句。
当你在Python交互式环境中输入一串字符并按回车键后,Python解释器会尝试将这串字符解释为一行Python语句。那么,解释器具体使用什么过程来将字符序列解释为Python语句呢?
自然语言与编程语言的类比
Python使用的过程与自然语言(如英语、法语、普通话)的处理过程类似。
在自然语言中,文本被划分为基本单元,如单词和标点符号。这些单元被组合成称为句子的句法结构。然后,意义被赋予句子中的单词和标点。
Python解释过程也遵循类似的步骤,以确保代码能被正确理解和执行。
第一步:词法分析
上一节我们介绍了Python解释过程与自然语言的类比,本节中我们来看看第一步——词法分析。
在这一步中,字符序列被翻译成一系列称为“词法单元”的基本单元。当你用语言思考、阅读、书写或说话时,你使用的是像“Mary”这样的词法单元,而不是像“M”、“a”、“r”、“y”这样的单个字符。
在Python中,解释器将字符组合成词法单元。词法单元的定义是:一个具有基本语言单元意义的字符序列。
在自然语言中,有两种词法单元:一种是像“Mary”、“is”、“I”这样的单词;另一种是像问号和逗号这样的标点符号。
编程语言也有多种词法单元。Python有五种不同的词法单元:
- 分隔符:在编程语言中类似于自然语言的标点。例如,左括号
(和右括号)就是Python中的分隔符词法单元。 - 字面量
- 标识符
- 运算符
- 关键字
字面量和标识符在编程语言中类似于自然语言中的不同词性。名词指代特定对象,如“Alberta”。编程语言中的字面量字符串类似于名词,因为它也指代特定对象,如 "hello"。代词在不同时间指代不同的人,例如“I”在我(Dwayne)指代自己时指代我,在另一个人(Elise)指代自己时指代她。编程语言中的标识符类似于代词,因为你将发现同一个标识符可以在程序的不同部分指代不同的对象。例如,在表达式 len("hello") 中,标识符 len 指代长度函数对象;然而,在程序的其他地方,也可以使用相同的标识符 len 来指代一个不同的对象。
如果在自然语言中使用了无效的词法单元会怎样?例如,字符序列“its snus”如何被解释?在自然语言中,列出所有有效单词词法单元的表格称为字典。如果一个字符序列不在字典中,它就不能构成有效的词法单元,因此会导致词法错误。“its”是一个有效的英语单词词法单元,但“snus”不在英语字典中,所以它会导致词法错误,即一个无效的词法单元。
类似地,如果编程语言解释器在从字符创建词法单元时遇到错误,这被称为词法错误。然而,Python解释器将词法错误报告为一种语法错误。
还记得我输入字符序列 len? 时发生了什么吗?问号 ? 不是一个有效的Python词法单元,就像“snus”不是一个有效的英语单词一样。
如果我输入字符序列 "hello(开头有引号,但结尾没有),Python解释器会报告一个语法错误,这实际上是一个词法错误,因为 "hello 没有结束引号,不是一个有效的字面量字符串词法单元。
第二步:语法分析
在成功将字符转换为词法单元之后,解释过程进入第二步——语法分析。
在这一步中,词法单元被组合成英语句子或Python语句。适用于自然语言和编程语言的语法定义是:语法定义了句子和语句的格式或结构。
人们如何将自然语言词法单元(单词和标点)组合成语法正确的句子?自然语言使用语法。考虑一个非常小的英语子集,它包含一个标点符号(问号)和六个单词:动词“was”和“is”,名词“Mary”和“school”,以及疑问副词“when”和“how”。
语法图定义了一种语法,它表示自然语言句子的有效格式。例如,以下是我们这个小型英语子集中称为“疑问句”的句子的语法图。
[疑问副词] -> [动词] -> [名词] -> [?]
你可以使用这个图来检查一个词法单元序列是否构成语法上有效的疑问句。一个词法单元序列在语法上是有效的,条件是它恰好有四个词法单元,并且按顺序匹配图中的四个状态。
这个词法单元序列是否构成语法上有效的疑问句?How is Mary?
这个词法单元序列有四个词法单元。词法单元“How”是副词,匹配副词状态。词法单元“is”是动词,匹配动词状态。词法单元“Mary”匹配名词状态。词法单元“?”匹配问号状态。由于没有更多词法单元,这个词法单元序列是一个语法上有效的疑问句。
这个词法单元序列 When Mary was? 在语法上是无效的。它以副词“When”开始,这匹配语法图的第一个状态。下一个词法单元是名词“Mary”,它不匹配动词状态。这种不匹配在我们的小型英语子集中是一个语法错误。
类似地,如果Python解释器在将词法单元组合成语句时遇到错误,它会报告一个语法错误。回想一下,Python成功评估了 len("hello")(括号和引号都包含在内)。然而,当括号被省略时,就发生了语法错误。虽然这个表达式有两个有效的词法单元 len 和 "hello",但不存在一个Python语法图包含这两个按顺序排列的词法单元种类(标识符和字面量)作为一个有效的表达式。
第三步:语义分析
一个词法单元序列可能具有有效的语法,但仍然没有意义。例如,词法单元序列 When is Mary? 在我们简单的英语语法中是语法上有效的。它由副词“when”后跟动词“is”,再后跟名词“Mary”,并以问号结束。虽然这个句子语法有效,但其含义或语义是无效的。为了有用,自然语言句子和Python语句除了语法有效外,还必须在语义上有效。
这就引出了解释过程的第三步——语义分析,解释器在此步骤中为语法上有效的句子和语句赋予意义。
语义定义了句子和语句的含义。人们如何为自然语言句子赋予意义?你可以使用字典来解释语法正确句子中单个单词的含义。许多单词可以用作不同的词性,并且大多数有多个含义。因此,一个单词在句子中的含义在很大程度上取决于上下文。例如,单词“school”至少可以作为名词有两种不同的含义:“学校”或“鱼群”。它也可以作为动词使用,如“I can school my brother.”。
这种上下文依赖性意味着,没有一套简单的语义规则可以描述所有语法上有效的自然语言句子的含义。
在编程语言中,词法单元的上下文依赖性较低;一套简单的语义规则可以定义所有语法正确语句的含义。为了实现这一点,编程语言的语法和语义规则必须非常精确。这意味着必须非常小心地正确表达Python语句。例如,在Python中,你可以写 len("hello"),但不能写 len(hello)(未加引号的hello)或 "hello".len。英语的结构性较弱,你可以说“the length of a string”或“the string's length”,它们具有相同的含义。
如果在语义分析阶段没有错误,Python解释器会成功计算出一个对象作为表达式求值的结果。如果表达式是在交互式环境中求值的,那么Python解释器通常会以人类可读的形式在交互式环境中显示其结果对象。

总结
本节课中我们一起学习了Python解释器评估代码的三步过程:词法分析、语法分析和语义分析。Python使用这个过程通过计算结果对象来评估表达式。为了理解解释器如何计算这些结果对象,你必须学习更多关于Python词法单元、语法和语义的知识。在观看后续视频后,你将对这三个概念有更深入的理解。
016:词法分析详解 🧩

在本节课中,我们将要学习Python解释器执行代码的第一步:词法分析。我们将了解词法分析器如何将源代码中的字符组合成称为“词法单元”的基本单位,并详细探讨标识符、分隔符和字面量这三种词法单元。
概述
词法分析是Python解释器处理代码的三步流程(词法分析、语法分析、语义分析)中的第一步。它的核心任务是将字符序列转换为有意义的词法单元序列。
词法分析过程
回忆一下,Python解释器使用三步流程:词法分析、语法分析和语义分析。词法分析步骤是如何将字符组合成称为词法单元的基本单位的?
之前,我们评估了表达式 len(“hello”)。这个表达式包含四个词法单元:len、左括号、“hello” 和右括号。Python解释器是如何执行这个词法分析的呢?
一个词法单元由一个或多个字符构成,其中每个字符可以是字母、数字、符号、特殊字符或空白字符。空白字符包括空格、制表符或换页符。
Python词法单元的种类
回忆一下,Python有五种不同的词法单元:标识符词法单元、分隔符词法单元、字面量词法单元、运算符词法单元和关键字词法单元。
每种词法单元要么由简化的词法规则描述,要么由词法表定义。
Python从左到右将字符翻译成词法单元。
标识符词法单元
第一个词法单元是 len,一个标识符词法单元。标识符是我们在Python代码中用来引用内存中对象的名称。len 引用了一个计算序列(如字符串)长度的函数对象。
Python使用简化的词法规则来识别标识符词法单元:
规则:以字母或下划线开头,后跟零个或多个字母、下划线或数字。
标识符区分大小写,因此大写的 R2D2 与小写的 r2d2 是不同的标识符。
分隔符词法单元
第二个词法单元是左括号 (,一个分隔符词法单元。分隔符用于分隔其他词法单元,就像自然语言中使用标点符号一样。
我们使用词法表来定义Python的分隔符词法单元。每个分隔符由一个、两个或三个符号字符组成。有些分隔符,如逗号和星号等号,单独出现。其他分隔符成对出现,例如左括号和右括号,它们出现在 len(“hello”) 表达式中。
字面量词法单元
第三个词法单元是 “hello”。这是一个字面量词法单元。
字面量词法单元有几个类别,而 “hello” 属于字面量字符串类别。
字面量字符串的简化规则是:
规则:以引号开头,后跟零个或多个非引号字符,并以引号结尾。
另一个字面量类别是字面量浮点数,它表示非负有理数。字面量浮点数最简单的规则是:一个点、一个或多个数字,没有其他字符。
第三个字面量类别是字面量整数,它表示非负整数。字面量整数的简化规则是一个或多个数字。
例如,27 和 0 是字面量整数,而 4.5、0.0 和 0.3 是字面量浮点数。然而,-5 和 “2.3” 不是字面量整数或字面量浮点数,因为 -5 有减号,“2.3” 有引号。实际上,“2.3” 是一个有效的字面量字符串。您稍后会了解到,-5 实际上由两个词法单元组成:减号运算符和字面量整数 5。Python必须评估这两个词法单元才能创建一个值为负5的int对象。
最长词法单元规则
为什么Python创建单个词法单元 27 而不是创建两个词法单元 2 和 7?两种选择都满足字面量整数的词法规则。
词法分析器使用最长词法单元规则:在创建词法单元时,创建尽可能长的词法单元。
因此,在创建 2 或 27 之间选择时,词法分析器创建更长的词法单元 27。
给定字符序列 4.5,Python解释器使用最长词法单元规则创建单个浮点词法单元 4.5,而不是创建字面量整数 4 后跟字面量浮点数 .5,或者创建字面量浮点数 4. 后跟字面量整数 5。
类似地,字符序列 last_chance 将被解释为单个标识符词法单元 last_chance,而不是两个标识符词法单元 last 和 chance。
空白字符规则
除了最长词法单元规则,词法分析还有空白字符规则。
对于不在行首的空白字符:如果空白字符在字面量字符串内部,则它是字面量字符串的一部分。否则,它结束当前词法单元,并且不为它创建词法单元。
行首的空白字符具有特殊含义,我们将在未来的课程中讨论。
字面量字符串的作用
字面量字符串是我们在Python代码中用来引用表示字符序列的内存对象的词法单元。
Python使用引号字符来区分字面量字符串词法单元和标识符。词法单元 len 是标识符,而词法单元 “hello” 是字面量字符串。
在表达式 len(“hello”) 中,标识符 len 被绑定到一个计算字符串 “hello” 长度(为5)的函数对象。
使用错误的词法单元类型
使用有效但类型错误的词法单元可能导致语法错误或语义错误。
例如,如果我在这个表达式中使用字面量字符串 “len” 而不是标识符 len,会发生什么?
这个表达式包含四个有效词法单元,因此没有词法错误。Python报告一个类型错误,这是一种语义错误。错误消息表明Python期望一个函数对象作为函数调用的一部分,但遇到了一个字符串对象。
回到示例表达式
现在让我们回到表达式 len(“hello”)。这个表达式中的第四个也是最后一个Python词法单元也是一个分隔符词法单元:右括号 )。
因此,这个Python表达式由标识符 len、分隔符左括号 (、字面量字符串 “hello” 和分隔符右括号 ) 组成,共四个有效词法单元。
总结

在本视频中,我们使用词法表定义了分隔符词法单元。我们还使用简单的词法规则描述了标识符词法单元以及三种字面量词法单元类别:字面量字符串、字面量整数和字面量浮点数。
017:Python语法分析

在本节课中,我们将学习Python编程语言的基本语法结构,包括字面量表达式、标识符和函数调用表达式。我们将使用语法图来理解Python解释器如何识别有效的代码结构。
概述
Python程序由语句构成。在词法分析课程中我们了解到,Python解释器首先通过词法分析过程将代码分解为称为“词法单元”的基本单位。语法规定了每个语句的格式,而语句的格式则指定了该语句中词法单元的顺序。我们使用语法图来定义语句中包含哪些词法单元以及它们的顺序。
Python语句与表达式
Python有多种语句。表达式语句在一行中包含一个或多个表达式。在本课中,我们只考虑由单个表达式构成的表达式语句。
表达式也有很多种。我们将探讨三种:字面量、标识符和函数调用。
语法图简介
Python解释器使用规则来识别有效的语法。我们使用语法图来描述这些规则。一个语法图包含状态和转换。
以下是包含字面量、标识符和函数调用的表达式的语法图:
- 起始状态绘制为一个圆圈。
- 终止状态代表来自一组词法单元(例如所有分隔符词法单元集合中的一个分隔符词法单元)的单个词法单元。终止状态绘制为圆角矩形,并匹配一个词法单元。
- 字面量是一个终止状态,代表来自任何字面量类别(包括字符串、整数和浮点数)的一个词法单元。
- 标识符也是一个代表一个标识符词法单元的终止状态。
- 不代表来自一组词法单元的单个词法单元的状态称为非终止状态。它绘制为矩形,必须使用其自己的语法图进行展开。函数调用就是一个非终止状态。
- 具有双边框的状态称为接受状态。字面量、标识符和函数调用都是接受状态,稍后将进行解释。
- 箭头称为转换,通过连接状态形成图中的路径。
验证表达式语法
要验证表达式的语法,必须找到一条从起始状态到接受状态的路径,该路径按顺序匹配所有词法单元。接受状态是语法图中任何可用于匹配你试图匹配的词法单元序列中最后一个词法单元的状态。
示例:字面量表达式
我们之前评估过字面量字符串表达式 "hello"。这个表达式包含一个词法单元:字面量字符串 hello。
这个单一词法单元序列是否构成有效表达式?从起始状态开始,有三个转换。第一个转换指向字面量终止状态。由于 "hello" 是一个字面量词法单元,可以将其匹配到终止的字面量状态。该词法单元序列匹配具有双边框的终止字面量接受状态。因此,"hello" 是一个有效的表达式。
示例:无效的单一词法单元
是否每个Python词法单元本身都能形成一个有效的表达式?让我们尝试标识符 len,它是一个有效的词法单元。len 求值后得到一个对象,其人类可读输出是 built-in function len。
现在尝试一个右括号 ),它也是一个有效的词法单元。不,这个单一词法单元不是一个有效的表达式。
为什么?从起始状态开始,有三个转换。前两个转换指向不匹配分隔符词法单元的终止字面量和标识符状态。第三个转换指向一个非终止状态。请记住,每个状态要么是匹配单个词法单元的终止状态,要么是必须使用其自己的语法图展开而不是匹配单个词法单元的非终止状态。
以下是函数调用的简化语法图,它要么有一个参数(表达式),要么没有参数。从其起始状态开始,只有一个转换,它指向终止标识符状态。由于分隔符词法单元不是标识符词法单元,因此不匹配。由于没有更多路径可尝试,单个分隔符不是有效的Python表达式。
示例:函数调用表达式
在词法分析课程中,你看到函数调用表达式 len("hello") 求值结果为 5。这个表达式包含四个词法单元:标识符 len、分隔符左括号 (、字面量字符串 hello 和分隔符右括号 )。
如何匹配 len("hello") 到语法图?从表达式的起始状态开始。起始状态有三个转换:字面量、标识符和函数调用。由于第一个词法单元 len 是一个标识符,它不匹配到字面量终止状态的转换,因此不能使用那条路径。
第二条转换指向标识符终止状态。然而,该状态没有向外的转换,并且还有词法单元需要匹配。如果它是一个非终止状态,可以展开它以匹配更多词法单元。然而,它是一个终止状态,因此只能匹配一个词法单元而不能展开它。尝试另一条路径。
第三条转换指向函数调用非终止状态,因此必须展开它。遵循函数调用图起始状态的单一转换,将词法单元 len 匹配到终止标识符状态。由于只有一个转换离开标识符状态,尝试遵循它。下一个词法单元左括号 ( 匹配左括号状态。到目前为止一切顺利。
下一个词法单元是字符串字面量 "hello"。这个词法单元不匹配右括号状态。因此不遵循这个转换。相反,遵循指向表达式非终止状态的转换。使用表达式语法图展开这个状态。
表达式的定义是递归的,因为表达式可以是函数调用,而函数调用本身包含表达式。表达式起始状态有三个转换:一个指向字面量终止状态,一个指向标识符终止状态,一个指向函数调用非终止状态。这次,如果遵循指向字面量状态的转换,它会匹配第三个词法单元,即字面量字符串 hello。仍然有词法单元剩余,并且没有离开此状态以匹配剩余词法单元的转换。现在该怎么办?
回想一下是如何到达这里的:正在尝试匹配函数调用语法图中的表达式非终止状态。由于处于表达式图中的接受状态,已成功匹配了一个表达式,因此可以返回到函数调用图中的路径。下一个转换将右括号词法单元与右括号终止状态匹配。成功,因为已经匹配了所有四个词法单元,并且右括号是一个接受状态。因此,原始表达式 len("hello") 在语法上是有效的。
识别语法错误
记住 Eval 示例视频中,计算 len "hello"(没有括号)导致了语法错误。Python解释器是如何检测到这个语法错误的?
解释器从表达式的起始状态开始,尝试找到任何有效路径。由于第一个词法单元 len 是一个标识符,它不匹配从起始状态到字面量的转换。已经看到 len 匹配标识符状态,但是还有剩余的词法单元需要匹配,而没有转换可遵循。因此,解释器尝试匹配函数调用状态。
函数调用的语法图有一个从起始状态到标识符状态的单一转换,它匹配 len 词法单元。然而,离开标识符状态的唯一转换是到左括号状态,它不匹配 "hello" 字面量字符串词法单元。因此,在语法图中没有路径匹配词法单元序列 len 后跟 "hello"。所以这个词法单元序列不是一个有效的表达式。
让我们再尝试一个词法单元序列:len 后跟一个左括号 (。解释器从表达式的起始状态开始,尝试找到任何有效路径。如前所述,len 后跟 ( 不匹配字面量或标识符,但匹配函数调用的前两个状态。没有更多词法单元了,并且当前状态不是接受状态,因此这条路径无效。由于解释器在所有路径上都失败了,这个词法单元序列作为表达式是无效的。
注意,在这种情况下不会立即报告错误。在某些情况下,解释器会识别语法图中的当前进度并等待更多词法单元。然而,如果没有提供其他词法单元,它就是一个语法错误。
总结

在本节课中,我们一起学习了如何使用语法图来识别有效的Python字面量、标识符和函数调用表达式的格式。我们的下一步是研究Python语义。然而,在此之前,你必须学习Python对象和引用。
018:Python对象

概述
在本节课中,我们将学习Python编程语言中一个核心概念:对象。我们将了解Python解释器如何在计算机内存中创建对象来表示数据,并详细探讨每个对象所具备的三个基本属性:身份、类型和值。理解这些概念是掌握Python编程的基础。
对象的三个属性
Python解释器在内存中创建对象来表示程序中的数据。每个对象都拥有三个不可分割的属性:身份、类型和值。
身份:对象的唯一标识
对象的身份在对象创建时被确定。一旦创建,其身份就是唯一且不可变的,这意味着它不能被更改。我们可以将对象的身份理解为它在计算机内存中的位置。
我们可以通过内置的 id() 函数来获取对象的身份。例如:
id(27)
执行上述代码会返回一个代表对象身份的整数。需要注意的是,这个身份标识在不同的Python解释器会话中可能不同。
以下是Python标准库中关于 id() 函数的文档说明:
返回对象的身份。这是一个整数,保证在该对象的生命周期内是唯一且恒定的。
类型:对象的格式与解释
对象的类型决定了它在内存中的格式(或形状),以及在计算过程中如何被解释。每种类型都定义了一种内存格式,不同的格式可能需要不同大小的计算机内存。
一旦我们创建了一个特定类型的对象,其类型就是不可变的。我们不能改变它的类型,因为改变其形状会破坏内存中相邻的对象。
为了简化理解,在后续的图示中,我们将对所有类型的对象使用相同的表示方式,尽管实际上不同类型在内存中的大小和形状各不相同。
Python中有许多内置类型,本节我们重点介绍两种基本类型:
- 整数:类型名为
int,用于表示整数,支持加法和乘法等运算。 - 字符串:类型名为
str,用于表示字符序列,Python提供了计算字符串长度、连接两个字符串等操作。
我们可以通过内置的 type() 函数来获取对象的类型。
以下是Python标准库中关于 type() 函数的文档说明:
传入一个参数时,返回该对象的类型。
例如:
type(65) # 显示 <class 'int'>
type('A') # 显示 <class 'str'>
在Python中,“类型”和“类”这两个术语可以互换使用。因此,type() 函数告诉我们 65 的类型是 int,而字符串 'A' 的类型是 str。
值:对象的二进制表示
现代计算机使用二进制数字(简称“位”)来存储所有数据。每个位要么是0,要么是1。对象的值就是用于表示它的位序列。
例如,整数 65 有其对应的二进制表示。由于整数 65 和 27 的值不同,它们必须有不同的二进制表示。
既然不同的值有不同的二进制序列,为什么我们还需要类型呢?这是因为对象的类型定义了如何解释这些二进制数字。
例如,在许多编程语言中,整数 65 和字符 'A' 可能使用相同的二进制序列来表示它们的值。对象的类型决定了这两个对象将如何被解释:一个作为整数,一个作为字符串。
请看以下示例:
65 + 65 # 结果为 130 (整数加法)
'A' + 'A' # 结果为 'AA' (字符串连接)
尽管初始对象的二进制序列可能相同,但由于类型不同,运算产生的二进制值也完全不同。
Python对象的特性与图示约定
每个对象都拥有一个不可变的身份和类型,以及一个由位序列表示的值。
在示意图中,为了清晰起见,我们使用16位的序列来表示整数和字符串。但实际上,Python并不使用固定长度的二进制序列来表示这些类型。
许多其他编程语言(如C、Java)使用固定位数(如32位或64位)来表示整数,这限制了整数的最大范围。Python的 int 类型则没有这种限制,它可以表示任意大的整数。
例如,宇宙中的原子数量大约在1078到1082之间,这个巨大的数字在Python中可以直接用一个 int 类型的对象来表示。
用于对象值的内存表示的具体位序列因Python的不同实现而异。因此,Python标准库中没有内置的 value() 函数来显示对象的位表示。
幸运的是,我们编写Python程序时通常不需要了解Python类型的内存表示细节,因为Python解释器会自动在内存表示和人类可读的格式之间进行转换。
因此,在我们的示意图中,为了更易于理解,我们将使用更人性化的格式来表示对象的值,而不是使用位序列。具体来说,对于 int 类型,我们用十进制数字代替二进制数字;对于 str 类型,我们用不加引号的字符序列来表示。

总结
本节课我们一起学习了Python中对象的核心概念。我们了解到,内存中的每个对象都包含身份、类型和值这三个属性。我们认识了两种基本类型:int(整数)和 str(字符串),并将在后续课程中遇到更多类型。在讨论对象的行为(语义)之前,我们还需要探索Python程序如何引用这些对象,这将是下一节的内容。
019:字面量与标识符的语义 🐍

在本节课中,我们将学习Python编程语言中字面量和标识符的语义。理解这两个核心概念是掌握Python如何解读和执行代码的基础。
回顾Python的解释过程
Python解释器通过一系列步骤来评估一条语句。首先,词法分析将代码分解为词法单元。接着,语法分析验证这些词法单元的顺序是否符合语法规则。最后,语义分析通过评估语句来定义其含义。
我们将使用简化的语义规则来定义Python语句的含义。
表达式语句的构成
表达式语句由一个单独的表达式构成,这个表达式可以是字面量、标识符或函数调用。我们已经定义了字面量和标识符作为词法单元,也定义了由单个字面量或单个标识符构成的表达式语法。
为了定义字面量和标识符的语义,我们首先需要了解为什么在Python表达式中会使用它们。
字面量的语义
在表达式中使用人类可读的对象表示,是为了让人们能够理解这些对象。解释器必须将这些人类可读的表示转换为计算机内存中的对象,以便对它们进行计算。
在表达式中表示对象的最简单方式是使用字面量。字面量通过描述对象来代表它,包含了创建对象所需的所有信息。
字面量表达式的语义很简单:为字面量创建一个新的对象。 例如,当解释器评估字面量词法单元 300 时,它会创建一个新的整数对象。
表达式中的字面量类似于现实世界中的优惠券或代金券,可以兑换成物品。优惠券只是物品的表示,并非物品本身。一旦优惠券兑换成物品,优惠券就不再需要。同样,字面量本身不是对象,它只是在表达式中代表那个对象。一旦字面量被用来在内存中创建了对象,字面量就不再需要,只有对象是必需的。
如果解释器在另一个表达式中再次遇到字面量词法单元 300,它会创建另一个整数对象。回想一下,id() 函数为每个不同的对象返回不同的身份标识号。id() 函数可以用来判断相同字面量的再次使用是创建了新对象,还是复用了之前创建的对象。
例如,使用字面量 300 两次,会创建两个不同的对象,因为它们的 id 不同。
每个Python解释器对于复用字面量创建的对象都有自己的策略,并且这个策略取决于字面量本身。例如,这个解释器会复用所有在 -5 到 256 之间的整数对象,但对于任何其他整数,每次使用时都会创建新对象。
为了简化字面量的语义,我们忽略特定解释器是否会为多次出现的相同字面量复用同一个对象。我们定义字面量的语义为:每次评估字面量时都创建一个新对象。为字面量复用对象只影响Python代码的评估速度,通常不会影响代码结果。
标识符的语义
除了字面量,Python还使用标识符(也称为名称)在表达式中代表对象。
标识符通过引用一个对象来在表达式中代表它。我们说一个标识符绑定到一个对象,并用一个箭头来表示这种绑定关系。例如,标识符 len 代表一个函数对象。
表达式中的标识符类似于一个人的名字或角色。人们通过名字或角色来指代我们,但名字或角色只是一个标签,并不是人本身。正如人们可以多次使用同一个名字或角色来指代同一个人一样,一个标识符也可以多次引用同一个对象。
标识符是对象的持久性表示,只要在交互式环境或程序中的所有表达式正在被评估,它就存在。而字面量只是一个临时性表示,直到它所代表的对象被创建。
Python使用命名空间来允许标识符在评估期间持久存在。命名空间将出现在Python表达式中的每个名称与其绑定的对象关联起来。因此,len 标识符就与一个函数对象关联或绑定。
命名空间就像一个字典,只不过解释器不是查找一个单词并找到其定义,而是查找一个名称并找到它绑定的对象。这一步称为解引用。解引用是在Python代码评估期间,将标识符转换为其绑定的内存对象的行为。这个过程就像在网上搜索餐厅名字:你在浏览器或应用中输入餐厅的名字,然后得到它的位置,你用这个位置在现实世界中找到餐厅。
当我们打开一个Python交互式环境或运行一个Python程序时,解释器会创建一个命名空间,并在评估我们的代码之前预绑定许多标识符。例如,解释器创建一个计算序列长度的函数对象,然后将名称 len 添加到命名空间,并将这个名称绑定到该函数对象。解释器预绑定了所有内置函数的名称,例如 id 和 type 函数名。如果内置函数的名称没有被预绑定,我们将无法引用它们,也就无法使用它们。
解释器还将每个内置类型的名称预绑定到一个类型对象,并预绑定许多其他标识符。你可以使用内置函数 dir(__builtins__) 来查看所有内置标识符。你会注意到内置函数名 len 和 id,以及内置类型名 str、int 和 float。同时,也会看到一些熟悉的错误名称,如 SyntaxError 和 NameError。在我们的图示中,不会展示所有预绑定的标识符,只会展示特定Python交互式环境示例或程序中将要使用的预绑定名称。
以下是标识符表达式的语义:
- 如果标识符在命名空间中,则解引用它以获取结果对象。
- 否则,报告错误。
例如,如果我们评估一个包含标识符 len 的表达式,解释器会在命名空间中找到 len 并解引用它以获取函数对象。
让我们评估一个不在命名空间中的标识符 hello。这个表达式包含一个词法单元,即标识符 hello。解释器应用标识符表达式的语义规则:它检查标识符 hello 是否在命名空间中。由于 hello 没有被预绑定到任何内置函数、类型或其他类型的对象,它不在命名空间中。解释器报告一个语义错误。由于没有结果对象,只显示错误信息。我们将在后续课程中讨论如何绑定那些未被预绑定的标识符。
标识符绑定的特性
一个姓名标签可以被一个人佩戴,然后稍后被另一个人佩戴。例如,在驾驶公交车时,一个人可能佩戴“司机”姓名标签来代表他们的角色。当另一个人替换第一位司机时,“司机”姓名标签可以从第一个人身上取下,由第二个人使用。
标识符就像一个姓名标签,因为我们将在未来的课程中发现,一个标识符可以在不同时间绑定到不同的对象。然而,命名空间中的一个标识符不能同时绑定到两个不同的对象。类比来说,一辆公交车上不能有两个具有相同“司机”角色的人。公交车上只有一个“司机”姓名标签,一次只能贴在一个人的身上。
总结
本节课中,我们一起学习了Python编程语言中字面量和标识符的语义。我们了解到:
- 字面量是对象的直接描述,每次评估时通常(在我们的简化模型中)会创建一个新对象。
- 标识符是对象的引用或名称,它们存储在命名空间中,可以被多次使用来引用同一个对象。
- Python解释器在启动时会预绑定许多内置标识符(如
len、id),以便我们可以直接使用它们。 - 评估标识符时,解释器会在命名空间中查找并解引用它,如果找不到则会报错。

理解这些基础语义,是后续学习变量赋值、函数调用等更复杂概念的关键。
020:函数调用的语义 🐍

在本节课中,我们将学习Python编程语言中函数调用表达式的语义。我们将了解当Python解释器遇到一个函数调用时,它内部是如何一步步执行的。
在上一节中,我们介绍了字面量表达式和标识符表达式的语义。本节中,我们来看看当这些元素组合成一个函数调用时会发生什么。
函数调用的语义定义
回忆一下,函数的作用是应用于一个参数对象,以获得一个结果对象。函数调用的语义可以分解为以下四个步骤:
以下是函数调用语义的四个核心步骤:
- 计算标识符,以获取函数对象。
- 如果存在参数表达式,计算该表达式,以获取参数对象。
- 如果存在参数对象,将其传递给函数。
- 执行函数内部的代码,以获取结果对象。
语义分析示例
在语法课程中,你已经知道 len(‘Hello’) 这个表达式是一个语法有效的函数调用。它以一个标识符 len 开头,后跟左括号、一个字符串字面量 ‘Hello’ 和右括号。
在Python解释器评估这个表达式之前,标识符 len 已经被预先绑定到了内置的 len 函数对象上。
现在,让我们应用函数调用的语义规则来分析它:
- 步骤1:解释器评估标识符
len。标识符表达式的语义规则会在命名空间中查找len。由于len是预绑定的内置函数,解释器找到它并解引用,从而获得len函数对象。 - 步骤2:解释器评估表达式
‘Hello’。字面量的语义规则会创建一个新的字符串对象‘Hello’。 - 步骤3:我们将这个参数对象(字符串
‘Hello’)传递给len函数。 - 步骤4:解释器执行
len函数内部的代码(这些代码对我们不可见)。这个函数的执行过程与你正在学习的解释器过程相同。最终,该函数返回整数对象5。
这个 5 对象随后会显示在Shell中。
更多调用示例
表达式 len(‘Goodbye’) 是对同一个函数的调用,但使用了不同的参数(字面量字符串 ‘Goodbye’)。函数调用使用相同的语义计算出整数对象 7,并显示出来。
让我们尝试评估 len(27)。当解释器尝试解释这个表达式时,它会报告一个类型错误。
它是如何以及为何失败的呢?解释器成功地完成了步骤1到步骤3。错误发生在步骤4,当它尝试使用参数对象(整数 27)来执行 len 函数的代码时。len 函数无法计算一个整数中的元素数量,因为整数本身并不包含“元素”。Python Shell会报告这个错误。
总结

本节课中,我们一起学习了Python编程语言中函数调用表达式的语义。我们通过几个步骤定义了函数调用的执行过程,并在Python Shell中评估了多个表达式。Shell以人类可读的形式显示了这些函数调用的结果对象。理解这些底层语义,有助于你更清晰地预测和理解代码的行为。
021:Python程序解释

概述
在本节课中,我们将学习Python程序的基本概念,了解如何通过编写包含多个语句的程序来按特定顺序执行计算。我们将探讨程序与交互式Shell的区别,并学习如何使用print和input这两个内置函数来显示信息和获取用户输入。
程序与顺序执行
上一节我们介绍了在Python Shell中逐句执行语句。本节中我们来看看如何通过程序来顺序执行多个语句。
一个应用程序通常需要按照适合该应用的顺序计算多个对象。例如,游戏通常在开始时创建并显示欢迎界面,在游戏过程中创建并显示移动的游戏对象,在游戏结束时显示其他对象,如分数或排行榜。
如何按特定顺序创建并显示多个对象?你可以使用多个语句,Python解释器会按顺序评估这些语句。这些多个语句被称为一个程序。
Python解释器使用词法分析为整个程序创建标记。然后,它按顺序从第一条语句到最后一条语句检查每条语句的语法。最后,它使用语义分析按顺序评估每条语句。实际上,有些语句会改变这种默认的顺序评估方式,但这些将在未来的课程中介绍。
从文件运行程序
Python允许我们调用解释器来运行存储在文件中的程序。
以下是一个按顺序计算“hello”的长度、“goodbye”的长度以及“Ho”的ID的程序。这个程序包含三个表达式语句,这些语句在之前的课程中曾在Python Shell中单独评估过。
len("hello")
len("goodbye")
id("Ho")
如果运行这个程序,其输出将显示在Python Shell中。为什么表达式的结果对象没有显示出来?当这些表达式在Python Shell中评估时,结果会立即显示。解释器使用相同的语义规则来评估程序语句和输入到Python Shell中的语句。然而,在Python Shell中,表达式语句评估后,结果对象会立即在Shell中显示。当解释一个程序时,表达式语句的结果不会自动显示。
Shell显示每个评估后的表达式,以便你可以查看结果。但在大多数程序中,你并不希望显示每次计算的结果。因此,结果对象不会自动显示。
使用print函数显示输出
为了在程序评估时显式地显示一个对象,你需要使用内置函数print。该函数显示其参数对象,然后将光标移动到下一行。
不幸的是,print的文档难以理解。目前,只需知道print函数会显示其参数对象的人类可读形式。
为了有选择地显示程序中的三个对象中的两个,我将把第一个和第三个对象作为print函数调用的参数。
print(len("hello"))
len("goodbye")
print(id("Ho"))
当我运行这个程序时,每个表达式语句按顺序评估,print函数调用显式地以人类可读形式显示第一个和第三个结果对象。
print函数是一个函数,因此它必须返回一个结果对象。print函数返回一个类型为NoneType的唯一对象。任何不返回有用对象的函数都会返回此对象。
当我评估type(print("Ho"))时,Python解释器首先显示print函数调用的输出“Hello”。由于我是在Shell中评估此表达式,解释器随后显示print函数调用返回的对象的类型,即NoneType对象。
使用input函数获取用户输入
print函数在程序中显示信息。程序如何从用户那里获取信息?你可以使用内置函数input将用户的按键转换为str类型的对象。
Python标准库中关于input的条目如下:
input([prompt])
此文档中有一些新符号。prompt参数周围的方括号表示该参数是可选的。它可以包含,也可以省略。
如果存在prompt参数,它将被写入Shell,且不带尾随换行符。如果省略该参数,则不显示任何内容。然后,该函数从输入中读取一行,将其转换为字符串,去除尾随换行符,并返回该字符串对象。
这个prompt参数有什么用?如果程序需要用户输入一些信息,它应该提示用户,然后等待响应。显示提示后,input函数将光标保留在同一行。因此,用户输入位于提示的右侧。用户通过按Enter键表示输入完成。然后,该函数将用户的按键转换为一个不包含Enter键换行符的单个字符串对象。
我将运行一个程序来展示input函数的工作原理:
color = input("What is your favorite color? ")
print(color)
请注意,键入时颜色会显示在提示行上。然后,print函数调用在下一行也显示这个颜色字符串。
语法错误与程序执行
当我运行这个程序时,Python解释器在评估任何语句之前会检查整个程序的语法。
第一行中字符串参数“hellello”没有被print函数调用显示,即使第一行具有有效的语法和语义。这是因为第二行发生了语法错误。
如果你的程序包含任何语法错误,则不会评估任何语句。

总结
本节课中我们一起学习了Python程序解释的基本过程。我们了解了程序是多个语句的集合,解释器会按顺序进行词法分析、语法检查和语义评估。我们掌握了使用print函数来显式输出信息,以及使用input函数来获取用户输入。最后,我们认识到程序在执行前会进行完整的语法检查,任何语法错误都会阻止整个程序的执行。
022:程序编写 - 版本1 🎮

在本节课中,我们将学习如何将设计好的算法转化为实际的Python代码。我们将遵循“创建程序”这一步骤,包括编写代码、测试代码和调试代码。
概述:从设计到实现
上一节我们完成了游戏版本1的设计,包括版本描述、功能测试计划和算法。本节中,我们来看看如何实现这个解决方案。编写代码是一个精炼的过程,它比算法描述更加精确。
从算法到代码
我们可以用蛋糕算法来类比。仅凭“制作面糊”这个算法步骤无法烤出蛋糕,我需要知道具体的配料、每种配料的分量以及如何混合它们。同样,“烘烤蛋糕”的步骤需要具体的温度和时间。通过添加细节,我们可以从算法创建出一个食谱。这个食谱就是算法的实现,正如代码是软件算法的实现。
我们需要一个将算法转化为代码的过程。
以下是“创建程序”的三个子任务:
- 编写代码:将你的算法翻译成编程语言的过程。算法的每一步都将成为某种编程语言中的一个或多个顺序语句。在本课程中,我们使用Python。
- 测试代码:通过运行代码来确定出现了哪些错误。当你编写Python程序时,你会犯很多错误。不要气馁,犯错是学习编程非常重要的一部分。
- 调试代码:发现究竟是代码的哪一部分导致了错误,并决定如何修复它的过程。学习调试与学习编写代码同等重要。
测试和调试不是同一个过程。测试是运行你的代码以确定程序中存在哪些错误。调试是发现哪些程序语句导致了错误,并分析如何修复这些语句。
一旦知道如何修复错误,你就必须编辑你的代码。这可能涉及更改现有语句或添加新语句。自然地,当你编写新语句时,你将需要再次测试并可能调试它们。因此,你需要多次重复编写、测试、调试的过程。我们在游戏创建过程图中添加了一个循环箭头图标来表示这一点。


使用集成开发环境
在本课程中,我们使用集成开发环境来编写代码。我们将使用Wing IDE的免费版本。IDE用于编写、运行和调试代码。
使用IDE编写代码类似于使用Microsoft Word或Google Docs等文本编辑器来撰写文章。
在开始编码之前,我们先看看Wing IDE的面板。要开始一个新程序,我可以选择“文件”->“新建”,或单击任务栏上的蓝色“新建页面”按钮。一个名为“Untitled-1.py”的新程序会出现在任务栏正下方的面板中。该面板左侧现在有行号,这将帮助你确定语句中错误的位置。
程序下方是一个带有多个标签页的面板:搜索、堆栈数据和Python Shell。“搜索”标签用于在程序中查找和替换单词。我们将在后续的“黑客”版本中使用相关调试功能时讨论“堆栈数据”标签。“Python Shell”标签你应该在编程语言课程中已经很熟悉了。你的IDE中可能会看到其他标签配置。
开始编码
编程的挑战之一在于确保你的代码易于理解和有良好的文档记录,这不仅是为了方便他人使用,也是为了你自己。我们常常认为在几周或几个月后,我们仍然能准确记得代码是如何工作的。不幸的是,在很长一段时间后回到一个文档记录不佳的项目,会导致挫败感和延误。
良好的文档包括描述性的标识符名称和表明代码意图的注释。你可以在行首使用井号来创建注释行。注释是供人类使用的,Python解释器会忽略它们。
我将首先添加注释,包括此游戏版本的标题和游戏的简短摘要。
现在,让我们将算法翻译成代码。
我们算法的第一步是“显示标题”。让我们添加一个名为“显示标题”的注释。实际上,我会将每个算法步骤作为一个块注释添加,该注释描述你将编写来翻译该步骤的程序语句块。
我从主面板开始,包含四个步骤:显示标题、显示密码列表、提示猜测和结束游戏。然后,我在“结束游戏”注释下添加“显示失败结果”和“提示结束”的注释,因为它们是“结束游戏”的一部分。然而,我在注释中添加了额外的空格,以表明它们是“结束游戏”的子步骤。这使得我们很容易看到所有必须翻译的算法步骤。
正如我们之前所说,每个算法步骤将被翻译成一个或多个Python语句的块。
现在让我们将第一个算法步骤“显示标题”翻译成Python代码。我们在编程语言视频中看到,Python使用print函数来显示字符串。

参数字符串应该是什么?根据版本描述,你必须输出“调试模式”、“剩余一次尝试”和一个空行。你可以通过重新运行程序来找到确切的字符串。

我在“显示标题”注释下方键入了print函数调用,然后使用任务栏上的绿色箭头按钮运行这个程序。标题显示在Shell中。
我只翻译了第一个算法步骤后就运行程序,是为了让错误更容易被发现。如果测试发现错误,你需要使用调试来定位导致错误的语句。如果你只在做了少量添加或更改后进行测试,就更容易识别错误发生的语句。
例如,我将从“显示标题”的最后一个print函数调用中删除右括号,但我不测试程序。相反,我将翻译更多的算法步骤。我将把“显示密码列表”的翻译留作一个活动,转而翻译“提示猜测”。
一个提示会显示一些文本,并将用户的按键捕获为一个字符串。我们可以将我们的“提示”步骤翻译成对Python input函数的调用。相同的算法步骤在其他语言中将被翻译成其他函数调用,例如Java中的Readline和C语言中的gets。
我将编写input函数调用,其参数字符串以“输入密码”开头,以大于号结尾。我再次使用绿色的“运行”按钮运行程序。Python Shell报告了一个语法错误,在input函数调用行,尽管我们知道错误实际上在print函数调用行。报告的行号并不总是错误实际发生的位置。
我们每翻译完一个算法步骤就运行程序,是为了让调试更简单。在下一个活动中,你将修复这个错误,并将算法的其余部分翻译成代码。
你必须将测试计划应用于你完成的代码,以确保你的代码功能正确。确保你执行了测试计划中的所有测试操作,从“启动程序”到“输入错误答案并按回车键退出”。确保所有问题的答案都是“是”。如果任何问题的答案是“否”,那么你必须应用更多次的编写、测试和调试,直到你的程序正确为止。
总结

本节课中,我们一起学习了如何将设计好的算法转化为可运行的Python代码。我们了解了“创建程序”的三个核心子任务:编写代码、测试代码和调试代码。我们介绍了使用集成开发环境进行编程,并强调了良好文档和逐步测试的重要性。记住,编程是一个迭代的过程,犯错和调试是学习过程中不可或缺的部分。
023:反思过程 🧠

在本节课中,我们将要学习应用开发流程中的一个关键环节——反思。许多初学者在编写出一个可以运行的程序后,可能会认为任务已经完成。然而,从设计解决方案到实现代码,这并非流程的终点。通过本节课,你将理解为何以及如何进行反思,以提升你的问题解决能力。
反思的重要性
上一节我们介绍了从设计到实现的基本流程。本节中我们来看看为何在“创建版本”任务中,“反思”是一个至关重要的子任务。
反思是游戏(乃至任何应用)创作过程中的重要步骤。学习一项新技能通常最初会聚焦于特定的、具体的情境。为了能举一反三,必须将这项技能泛化,使其能够应用于新的、不同的情境。
什么是解决方案泛化?
解决方案泛化,指的是将一个针对特定问题的解决方案,替换为能够解决一组相关问题(包括原特定问题)的通用方案。这是一种被称为抽象的新问题解决技术。
例如,在之前的编程课程中讨论过,我可以根据“蛋糕算法”创建一份巧克力蛋糕食谱,从而烘焙出我的第一个蛋糕。但在成功烘焙一个蛋糕后,我未必能制作另一种蛋糕。
我希望利用在创建巧克力蛋糕食谱时获得的知识,来创建适用于任何种类蛋糕(如芝士蛋糕或咖啡蛋糕)的食谱。😊 这就是解决方案泛化的一个例子:一份通用的蛋糕食谱将取代针对不同蛋糕的独立食谱。
以下是其核心思想:
- 算法步骤(如制作面糊、烘烤蛋糕)保持不变。
- 需要识别出哪些细节对应于参数(在切换食谱时会改变),哪些细节保持不变。
- 例如,打开烤箱这个动作不会改变,但烤箱温度这个参数会改变。
在编程中如何泛化?
你必须学会通过改变所使用的函数、字面量和其他对象,来泛化你编写的代码语句,以解决不同的问题。
以下是几个例子:
- 在编码活动中,你打印了特定的密码字符串。然而,更一般地说,
print函数可用于打印任何字符串。 - 更进一步,你可以通过使用不同的函数调用(例如
input)来执行不同的操作。
用代码来描述这个核心概念:
# 特定解决方案:打印固定字符串
print("MySecretPassword123")
# 泛化方案:打印任何作为参数传入的字符串
def print_any_message(message):
print(message)
# 调用泛化后的函数
print_any_message("Hello, World!")
print_any_message("Another message")
反思让你能够将学到的技能,应用到最初学习它的狭窄语境之外。
总结

本节课中我们一起学习了应用开发流程中的“反思”阶段。我们了解到,仅仅实现一个能运行的程序并不够,关键在于通过解决方案泛化和抽象的思维,将针对特定问题的代码转化为可复用的模式与知识。这使你从一个特定问题的解决者,成长为能够应对一类问题的程序员。记住,反思是将经验转化为能力的关键一步。
024:代码审查与反思活动

在本节中,我们将学习程序开发中“反思”阶段的两个核心子任务:代码审查与反思活动。我们将通过调试器追踪代码执行,并了解如何评估和改进代码质量。
代码审查
上一节我们编写了“黑客”游戏的第一版程序。为了更深入地理解程序的工作原理,现在我们需要进行代码审查。这意味着你需要按程序实际运行的顺序,逐一检查每一条语句。你的目标是理解这些语句如何协同工作,以实现游戏功能。
我们可以使用调试器来追踪Python解释器运行程序的过程。以下是使用调试器的基本步骤:
以下是启动和进行代码追踪的步骤:
- 点击“开始调试”按钮(也称为“步入”按钮,图标是一个箭头指向方框)。调试器会高亮显示程序的第一条语句,表示该语句将被首先执行。
- 要执行当前高亮的语句,点击“单步跳过”按钮(图标是一个箭头越过方框)。例如,当执行一个
print函数调用时,其参数字符串会显示在Python shell中。 - 每次点击“单步跳过”按钮,解释器就会按顺序执行下一条Python语句。
- 你可以随时点击红色的“停止”按钮来结束调试,返回程序。
让我们看看当解释器执行一个 input 函数调用时会发生什么。请注意,下一条语句不会高亮,并且除了“停止调试”按钮外,所有按钮都会变暗。这是因为 input 函数尚未执行完毕,它正在等待用户输入一个字符串。
在Python shell中输入密码(例如“gas”)后,解释器完成了对 input 函数调用的求值,并会高亮显示 input 函数调用之后的语句。你可以继续点击“单步跳过”按钮,逐条语句地完成对整个程序的追踪。
这种追踪展示了Python解释器默认的顺序执行过程。在未来的课程中,我们将介绍改变这种默认顺序执行的语句,并使用追踪来理解非顺序的执行流程。
为了确保你理解如何追踪程序,你应该亲自尝试完整地追踪一遍你的程序。如果在追踪过程中,你不小心点击了“步入”按钮而不是“单步跳过”按钮,调试器可能会高亮显示一个不在你程序中的代码行。例如,在 input 函数调用行点击“步入”按钮就会发生这种情况。此时,你将看到属于该库函数实现部分的代码。要返回到你的程序代码,请点击“步出”按钮(图标是一个箭头从方框中指出来)。
代码质量与软件质量测试
现在你已经理解了代码的功能,接下来让我们讨论代码的质量。代码质量通过软件质量测试来保证。这些测试与你已经创建和使用过的功能测试结合使用。
- 功能测试 用于证明代码按预期工作。
- 软件质量测试 则用于确保你做出了良好的编码选择并遵循了最佳编程实践。
你不需要创建自己的软件质量测试,我们会为每个游戏版本提供具体的测试要求。
对于“黑客”游戏的第一版,你只需要使用一个软件质量测试来检查你的注释。你必须测试两种注释:程序注释和块注释。
- 程序注释 是对程序功能的总结,必须放在程序开头。
- 块注释 是单行注释,必须出现在构成一个逻辑任务的每组语句上方,并对后续代码进行简短说明。
以下是我们为“黑客”游戏第一版提供的解决方案示例。程序顶部是游戏标题和简短摘要,这满足了程序注释的软件质量测试要求。其余注释是块注释,它们是在编程演示过程中作为算法步骤添加的。按照目前的写法,它们满足了块注释的软件质量测试要求,每个块注释都位于构成一个逻辑任务的语句块开头。
随着课程引入新的编码概念,我们将添加更多的软件质量测试。请将这些不断演进的软件质量测试视为一系列风格指南,来指导你编写代码。
反思活动
反思的第二个子任务是进行反思活动。每次反思活动都将加深你对标记、语法、语义、对象类型的理解,并教你如何将已编写的代码推广到解决新问题。
现在请完成“黑客”游戏第一版的反思活动。

本节课中,我们一起学习了如何通过调试器进行代码审查以理解程序执行流程,并了解了使用软件质量测试(特别是针对注释的测试)来评估和提高代码质量。最后,我们介绍了反思活动的作用,它是巩固和扩展编程知识的重要环节。
025:识别解决方案问题

在本节课中,我们将学习反思过程的第三个也是最后一个子任务:识别解决方案问题。我们将发现解决方案中可以改进的部分。
上一节我们回顾了代码,本节中我们来看看如何识别解决方案中的潜在问题。
识别解决方案问题
识别解决方案问题意味着发现我们当前解决方案中可以改进或优化的部分。
例如,黑客游戏版本1的一个解决方案问题是,我们为每个显示部分(标题、密码列表和结果)使用了多个print函数调用。这是显示多行文本最直接的方式。
以下是显示多行文本的另一种方法:将所有要打印的文本组合成一个单独的字符串,并在每行之间添加换行符。
换行符是一种特殊字符,可以在字符串字面量中使用反斜杠后跟字母N来表示,即\n。当print函数遇到换行符时,它会开始新的一行。
让我们运行下面这个程序:
print("DEBUG MODE\n1 ATTEMPT(S) LEFT\n\n1234\n4321\n9999\n\nEnter the correct password:")
这个程序的行为与使用多个print函数调用的程序完全相同。
方案对比
这两种解决方案都是有效的,但各有优缺点。
以下是两种方案的对比:
- 多个
print调用方案:程序更长,因此需要更多输入。它重复了标识符print多次。 - 单个
print调用方案:程序更短。然而,它更难阅读。此外,在未来的版本中,我们需要从这个字符串中操作单个密码(例如,以图形方式绘制单个密码、检查玩家猜测的密码是否正确、以及用符号字符包裹每个密码)。如果使用单个字符串,操作单个密码将变得困难。
当存在多种可能的解决方案时,你应该比较每种方案的优缺点。在未来的版本中,我们将在引入解决这些问题的游戏新版本时,继续识别解决方案问题。
总结
本节课中我们一起学习了反思的第三个子任务——识别解决方案问题。你已经完成了反思的前两个子任务(反思活动和代码回顾),并初步了解了识别解决方案问题这个子任务。这个子任务将在下一个模块开始时完成,因为它将推动黑客游戏版本2的开发。

恭喜你,你已经完成了黑客游戏的第一个版本。
026:识别解决方案问题与添加图形

在本节课中,我们将学习如何识别代码中的“解决方案问题”,并开始为我们的“黑客”游戏添加图形界面。我们将分析第一个版本游戏的不足,并了解如何通过使用外部图形库来改进它。
识别解决方案问题
每次完成一个游戏版本后,你都需要决定下一个版本要添加或改进什么。这个过程被称为识别解决方案问题。解决方案问题是指你寻求的最终解决方案与你已编写的设计和代码之间的差距。识别这些问题是系统化改进代码和功能的关键。
“黑客”游戏的第一个版本并不令人兴奋,与你需要完成的完整游戏只有少数相似之处。它仅以黑白文本格式显示标题和密码。虽然它允许你输入猜测,但它只在Python shell中显示未经修饰的密码,而不是使用自己的窗口。此外,你只有一次猜测密码的机会,并且无论你选择什么,游戏都会报告你失败了。
我们可以识别出第一个版本至少存在五个不同的解决方案问题。
以下是这些问题及其对应的改进计划:
-
代码质量问题:在之前的课程中,我们讨论了多次调用
print函数的缺点。我们需要移除这些重复的语句,因为重复会使程序变得不必要的冗长。我们将在“黑客”版本4中通过引入一种称为重复控制结构的语言机制来解决这个问题。 -
逻辑判断问题:当你输入正确密码时,游戏却显示你失败了。为了解决这个问题,你需要一种机制来比较密码和猜测。然后,你需要选择向玩家报告成功还是失败。你将在“黑客”版本3中学习如何做到这一点。
-
猜测次数限制:你只被允许进行一次猜测。在未来的版本中,你必须被允许最多进行四次猜测。为此,你必须能够在算法和代码中一致地重复步骤和语句。你将在“黑客”版本5中学习如何做到这一点。
-
密码装饰问题:你的密码不包含最终版本中的任何符号字符装饰。为了进行这项改进,你必须能够操作密码字符串,并以随机方式添加符号字符。你将在“黑客”版本7中学习如何做到这一点。
-
图形界面问题:你的游戏是基于文本的,而不是基于图形的。你必须学习创建和打开一个窗口,并在窗口中绘制图形对象。


为版本2添加图形
上一节我们介绍了“黑客”游戏版本1存在的五个主要问题。本节中,我们来看看如何为第二个版本进行改进。
对于“黑客”的第二个版本,你将进行的唯一解决方案改进是用图形替换文本以及显示玩家的猜测。其他改进需要你学习和实践几个新的设计组件和Python语句。
要使用图形窗口,你必须掌握一些新的库函数。使用图形窗口的游戏不能使用 print 函数在该窗口中显示文本。print 只在终端窗口或Python shell中显示。它不允许你指定要显示的文本的位置、颜色或字体。
对于“黑客”游戏,你需要在屏幕的左上角、右上角、右下角和中心显示绿色文本。为此,你必须使用一个图形库来显示文本。
到目前为止,你使用的函数是Python标准库中包含的内置函数。不幸的是,标准库不支持图形。你不能用它来打开窗口、绘制圆形或打印不同颜色的文本。因此,我们将使用一个外部库。
一个外部库或第三方库是由Python社区提供的代码编译集合。它包含增强标准库的函数和其他代码。使用库是使用模板的一个例子。使用模板是一种抽象技术,它利用先前创造的解决方案并将其调整以解决新问题。
回顾“黑客”版本1的代码。如果你深入 input 函数,你会看到实现它的底层代码。这个 input 函数的代码是由其他人编写的,其他人可以在不重写的情况下使用它。试想一下,如果你必须在实现“黑客”游戏之前先实现Python的所有内置函数,那么编写本课程中的游戏会有多困难。这将花费很长时间,以至于你永远没有时间编写使用这些函数的游戏代码。不要浪费时间重新发明轮子。使用模板可以基于他人的工作更快地创建解决方案。
在本课程中,你将使用我们创建的一个名为 UAGame 的库。这是一个简单的库,可以让你创建一个窗口并在其中绘制字符串。它基于一个流行的Python图形库,称为 Pygame。我们使用这个更简单的图形库来实现“黑客”的所有版本。你将在本课程的第二个游戏“Pol the dots”中使用Pygame本身。
让我们为“黑客”游戏添加图形。
总结

本节课中,我们一起学习了如何识别代码中的“解决方案问题”,这是迭代开发游戏的关键步骤。我们分析了“黑客”游戏第一个版本的五个主要不足,并明确了后续版本的改进方向。接着,我们重点介绍了为第二个版本添加图形界面的必要性,解释了为什么不能使用内置的 print 函数,以及如何通过引入外部图形库(如UAGame和Pygame)来创建窗口和绘制文本。使用外部库是高效编程的重要实践,它能让我们站在巨人的肩膀上,避免重复劳动,专注于实现游戏的核心逻辑。
027:观察黑客入侵游戏第二版 👀

在本节课程中,我们将观察“黑客入侵”游戏的第二个版本。通过观察,我们将了解这个版本与第一版在视觉和功能上的区别,并学习如何分析一个正在开发中的游戏。
上一节我们介绍了游戏开发的基本流程,本节中我们来看看“黑客入侵”游戏第二版的实际运行效果。
游戏立即开始。可以看到,黑客入侵第二版的外观与第一版有很大不同。一个新的图形窗口打开,标题为“Hacking”,背景为黑色。文本显示为绿色,但文本内容与黑客入侵第一版相同。
以下是观察游戏时需要问自己的几个关键问题:
- 游戏看起来是什么样子的?
- 屏幕上的元素何时出现?
- 玩家可以执行哪些操作?
我将执行与玩第一版时相同的操作。当提示输入密码时,我将输入正确的密码 hunting。不幸的是,与第一版一样,屏幕上出现了失败提示信息。不过别担心,我们将在下一个版本中修复这个问题。
至少,屏幕会清空并显示猜测次数,这与最终版本一致。失败信息正确地显示在屏幕中央。实际上,黑客入侵第二版的功能与第一版几乎相同,但其图形和风格已经与最终版本一致。
在接下来的活动中,你将亲自试玩黑客入侵第二版,以确保在描述它之前能完全理解其行为。

本节课中,我们一起观察并分析了“黑客入侵”游戏第二版。我们了解到,此版本在图形界面上进行了重大更新,采用了与最终版一致的黑底绿字风格,但核心游戏逻辑(如密码验证)仍与第一版相同。通过对比观察,我们明确了当前版本的状态,为后续的描述和修改工作做好了准备。
028:描述黑客游戏版本2

概述
在本节课中,我们将学习如何为图形界面的“黑客游戏”版本2创建描述。我们将基于版本1的文本描述进行修改,重点关注从文本界面到图形窗口的转变所带来的变化。
从观察到描述
上一节我们观察并试玩了黑客游戏版本2。本节中,我们将正式描述它。
观察、试玩、描述这个过程,随着课程的深入,应该成为你的自动反应。这是一种经验性分解,它能巩固你对主题的理解,这也是为什么这三个子任务构成了“理解版本”任务。
与版本1一样,你将使用描述构建器来为版本2创建描述。然而,由于游戏的第二个版本是在第一个版本的基础上构建的,因此你不是从零开始,而是通过修改版本1的描述来创建版本2的描述。
考虑到这一点,创建版本2描述的第一个任务是,删除版本1描述中所有不再相关的内容。
我将打开版本1的描述,以便找出应该删除的对象、属性和动作。
识别并删除过时内容
版本1的描述指出,标题的位置是游戏输出的顶行。这在版本2中看似仍然成立,因为标题显示在窗口顶部。然而,在图形窗口中,位置必须以不同的方式指定。因此,必须从描述中删除此属性。
由于玩家的猜测现在作为失败结果显示,失败结果的属性也必须更改。猜测是失败结果内容的一部分。这意味着必须移除内容属性,以便用版本2的适当内容属性替换。
让我们关闭版本1的描述,打开版本2。
构建版本2的描述
描述构建器的组装面板中已经加载了版本1描述的概念。但是,不适当的概念已被移除。
版本2的功能与版本1相同。改变的两件事是游戏的外观(使用图形窗口而非基于文本的Shell)以及必须显示玩家的猜测。
在版本1中,程序启动后,所有内容都显示在Shell中。在这个版本中,窗口在显示任何内容之前出现。
在左侧面板中,描述了一个新对象:游戏打开一个窗口。这是我们游戏的新第一个对象,因此我将把这个对象拖到组装面板中“游戏显示标题”的上方。
如果我把它放在标题下方,反馈会提示我添加对象的顺序错误。
与之前的描述活动一样,我可以添加新对象,也可以添加属性。让我们看看标题的属性。你将在后续活动中添加窗口的属性。
描述图形对象的属性
回想一下,每个显示对象都有五个属性:内容、位置、大小、颜色和时序。
当我们从文本转向图形时,每个对象的内容不会改变。因此,内容属性的描述将保持不变。
在制作图形游戏时,你必须描述窗口中所有对象的位置,因为你需要在程序中指定这些位置。在基于文本的游戏中,每个print调用的参数都显示在连续的行上,无需指定位置。
在我们的描述中,我们将使用通用术语来指示窗口位置,例如“左上角”、“窗口中心”或“直接在某些其他文本下方”。
由于标题在左上角,让我们将此属性添加到标题中。
下一个属性是文本的大小,即字体大小。在我们的描述中,让我们使用小、中、大。在黑客游戏中,所有文本大小相同,所以我将使用小字体大小。我将把这个属性添加到标题中。
黑客游戏中的所有文本都是绿色在黑底上,所以我将把“它是绿色在黑底上”添加到标题中。
最后一个属性是关于时序的。在版本1中,我们处理了对象显示的时间顺序。版本2引入了一个新的时间属性:在显示每一行后,有0.3秒的暂停。我将添加此属性以完成标题的描述。
后续任务
在下一个活动中,你将添加所有的窗口属性,以及其余对象的位置、大小和颜色属性。你还将在适当的时间顺序中添加两个新动作:清除窗口和关闭窗口。
此外,还需要添加一个属性来显示玩家的猜测。

总结
本节课中,我们一起学习了如何为图形化的黑客游戏版本2构建描述。我们基于版本1的文本描述,删除了不再适用的属性(如基于行的位置),并开始为图形界面添加新的属性,如窗口位置、字体大小、颜色和显示时序。关键点在于理解从文本到图形界面时,描述对象的方式需要从抽象的行概念转变为具体的窗口坐标和视觉属性。
Python编程与电子游戏问题解决:04_04_01:回归测试与删除过时测试

在本节中,我们将学习如何更新功能测试计划,重点介绍回归测试的重要性以及如何删除新版本中不再相关的过时测试。
完成了第二个版本的游戏描述后,接下来需要更新功能测试计划。更新测试计划能帮助我们追踪游戏功能增加过程中的进展。
然而,当我们添加新功能时,必须重新测试前一版本中所有在当前版本中仍然相关的功能。这个过程称为回归测试。进行回归测试是因为在添加新代码时,很容易无意中破坏现有的代码。
例如,假设我正在开发一个手机应用,功能包括编辑图片、排序图片以及上传图片到社交媒体。在第一个版本中,我创建了编辑图片的功能,并在第一版的测试计划中测试了这个功能。在版本2中,我添加了上传图片到社交媒体的功能,但没有重新测试图片编辑器。到了版本3,我添加了排序功能,并且只测试了这个新功能。不幸的是,当用户使用我的应用时,编辑器无法工作。由于我在创建版本2和版本3后没有重新测试编辑器,因此不知道是哪个版本的代码破坏了编辑器功能。为了定位和修复这个错误,我需要检查版本2和版本3的代码,而不仅仅是单个版本的代码。最终我发现,是版本2的上传功能破坏了编辑器。如果我在版本2的测试计划中重新测试了版本1的编辑器功能,定位和修复这个错误就会容易得多。具体来说,我为了便于上传而将图片转换成了新的图像格式,但没有修改编辑器来处理这种格式。虽然我实际上没有改动编辑器代码,但添加上传代码的行为导致了编辑器代码出错。
为了支持回归测试,前一版本的功能测试已经加载到功能测试计划构建器中。当你修改测试计划以适应新版本功能时,首先要做的就是删除任何不必要的测试。这类似于从描述中删除不必要的概念。
对于任何要求你从描述、测试计划或算法中永久删除元素的版本,你将完成一个多项选择活动,以确定需要删除哪些元素。对于功能测试计划,这包括不再相关的测试,以及需要修改的测试。由于测试计划中的所有问题都被分组到单个问题块中,任何没有包含完全正确问题的问题块都被视为不正确,需要删除。
例如,当前针对标题的测试不包含任何与文本颜色或窗口位置相关的问题。然而,其他测试,例如“游戏是否显示标题”和“是否指示调试模式”,仍然是正确且需要的。由于你不能删除单个问题、添加缺失的问题或修改问题块中的问题,因此需要删除整个问题块,使其不会显示在构建器的问题面板中。这与从构建器的组装面板中拖出概念不同——拖出概念是将其移回左侧面板,而不是删除它。当你在多项选择活动中删除任何概念时,它甚至不会出现在相应的构建器中。它与当前游戏版本无关,因此在组装过程中你将无法使用它。
在接下来的两个活动中,你将删除不相关的问题块,并使用构建器创建第二个测试计划。请记住,你必须测试窗口的新特性,包括它如何打开、清空和关闭,以及窗口中文本的新位置和视觉效果。
总结

本节课我们一起学习了回归测试的概念及其重要性,它确保在添加新功能时,现有功能不会意外失效。我们还了解了如何通过删除不再相关或需要修改的测试问题块来更新功能测试计划,为创建新版本的测试做好准备。
030:创建黑客游戏算法(版本2)

在本节课中,我们将学习如何为黑客游戏的第二个版本创建算法。我们将基于版本1的算法,通过添加图形化功能来构建更复杂的版本2算法。
上一节我们介绍了版本1的基本算法。本节中,我们来看看如何通过添加图形库功能来升级到版本2。
算法构建基础
版本1的算法已经存在于构建器中。由于版本2的算法包含了版本1的所有步骤,因此你无需决定删除哪些步骤。你将通过添加步骤来创建你的版本2算法。
回想一下,功能选择用于将一个复杂问题转化为一系列称为“版本”的子问题。功能选择是为每个版本确定合理功能集的过程。
在版本1中,你为最小的功能集创建了算法。在版本2中,你将添加图形化功能。你必须使用图形库来代替内置函数显示文本。
图形库功能
大多数图形库都具备一些通用功能。以下是常见的图形库能力:
- 打开和关闭窗口。
- 在窗口中显示对象。
- 清除窗口内容。
你将把类似上述的步骤添加到你的算法中。诸如文本颜色等细节包含在你的描述中,但它们过于具体,不应包含在算法步骤里。然而,当你编写代码时,会需要这些细节。
游戏结束步骤的扩展
你的“游戏结束”算法步骤包含了前一版本中的“显示失败结果”和“提示结束”步骤。你必须为“游戏结束”添加其他步骤。同时,你必须扩展“显示失败结果”步骤,使其使用自己的面板,以便为显示的每一行添加一个步骤。
这是必要的,因为必须显示猜测内容,并且结果行必须居中显示。
文本对齐的复杂性
居中显示文本行比显示左对齐的连续行(例如标题和密码列表)需要更复杂的计算。

请使用你已完成的黑客游戏版本2描述和测试计划,从版本1的算法出发,创建出版本2的算法。
本节课中,我们一起学习了如何从版本1的算法升级到版本2。核心在于通过功能选择添加图形库功能,并特别注意了扩展“游戏结束”逻辑以及处理文本居中显示所带来的算法复杂性。记住,算法描述应保持高层次,而将具体实现细节留给编码阶段。
031:Python赋值语句

在本节课中,我们将要学习Python编程中的一个核心概念:赋值语句。我们将了解它的语法、语义以及如何在程序中有效地使用它来存储和引用数据。
概述
赋值语句是Python中用于将标识符(如变量名)与一个对象(如数字、字符串等)绑定起来的关键工具。它允许我们在程序的一个地方计算一个值,并在后续的多个地方通过一个简单的名字来引用它,从而使代码更加清晰和高效。
赋值语句的语法与语义
上一节我们介绍了程序的基本结构,本节中我们来看看一种新的语句——赋值语句。
赋值语句用于计算一个表达式,并将一个标识符绑定到结果对象上。
以下是赋值语句最简单的语法形式:
target = expression
- target(目标):目前我们只讨论最简单的目标——一个标识符(即变量名)。
=(赋值符号):这是一个分隔符标记,由单个等号组成。在Python中,它不表示“相等”,通常读作“赋值为”或“得到”,而不是“等于”。- expression(表达式):任何可以计算出结果的Python表达式。
当一个赋值语句的目标是标识符时,其语义(执行规则)如下:
- 计算表达式以得到一个结果对象。
- 如果该标识符不在当前命名空间中,则将其添加到命名空间。
- 将该标识符绑定到这个结果对象。
程序示例分析
让我们通过一个具体的程序来理解赋值语句是如何工作的。
考虑以下程序代码:
favorite = input("Enter your favorite color: ")
second_favorite = input("Enter your second favorite color: ")
print("You picked:")
print(favorite)
print("and")
print(second_favorite)
当Python解释器执行这个程序时,其过程如下:
- 执行第一条赋值语句:计算表达式
input("Enter your favorite color: ")。假设用户输入“purple”,该函数调用返回字符串对象"purple"。解释器将标识符favorite添加到命名空间,并将其绑定到字符串"purple"。 - 执行第二条赋值语句:类似地,假设用户输入“red”,标识符
second_favorite被绑定到字符串"red"。 - 执行后续的表达式语句:当执行
print(favorite)时,解释器会解引用标识符favorite,即在其绑定的命名空间中查找它,并获取它指向的对象——字符串"purple",然后将其打印出来。print(second_favorite)的过程与之类似。
通过使用赋值语句,我们可以在程序开头获取数据,并在程序的任何后续部分方便地使用这些数据。
标识符的重新绑定
一个已经被绑定的标识符可以被重新绑定到另一个不同的对象。
如果对一个已绑定的标识符再次应用赋值语义规则:
- 该标识符已存在于命名空间中,因此不会被再次添加。
- 该标识符会被重新绑定到表达式计算出的新对象上,之前的绑定关系则被覆盖。
考虑以下程序:
L = id
print(L("Hello"))
- 程序开始时,命名空间包含预绑定的标识符,如
len(L的预绑定对象)、id和print。 - 执行第一条赋值语句:
L = id。表达式id被解引用,得到内置的id函数对象。标识符L被重新绑定到这个id函数对象,覆盖了原来指向len函数的绑定。 - 执行第二条表达式语句:
print(L("Hello"))。解释器解引用L,现在得到的是id函数。id("Hello")被调用,返回字符串"Hello"的身份标识(一个内存地址相关的数字),然后由print函数输出。
这个例子表明,任何标识符都可以被绑定到任何对象。但是,你绝对不应该在代码中重新绑定那些在程序运行前就已预定义的标识符(如 print, len, id 等),这会导致意想不到的行为并使代码难以理解。
检查命名空间
你可以使用内置的 dir() 函数来检查当前的命名空间。
- 在交互式环境中输入
dir(__builtins__),可以查看所有预绑定的内置对象名称。 - 输入
dir()(不带参数),可以查看当前作用域中已定义的所有名称,包括你自定义的变量。
例如,在绑定 favorite 和重新绑定 L 之后调用 dir(),结果列表中将会包含 favorite 和 L,表明它们已被绑定。
总结

本节课中我们一起学习了Python赋值语句的核心知识。我们了解到,赋值语句(变量名 = 表达式)用于将标识符与一个对象绑定,使得该对象可以在程序后续部分通过标识符方便地引用。我们分析了其执行语义,并通过示例看到了如何绑定以及重新绑定标识符。最后,我们知道了可以使用 dir() 函数来探查命名空间的状态。正确使用赋值语句是组织和管理程序数据的基础。
032:Python二元表达式与运算符标记

在本节课中,我们将学习一种新的表达式——二元表达式,以及一种新的标记——运算符标记。我们将把二元表达式添加到我们的表达式语法图中。
概述
我们已经介绍了Python五种标记中的三种:分隔符、字面量和标识符。在本节中,我们将介绍运算符标记。
运算符标记
考虑以下程序,其第三条语句包含一个二元表达式和一个名为“加号”的运算符标记。
first_value = input("Enter first value: ")
second_value = input("Enter second value: ")
sum = first_value + second_value
print("First value:", first_value)
print("Second value:", second_value)
print("Sum:", sum)
这个程序提示用户输入两个值,并将标识符 first_value 和 second_value 绑定到 input 函数返回的两个字符串对象。
运行此程序,输入 27 和 65,结果是 2765。发生了什么?
除了第三条语句,其他所有语句都包含你已经学过的标记和表达式。所有这些语句都具有有效的词法和语法。那么第三条语句呢?让我们对第三条语句应用词法分析、语法分析和语义分析。
词法分析
第三条语句包含以下标记:标识符 sum、赋值分隔符 = 和标识符 first_value。下一个字符是加号 +,而我们目前描述的任何标记类型都不以加号字符开头。加号字符是一种新的标记类型,称为运算符标记。
运算符用于从一个或多个操作数计算出一个结果对象。由于只有19个运算符标记,我们将其列在词法表中,而不是使用词法规则。
以下是Python中的运算符标记表:
+加法-减法*乘法/除法//整除%取模**幂运算==等于!=不等于<小于>大于<=小于等于>=大于等于=赋值(注意:这是分隔符,不是运算符)&按位与|按位或^按位异或~按位取反<<左移>>右移
一些数学运算符在Python中用相同的运算符标记表示,一些用不同的标记表示,还有一些则没有对应的运算符标记。
- 加号
+和减号-在Python和数学中相同。 - 乘法 在数学中使用乘号或点表示,在Python中使用星号
*。 - 小于等于 在数学中是
≤,在Python中用两个符号<和=表示。 - 幂运算 在数学中用上标表示,在Python中使用双星号
**运算符标记。 - 等于 在数学中是
=,在Python中,相等运算符是双等号==。请注意,单独一个等号=实际上是赋值分隔符标记,而不是运算符标记。在应该使用相等运算符时误用赋值分隔符是一个常见的编程错误。 - 阶乘 在数学中使用感叹号
!,但Python中没有阶乘运算符标记。
最长标记规则
你看到运算符标记表中同时包含 ** 和 *。如果词法分析器遇到 **,它是创建一个单独的运算符标记 **,还是两个运算符标记 * 和 *?我们可以将最长标记规则应用于运算符标记,就像我们在词法分析课程中将其应用于整数字面量和标识符一样。因此,答案是一个长标记 **。
语法分析:二元表达式
第三条语句的最后一个标记是标识符 second_value。第三条语句是一个赋值语句,其中的表达式是一种新的表达式——二元表达式。
以下是二元表达式的语法图:
二元表达式 -> 表达式 二元运算符 表达式
这个语法图包含一个新的非终结符 二元运算符。目前,我们将使用以下语法图来表示非终结符“二元运算符”:
二元运算符 -> 运算符标记(除 `~` 外)
任何运算符标记(除了 ~)都可以用作二元运算符。在未来的课程中,我们将把二元运算符的概念泛化,以包含一些其他非运算符标记。
第三条语句中的表达式是一个二元表达式,因为 first_value 匹配第一个表达式(作为标识符),加号标记 + 匹配二元运算符状态,second_value 匹配第二个表达式(作为标识符)。
语义分析
二元表达式用于将运算符函数应用于两个操作数对象。应用的函数取决于二元运算符和两个操作数的类型。
二元表达式的语义如下:
- 计算左表达式以获取第一个操作数对象。
- 计算右表达式以获取第二个操作数对象。
- 根据二元运算符和两个操作数的类型确定运算符函数。
- 应用二元运算符的函数。
在未来的课程中,你会发现一些二元运算符函数可能会忽略第二个表达式。
在将语义应用于语句3之前,我们先快速应用语义到语句1和2。
程序开始时,存在一个包含所有预绑定标识符的命名空间。在我们的例子中,程序中使用的唯一预绑定标识符是内置函数名 input 和 print。语句1和2是赋值语句。
- 语句1 提示用户输入一个字符串。在本例中,
input函数返回字符串对象"27"。解释器将标识符first_value添加到命名空间,并将其绑定到字符串对象"27"。 - 语句2 将标识符
second_value添加到命名空间,并将其绑定到字符串对象"65"。
现在,让我们对语句3应用赋值语句语义。表达式是一个二元表达式,因此我将对其应用二元表达式语义。
- 计算左表达式以获取第一个操作数对象。由于左表达式是一个标识符表达式,我们使用标识符的语义。由于标识符
first_value在命名空间中,解释器解引用它以获取字符串对象"27"。 - 解释器必须计算右表达式以获取第二个操作数对象。由于右表达式是一个标识符表达式,标识符
second_value被解引用以获取字符串对象"65"。 - 二元运算符是加号运算符标记
+,并且两个操作数的类型都是字符串,因此解释器找到为字符串实现加法的运算符函数。这个函数是字符串连接。 - 当字符串连接函数应用于操作数——字符串对象
"27"和字符串对象"65"时,会创建结果字符串对象"2765"。
为了完成赋值语句的语义,标识符 sum 被添加到命名空间并绑定到这个字符串对象。
语句4到9显示程序输出。
数值求和
要计算两个数字的数值和,我可以使用内置函数 int,它从字符串对象创建一个整数对象。以下是 int 函数的Python文档:
int(x) -> integer
这是修改后的程序:
first_value = input("Enter first value: ")
second_value = input("Enter second value: ")
sum = int(first_value) + int(second_value)
print("First value:", first_value)
print("Second value:", second_value)
print("Sum:", sum)
在这个程序中,语句3中的二元表达式包含两个函数调用表达式。第一个调用计算为整数对象 27,第二个调用计算为整数对象 65。
当应用二元表达式语义时,解释器找到为整数实现加法的运算符函数。这个函数是数值加法。解释器应用这个数值加法函数以获得整数对象 92,然后将标识符 sum 绑定到这个对象。语句4到9随后显示两个整数操作数及其和。
总结

在本节课中,我们一起学习了运算符标记和二元表达式。我们了解了运算符标记的种类,学习了如何通过语法图识别二元表达式,并深入探讨了二元表达式的语义分析过程,特别是如何根据操作数的类型选择正确的运算符函数(如字符串连接或数值加法)。理解这些概念是掌握Python表达式求值的基础。
Python编程与电子游戏:第4章第8节:import语句与关键字token

在本节课中,我们将学习一种新的语句——import语句,以及最后一种词法单元——关键字。import语句允许我们在程序中使用来自库模块的函数、类型和其他对象。掌握它是扩展Python程序功能的关键。
导入语句的作用
到目前为止,你只在代码中使用过内置函数。然而,内置函数的数量是有限的。如果你想为代码添加内置函数不支持的功能,就需要使用模块。
模块是一个拥有自己语句和命名空间的Python程序。它提供了你可以在自己程序中使用的函数、类型和其他对象。Python的标准库包含许多模块。
例如,如果你想在程序中暂停一段时间,没有内置函数可以实现这一点。但是,标准库中time模块的sleep函数可以做到。
# 尝试直接调用sleep函数会出错
sleep(2) # 错误!NameError: name 'sleep' is not defined
出错的原因是sleep这个名字不在程序的命名空间中。它不像len或int那样是预绑定的。我们可以使用内置的dir()函数来检查当前命名空间。
为了使用模块中的对象,我们需要使用一种新的语句:import。
理解import语句
import语句将标识符从模块导入到你程序的命名空间中,这样你就可以使用这些标识符所绑定的对象。
import语句有几种不同的语法形式,本节课介绍from...import...形式。
要理解这个语法图,我们必须先介绍关键字词法单元。
关键字词法单元
Python中有33个关键字。from和import都是关键字词法单元。
关键字看起来像标识符,但它是保留字,不能用作标识符。尝试将关键字用作标识符会导致语法错误。
例如,如果尝试用赋值语句绑定关键字from,就会发生语法错误。
from = 10 # 错误!SyntaxError: invalid syntax
回想一下我们之前学过的标识词法规则:以字母或下划线开头,后跟零个或多个字母、下划线或数字。现在,我们必须修改这个标识符词法规则,以排除关键字。
语法分析示例
让我们对下面的import语句进行语法分析:
from time import sleep, asctime
- 第一个词法单元是关键字
from,它与import语句语法图中的from终结状态匹配。 - 下一个词法单元是标识符
time,下一个状态是非终结状态module。在本课中,我们使用module最简单的版本,即单个标识符。标识符time匹配module语法图中的标识符状态。 - import语句中的下一个词法单元必须是关键字
import。 - 在这个关键字状态之后,是匹配一个或多个由逗号分隔的标识符的状态。注意,最后一个标识符后面没有逗号。
- 由于语句中没有更多词法单元,并且当前状态标识符是一个接受状态,因此这是一个语法上有效的import语句。
import语句的语义
在展示import语句的语义之前,让我们先运行一个使用它的程序:
from time import sleep, asctime
now = asctime()
print("Current time:", now)
sleep(2)
print("Goodbye!")
这个程序会显示当前时间,暂停两秒,然后显示“Goodbye!”。sleep和asctime函数不是Python内置函数,它们包含在一个名为time的库模块中。
以下是import语句的语义步骤,我们将它们应用到程序中的import语句上:
- 查找模块:解释器使用模块名
time来找到time模块。 - 评估模块:解释器通过评估其语句来评估
time模块。我们看不到time的语句,但解释器评估它们的方式与评估我们程序的方式相同:一次一个语句。评估time的语句会在内存中创建对象,将一些名称(如sleep和asctime)添加到time的命名空间,并将这些名称绑定到一些创建的对象上。 - 解引用名称:解释器解引用
time命名空间中的sleep和asctime名称,以获取它们绑定的两个对象。 - 检查程序命名空间:解释器在程序的命名空间中查找,没有找到
sleep或asctime,因此它添加这两个名称。 - 绑定对象:解释器将程序命名空间中的名称
sleep和asctime绑定到步骤3中使用time命名空间找到的对象上。
程序执行过程
当程序评估第二条语句(now = asctime())时,它使用赋值语句的语义:
- 首先评估函数调用表达式,在程序的命名空间中查找
asctime,绑定并解引用它以获取asctime函数对象。 - 评估这个函数对象,返回一个表示当前日期和时间的字符串对象。
- 最后,它将标识符
now添加到程序的命名空间,并将其绑定到这个字符串对象。
第三条语句是一个包含print函数调用的表达式语句。解释器评估这个函数调用,在程序命名空间中查找print名称,绑定并解引用它以获取print函数,然后评估参数表达式,连接字面字符串和标识符now绑定的字符串对象,并将这个字符串对象显示为当前时间和日期。
第四条语句是一个包含函数调用的表达式语句。解释器评估这个函数调用,在程序命名空间中查找sleep名称,找到sleep名称,解引用它以获取sleep函数,并用参数对象2评估它。这个函数使程序暂停两秒,并返回一个类型为NoneType的对象(被忽略)。
第五条语句显示告别字符串,程序结束。
使用多个import语句
你可以使用多个import语句从多个模块导入名称。这些语句可以按任何顺序放置,只要在使用一个名称之前导入它即可。
from math import sqrt
from random import randint
from time import sleep
# 现在可以使用sqrt, randint, sleep
总结
在本节课中,我们一起学习了:
- import语句:一种允许我们从外部库模块导入函数、类型等对象到当前程序命名空间的语句。
- 关键字:Python中最后一种词法单元,是保留的标识符,具有特殊语法含义,不能用作普通变量名。
from module import name语句的语法结构和执行语义。- 通过导入
time模块并使用sleep、asctime函数的实例,理解了模块如何扩展程序功能。

理解import语句是构建复杂、功能丰富Python程序的基础。在接下来的学习中,你会遇到更多强大的标准库和第三方模块。
034:多参数函数调用 🧩

在本节课中,我们将学习如何扩展函数调用的语法和语义,使其支持多个参数,而不仅仅是零个或一个参数。
概述
之前我们已经学习了如何调用不带参数或带一个参数的函数。然而,在实际编程中,许多函数需要接收多个输入值才能完成其任务。本节将详细介绍Python中多参数函数调用的语法规则和内部执行过程。
函数参数数量的多样性
不同的函数支持不同数量的参数。例如:
len()函数恰好需要一个参数。- 标准库
random模块中的randint()函数需要两个参数,用于指定随机数的范围。
许多Python函数可以接受可变数量的参数。print() 函数就是一个典型的例子。
以下是 print() 函数不同调用方式的示例:
print() # 无参数调用,输出一个空行
print("Hello") # 单参数调用
print(27, "Hello", 4) # 多参数调用,输出:27 Hello 4
当 print() 函数被传入多个参数时,它会从左到右依次求值每个参数表达式,并将结果对象在同一行输出,对象之间用单个空格分隔。
多参数函数调用的语法
为了支持多个参数,我们需要扩展函数调用的语法定义。以下是支持一个或多个参数的简化语法图。
上一节我们介绍了基础函数调用的语法,本节中我们来看看如何定义参数列表。
参数列表 (argument_list) 的语法规则是:它由一个表达式开始,后面可以跟随零个或多个由逗号分隔的表达式。
让我们用这个语法规则来分析表达式 print(27, "hello", 4) 是否有效。
- 标识符
print匹配函数名。 - 左括号
(匹配左括号状态。 - 进入
argument_list状态:- 字面量
27是一个有效的表达式。 - 逗号
,匹配分隔符状态。 - 字符串
"hello"是一个有效的表达式。 - 逗号
,再次匹配。 - 字面量
4是一个有效的表达式。
- 字面量
- 右括号
)匹配右括号状态,表示参数列表结束。 - 所有令牌消耗完毕,且当前处于函数调用的接受状态,因此该表达式语法有效。
多参数函数调用的语义
既然语法有效,解释器就可以进行语义分析,即执行这个函数调用。
之前单参数函数调用的语义规则需要被修改以支持多个参数。新的语义步骤如下:
步骤1:对函数名进行求值,获取函数对象。
步骤2:如果存在参数列表,则对其求值,得到一个参数对象列表;否则,创建一个空列表。
步骤3:将这个参数对象列表传递给函数对象。
步骤4:对函数对象进行求值,得到结果对象。
其中,参数列表的语义是:从左到右依次求值列表中的每个表达式,并将结果对象按顺序添加到一个列表中。
让我们将这些规则应用到 print(27, "hello", 4) 上。
- 程序启动时,名称
print已绑定到内置的print函数对象。 - 存在参数列表
(27, "hello", 4)。解释器对其求值:- 求值
27,得到整数对象27。 - 求值
"hello",得到字符串对象"hello"。 - 求值
4,得到整数对象4。 - 将这三个对象按顺序放入参数对象列表:
[27, "hello", 4]。
- 求值
- 将这个包含三个对象的列表传递给
print函数对象。 - 执行
print函数。其功能是接收一个对象列表作为输入,将这些对象以人类可读的形式打印出来(默认用空格分隔)。因此,屏幕输出:27 hello 4。
print函数返回一个NoneType类型的对象,通常被忽略。
总结

本节课中,我们一起学习了多参数函数调用的核心概念。我们扩展了函数调用的语法,使其能够定义由逗号分隔的参数列表;同时更新了其语义,明确了解释器会从左到右求值所有参数,并将结果对象列表传递给函数执行。理解这一点是编写和调用复杂函数的基础。
035:方法调用与属性引用 🐍

在本节课中,我们将学习Python中的两个核心概念:方法调用和属性引用。我们将了解如何通过对象访问其内部的功能(方法)和数据(属性),并深入探讨方法调用的独特语法和运行原理。
概述
在之前的课程中,我们学习了函数调用的基本语法和语义。本节我们将对其进行泛化,使其包含一种特殊的函数调用——方法调用。同时,我们将引入一种新的表达式类型——属性引用,它是访问对象内部命名空间的关键。
属性引用:访问对象的命名空间
每个程序都有一个命名空间,用于将标识符绑定到对象。此外,大多数对象也拥有自己的命名空间。对象命名空间中的名称被称为属性。
为了访问对象命名空间中的对象,我们使用一种名为属性引用的新表达式。
例如,每个字符串对象都有一个名为 lower 的属性,该属性绑定到一个方法对象。
在本课程中,我们假设对象的命名空间在对象创建时一同创建。实际上,Python不同实现中命名空间的管理方式可能有所不同。
以下是包含属性引用的表达式的新语法图:
表达式 . 属性
而这是属性引用本身的语法图。属性必须是一个标识符标记。
从这些语法图中可以看出,R2D2.lower 是一个语法上有效的属性引用,其表达式是字面字符串 "R2D2",属性是标识符 lower。
属性引用的语义
属性引用的执行遵循以下两个步骤:
- 计算表达式以获取一个对象。
- 如果该属性存在于对象的命名空间中,则返回其绑定的对象。否则,报告一个属性错误。
当我们计算属性引用 R2D2.lower 时,解释器会应用上述语义:
- 首先,表达式是字面字符串
"R2D2"。解释器计算这个字面量,创建字符串对象"R2D2"。由于这是一个字符串对象,其命名空间包含标识符lower,该标识符绑定到一个方法对象。 - 其次,解释器在
"R2D2"的命名空间中找到标识符lower,并解引用以获取该字符串对象的lower方法对象。
因此,每个字符串都有一个名为 lower 的属性,它绑定到一个方法对象。
方法对象与方法调用
那么,什么是方法对象?方法又有什么用呢?
一个方法是一个对特定类型对象进行操作的函数。我们说这个方法与该类型关联。方法所应用的对象被称为特殊参数。
方法应用的语法与普通函数调用不同。特殊对象不像普通函数调用那样作为参数列表中的一个求值表达式。相反,方法调用使用属性引用的语法,其中特殊对象是点号 . 前的一个求值表达式,而方法名则放在点号之后。
例如,lower 方法对一个字符串对象进行操作,并返回一个将所有大写字母转换为小写字母的相似字符串对象。
如果 lower 是一个普通函数调用,函数调用看起来会是这样:
lower("R2D2")
但 lower 不是一个普通函数调用,因此这种语法无效。Python中没有名为 lower 的内置函数。相反,存在一个与字符串类型关联的内置方法,其名称就是 lower。你可以通过查看类型文档来查阅内置方法的文档。
有些方法除了特殊参数外,还需要普通的参数对象。例如,字符串方法 find 还需要一个普通的字符串参数。
find 方法返回字符串中第一次出现子串的起始索引。
"Edmonton".find("on")
在这个例子中,子串 "on" 在字符串 "Edmonton" 中的第一次出现位于索引 3。在Python中,字符串索引从0开始编号。所以0是E,1是D,2是M,3是O。这个O就是第一次出现"on"的起始位置。
泛化函数调用语法以包含方法调用
上一节我们介绍了属性引用和方法的基本概念,本节中我们来看看如何将函数调用的语法进行泛化,使其能够包含方法调用。
以下是你已经见过的函数调用的当前语法:
函数名 ( 参数列表 )
为了将此语法泛化以包含方法调用,我们将语法图中的 函数名 状态替换为 表达式 状态。这样,任何表达式(包括属性引用)都可以在函数调用中用作函数名,而不仅仅是使用单个标识符。
我将使用这些语法图来验证方法调用表达式的语法,然后再定义方法调用的语义。
你已经看到,前三个标记 R2D2、. 和 lower 匹配属性引用(这是一个表达式)。最后两个标记匹配函数调用图中的左右括号。因此,这个标记序列是一个有效的函数调用。
由于 R2D2.lower 求值得到一个方法对象,而不是一个普通的函数对象,所以这个表达式是一个方法调用。
泛化函数调用语义以包含方法调用
现在,我将函数调用的语义泛化,以包含方法调用的语义。以下是更新后的步骤:
- 计算表达式以获取一个对象。
- 计算参数列表中的每个表达式,创建一个参数对象列表。
- 如果表达式对象是一个方法对象,则将其特殊参数添加到参数对象列表的开头。
- 将参数对象列表传递给函数对象。
- 计算函数代码。
让我们看看解释器如何将函数调用语义应用于 "Edmonton".find("on"):
- 首先,它计算表达式,该表达式由属性引用
"Edmonton".find组成。- 应用属性引用的语义:属性引用的第一个语义步骤是计算其表达式,即创建字符串对象
"Edmonton"。由于这是一个字符串对象,解释器也会创建其命名空间。 - 属性引用的第二步是在
"Edmonton"的命名空间中定位名为find的属性,并解引用以获取方法对象find。
- 应用属性引用的语义:属性引用的第一个语义步骤是计算其表达式,即创建字符串对象
- 属性引用求值完成后,解释器返回到函数调用的语义规则并执行步骤2:如果存在参数列表,则计算它。参数列表包含一个单一的字面字符串表达式
"on"。因此,创建一个包含字符串对象"on"的单元素参数对象列表。 - 在步骤3中,解释器评估表达式并识别出
find是一个方法对象,因此它将特殊参数"Edmonton"添加到参数对象列表的开头。此时参数对象列表变为["Edmonton", "on"]。 - 在步骤4中,解释器将参数对象列表传递给函数对象,即方法对象
find。 - 在步骤5中,解释器计算函数代码,即
find的方法代码。该方法计算并返回整数对象3,作为字符串"Edmonton"中第一个匹配字符串"on"的索引。
总结
在本节课中,我们一起学习了:
- 属性引用:一种用于访问对象命名空间中绑定的表达式,语法为
对象.属性。 - 方法:与特定类型关联的函数,用于操作该类型的对象。
- 方法调用:函数调用的一种特殊形式。其语法使用属性引用来指定方法(如
对象.方法名()),其语义会在执行时自动将点号前的对象作为第一个参数(特殊参数)传递给方法。

通过泛化函数调用的语法和语义,我们统一了对普通函数和方法的理解,这是掌握Python面向对象编程基础的重要一步。
036:编程黑客游戏版本2

概述
在本节课中,我们将学习如何将黑客游戏从命令行版本升级到图形窗口版本。我们将使用UA游戏库创建一个窗口,并在其中显示文本、获取用户输入,并处理图形界面的坐标系统。
从版本1到版本2
上一节我们完成了黑客游戏的命令行版本。本节中,我们来看看如何将其转换为一个带有图形窗口的版本。
开始新版本项目时,最佳实践是保存前一个版本的副本,并在新副本上进行修改。现在,我将复制hacking version 1,并将新副本重命名为hacking2.py,这将成为版本2的代码。
添加算法步骤注释
首先,为版本2算法的每个步骤添加描述性的块注释。部分注释在版本1中已存在,我将添加新的注释。我们使用注释开头额外的空格来表示它是更大步骤中的一个子步骤。
创建游戏窗口
我将查阅UA游戏库文档,看看可以使用哪些窗口方法。由于要创建一个窗口对象,我将选择一个合适的标识符来绑定我的窗口对象,例如window。
现在,我将使用UA游戏库中的window函数来创建一个窗口对象。文档指出,我必须传递三个参数:一个标题字符串、一个宽度整数和一个高度整数。我将从版本2的描述中获取这些值。
标题应为"hacking",窗口的宽高比应为6:5。这是在代码中需要更精确处理的描述不明确之处。我将使用一个600x500像素的窗口。
我不需要从程序开始到结束逐行添加代码。我可以将相关的功能放在一起添加。例如,为了在游戏结束时关闭窗口,我将在Close window注释下,在程序末尾添加名为window.close的方法。
导入与初步运行
现在我将运行我的程序,看看会发生什么。首先,我需要添加一个import语句,以便可以使用UA游戏库中的window类型。这使我能够访问与window相关的所有方法。
一个窗口打开了,但所有print函数调用仍然显示在Shell中。在我输入密码并按回车退出后,窗口关闭了。print和input函数在窗口中不起作用。因此,我将删除所有原始程序行。
在窗口中绘制文本
window类型有一个名为draw_string的方法。由于此方法在窗口中显示字符串,我将使用它来显示第一个标题行。
draw_string接受三个参数:一个要显示的字符串,以及两个指示其在窗口中位置的左上角坐标的整数。
计算机图形使用坐标系来定义窗口中的点。你可能从数学中熟悉坐标。将窗口视为一个图形,X轴是水平的,Y轴是垂直的。
在数学中,我们将原点(0,0)放在窗口的左下角;X从左向右增加,Y从下向上增加。在图形学中,原点位于窗口的左上角。X仍然随着向右移动而增加,但Y随着向下移动而增加。距离左上角向下10像素的点表示为(0,10)。一个600x500像素窗口的右下角点位于(600,500)。
为了在左上角显示标题的第一行,我将传入字面字符串"debug mode"、字面整数0和另一个字面整数0。
添加暂停以查看内容
我再次运行程序。窗口内容显示得太快,很难看清。由于我知道程序应该在显示每一行后暂停,我现在将添加一个暂停,以便在窗口关闭前更好地查看其内容。
为了添加暂停,我将使用导入课程中介绍的time模块中的sleep函数。我将在窗口导入语句旁边添加from time import sleep语句。回想一下,sleep需要秒数作为参数。
我将在draw_string调用之后添加一个参数为字面浮点数0.3的sleep调用。
现在再次运行程序。这样更容易看清。你可以通过增加秒数来保持窗口打开更长时间。
更新窗口与文本属性
我期望在左上角看到"debug mode"显示,但它不在那里。为了看到字符串,我需要在window.draw_string之后添加另一个方法调用window.update。
添加后再次运行程序。大多数图形库在调用更新方法之前实际上不会更改窗口。如果你想同时显示多个字符串,可以使用多次draw_string调用,然后调用一次update。
字符串格式看起来不太对。我将添加几行代码来更改文本的字体和颜色,然后运行它。现在文本与版本2视频中的文本匹配了。
我使用了几个方法来更改一些窗口属性:字体名称、字体大小、字体颜色和窗口背景颜色。这些窗口属性用于所有显示的文本,它们有默认值。默认字体是操作系统字体,默认的字体和背景颜色是白色和黑色。
由于默认背景颜色是黑色,我们实际上不需要设置它,但我们还是会设置。
显示标题的第二行
现在,我准备显示标题的第二行。我可以再次使用相同的三行代码,但我需要更改draw_string的参数。首先,我将替换字符串。
我应该使用什么字面整数来在第一行下方显示第二行?由于第二行应显示在窗口的左边缘,X坐标应为0。我需要计算它的Y坐标。如果我使用0,第二行将显示在第一行的顶部。我希望第二行显示在第一行下方,所以我的Y坐标应该是第一行的高度。
计算文本行高
如何计算一行文本的高度?数像素会很繁琐,而且可能数错。如果有一个函数能为我做这件事就容易多了。好消息是,UA游戏库有一个名为get_font_height的方法,它返回游戏字体的高度(以像素为单位)。
我将此方法调用的结果绑定到标识符string_height,并在draw_string中使用这个标识符。
标题以一个空行结束,所以我可以再次使用这三行代码并编辑它们。Y坐标应该是什么?在绘制第一行之后,我在绘制第二行之前将Y坐标更新为string_height。
我可以再次这样做以获得第三行的Y坐标。只需再次加上string_height。对于其他行,我们将需要重复此操作。下一个Y坐标将是三个string_height,依此类推。
然而,让我们以不同的方式思考Y坐标。每个新的Y坐标是前一个Y坐标加上string_height。如果我这样想,那么我可以使用line_y = line_y + string_height来更新Y坐标。


这行代码通过将其重新绑定到一个新值来更新Y坐标。在运行程序之前,我需要将标识符line_y初始化为0。否则,第一次调用draw_string时,在解引用line_y时会导致名称错误。
现在运行程序。这个新的Y坐标更新有效,但很难知道空行是否真的被显示了。
获取玩家猜测
接下来我想做的是获取玩家的猜测。我们已经知道内置的input函数在窗口中不起作用。相反,UA游戏库有一个名为input_string的方法。
此方法的行为类似于input函数,尽管它有一个在窗口中显示的提示参数,它还需要像draw_string一样的显示位置,使用2个整数参数作为位置。

我将使用提示字符串"enter Pass"、X坐标0和Y坐标line_y。由于我需要将玩家的猜测作为结果的一部分显示,我将把此对象绑定到一个名为guess的标识符。

调用input_string后不需要调用window.update方法,因为提示必须在输入字符串之前出现。
居中显示结果文本
结果文本在窗口中居中,因此我必须计算每个结果行的位置。在窗口左边缘显示文本是行不通的。让我们从X坐标开始。
我希望每个结果行在X方向上居中。让我们看一张图。如果我希望一行居中,我希望该行两侧的空间相等。
为了计算相等空间的大小,我从窗口宽度开始,减去行的宽度。这得到了两侧空间的总大小。我将使用get_width方法获取窗口宽度,使用get_string_width方法计算猜测字符串的宽度。
为了获得每个单独空间的大小,我将这个距离除以2。我将使用整数除法运算符//来除两个整数以获得整数结果。这个除法会丢弃任何余数。我将这个单独的空间距离用作该结果行的X坐标。
为了计算Y坐标,我使用相同的原理。窗口顶部边缘与第一个结果行顶部之间的空间应与窗口底部边缘与最后一个结果行底部之间的空间相同。
我通过将结果行的总数(包括空行)乘以string_height来计算结果行块的高度。现在,我将使用get_height方法从窗口高度中减去文本块的高度。最后,我将总空间除以二。
我可以使用刚刚计算的两个坐标作为参数来绘制玩家的猜测。当我再次运行程序时,猜测显示在它应该在的位置,但结果文本显示时没有擦除之前的游戏文本。当你完成游戏编程时,你将修复这个问题。
显示一个密码
在你接手之前,我要做的最后一件事是显示一个密码。我已经知道需要哪些代码行:一行用于显示字符串,一行用于更新窗口,一行用于暂停,一行用于递增line_y。我将复制这些行,更新第一个密码字符串,然后再次运行我的程序。
密码显示在它应该在的位置。你将在下一个活动中编写程序的其余部分。记住要经常运行和测试。你犯的每一个错误都是一个学习的机会。
总结

本节课中,我们一起学习了如何将黑客游戏升级到图形界面版本。我们创建了一个窗口,学习了图形坐标系统,使用draw_string显示文本,使用input_string获取用户输入,并计算了文本的居中位置。这些是构建图形用户界面游戏的基础步骤。
037:代码审查 - 黑客游戏第二版


在本节课中,我们将回顾黑客游戏第二版的代码。我们将使用调试器来观察赋值语句如何将标识符绑定到对象,以及如何通过标识符获取对象。同时,我们还将学习新的软件质量测试标准,特别是关于标识符命名的规范。
上一节我们介绍了调试器的基本功能。本节中,我们来看看一个包含赋值语句的更复杂示例。
以下是黑客游戏第二版的解决方案代码。它以一个描述版本信息的程序注释开始。
# 黑客游戏 - 版本 2
from uagame import Window
from time import sleep


代码的第一条语句是导入。我将按下“步入”按钮开始调试过程。第一条高亮显示的语句是 from uagame import Window。与之前的回顾一样,从现在开始,我将使用“步过”按钮,而不是“步入”按钮。没有必要进入那些未在程序中明确写出的代码。
我按下“步过”按钮,下一条高亮显示的语句是另一个导入语句 from time import sleep。我再次步过这个导入语句,接下来高亮显示的是一条窗口赋值语句。

我步过这条语句,解释器将执行这个赋值语句。

一个黑客游戏的窗口出现了,因为赋值语句创建了一个新的窗口对象。
代码下方是栈数据面板。这个面板有两个加粗的单词:Locals 和 Globals,每个词下面都有一系列以下划线开头和结尾的单词。该面板的 Locals 部分充当了程序的命名空间。在我们跟踪程序时,每一条赋值语句都会影响一个标识符。遗憾的是,内置函数和导入的名称虽然被添加到了命名空间,但不会显示在这个 Locals 部分。
在 Locals 部分的底部是新的标识符 window。如果我点击它旁边的箭头,可以看到窗口对象的命名空间。窗口有许多属性,包括背景颜色、字体名称和字体颜色。当前高亮的语句是一个设置字体名称的方法调用。当我步过它时,可以看到字体属性从空字符串变成了“courier new”。
代码中的下三条语句绑定了窗口的字体大小、字体颜色和背景属性。当我步过这些语句时,可以看到字体颜色属性从白色变为绿色,而背景颜色属性保持不变。这个属性没有改变,因为它实际上被重新绑定到了这个黑色的字符串对象。
我将收起 window 标识符,因为其他程序语句不会再影响它的属性。下一条语句是另一个赋值语句。步过这个赋值语句会将 line_y 添加到 Locals 命名空间,并将其绑定到 0。再下一条语句添加了 string_height 并将其绑定到 19。字符串高度是 19 像素,尽管字体大小是 18 点。字符串高度通常比点大小大几个像素。
我将步过接下来的三条语句,因为它们不会更新 Locals 命名空间。再下一条语句是一个重新绑定 line_y 到不同对象的赋值语句。你可以看到 line_y 绑定为 0,string_height 绑定为 19。当我步过这条语句时,可以看到 line_y 被重新绑定为 19,即 0 和 19 的和。我将步过接下来的几条语句,直到遇到下一个重新绑定 line_y 的赋值语句。步过这个赋值语句后,line_y 被重新绑定为 38,即旧的绑定值 19 加上 string_height(也是 19)的结果。
希望你现在对如何使用调试器跟踪标识符绑定有了更好的理解。
跟踪完成后,我们来谈谈代码的质量。在黑客游戏的第一版中,我们有一个软件质量测试:你的程序必须在开头有描述性注释,并且必须在每个逻辑代码块的开头有行内注释。对于版本 2,我们增加了第二个软件质量测试,它涉及标识符的命名。
以下是关于标识符的两个测试:
首先,你使用的名称必须描述它们所绑定的对象。例如,使用标识符 s 来描述字符串高度对象就不够清晰。
其次,测试评估标识符的格式。目前,你所有的标识符都应该是小写,单词之间用下划线连接。


例如,string_height 是可接受的,但 stringHeight 则不可接受。这是一个 Python 风格约定,它能让任何查看你代码的人更容易理解你写的内容。一些导入的类型名称(如 Window)可能包含大写字母,但我们将在后面讨论这个约定。
本节课中,我们回顾了如何使用赋值语句将标识符绑定到对象。恭喜你,黑客游戏版本 2 已完成。
038:黑客游戏V2解决方案问题与V3改进方向 🎮

在本节课中,我们将探讨黑客游戏第二个版本(V2)的解决方案改进点,这些改进将被纳入第三个版本(V3)。我们将重点关注如何通过引入“选择”控制结构,使游戏能够根据玩家输入的密码产生不同的结果路径。
概述:从V2到V3的演进
上一节我们介绍了黑客游戏V2的基本框架。现在,我们来看看V2版本存在的主要问题及其解决方案。
从视觉上看,V2游戏看起来已经很像最终版本,但它缺少大量关键功能。例如,无论玩家输入什么密码,游戏都只有一条固定的路径,最终总是显示失败的结果信息。这显然不符合一个密码破解游戏的核心玩法。
我们需要一种方法,让游戏能够根据玩家输入的密码来遵循不同的路径。游戏应该根据玩家输入的密码来选择相应的结果。将玩家的猜测与正确密码进行比较,这本身就是一个计算过程。我们的算法和程序必须能够根据计算结果来执行选择。
引入控制结构:选择
提供这种能力的语言结构被称为控制结构。一个控制结构决定了算法步骤或程序语句何时执行,甚至包括它是否被执行。黑客游戏的V3版本将专注于一种特定的控制结构:选择。
将“选择”引入你的解决方案,将导致你的游戏描述、测试计划、算法和代码都发生变化。通过观察和试玩V3版本,你可以识别出基于输入答案正确与否而产生的相应游戏结果。
更新游戏描述:引入选择属性
在本版本中,你将修改游戏描述,使其反映不同的游戏结果。这将涉及一种新的属性类型:选择属性。
选择属性是一种临时属性,用于响应游戏中的选择,通常以“如果”开头。例如,既然游戏会通过比较正确和错误的猜测来产生特定结果,你可以使用选择属性“如果猜测错误”来描述“游戏显示失败结果”这个对象。
以下是使用选择属性更新描述的关键点:
- 选择属性用于定义游戏中的分支逻辑。
- 它通常以条件语句(如“如果...”)开始。
- 它连接了玩家的选择与游戏的特定反馈。
更新测试计划:测试所有路径
每当你在游戏中使用选择来创建替代路径时,都需要测试每一条路径。在黑客游戏的第一个测试计划中,你添加了测试来考虑两种可能的玩家操作:输入正确密码和输入错误密码。
在V1版本中,只有一条路径。无论玩家输入什么猜测,都会显示失败信息。现在游戏中有多条路径,你需要更新测试计划,明确指出玩家的选择将决定两种可能结果中的一种。
以下是更新测试计划的要点:
- 这两种结果都必须在独立的程序运行中进行测试。
- 这些结果的测试和实现都是独立的案例。
- 这是一个新的问题分解技术示例,称为基于案例的分解。在这种技术中,多个独立的解决方案部分被单独创建,然后组合在一起形成一个完整的解决方案。
总结

本节课中,我们一起学习了如何为黑客游戏V2版本的问题制定解决方案,并为V3版本指明了改进方向。核心在于引入控制结构中的选择机制,通过选择属性更新游戏描述,并运用基于案例的分解方法来更新测试计划,确保所有可能的游戏路径(正确与错误猜测)都能得到充分测试。现在,请观察黑客游戏V3版本,以便你能创建自己的V3版本描述和测试计划。
039:观察黑客攻击游戏第三版 🎮
在本节课中,我们将要观察并分析“黑客攻击”游戏的第三个版本。我们将重点关注游戏在玩家输入正确密码后的行为变化,并与之前的版本进行对比。

游戏初始状态
上一节我们介绍了游戏第二版的行为,本节中我们来看看第三版的初始表现。
游戏开始时,其界面与第二版看起来完全相同。
输入正确密码
现在,我将输入正确的密码“hunting”。这一次,游戏会显示一个不同的结果。
在猜对密码后,游戏会显示一条成功信息,然后提示我按下回车键。
实践与观察任务
为了充分理解这个版本的变化,你需要亲自进行以下操作。
以下是具体的实践步骤:
- 自己玩几次这个游戏。
- 确保你看到了两种可能的结果(成功与失败)。
- 注意这两种结果之间的区别,以便你能完成你的功能描述和测试计划。
总结

本节课中我们一起观察了“黑客攻击”游戏的第三版。我们了解到,此版本在玩家输入正确密码后,会显示成功信息并新增一个等待玩家按下回车键的提示,这是与第二版的一个关键区别。通过亲自体验和对比两种结果,你可以为后续的编程实现制定更清晰的需求和测试方案。
040:创建黑客游戏版本3的算法 🎮

在本节课中,我们将学习如何为黑客游戏的第三个版本创建算法。这个版本引入了基于玩家输入的多条游戏路径,因此我们需要修改版本2的算法,加入选择控制结构来决定游戏的结局。
从版本2到版本3的演变 🔄
上一节我们介绍了版本2的算法,它只有一条固定的路径和一种可能的结局。本节中我们来看看版本3的核心变化:游戏结局现在取决于玩家的输入。为了实现这一点,我们需要在算法中加入一个控制结构。
控制结构决定了算法中的步骤何时执行,甚至是否执行。在算法构建器中,每种控制结构都有其独特的形状。选择控制结构由一个菱形表示。
理解选择控制结构 💎
为了说明菱形选择结构的用法,我们来看一个简单的例子:一个决定是否穿毛衣的算法。
首先,我添加一个菱形。我需要基于某个计算结果来选择“穿毛衣”这个动作。这个用于选择结果的计算通常被称为条件。条件被放置在菱形中。
在我的算法中,条件是“温度是否低于10摄氏度”。因此,我从词汇表中选择“温度低于10度”作为条件。
这个条件(温度低于10摄氏度)的结果只能是真或假。要么温度低于10度(条件为真),要么温度大于等于10度(条件为假)。
在算法构建器中,每个选择控制结构都关联着两个矩形:
- 真矩形:包含当菱形中的条件为真时要执行的算法步骤。
- 假矩形:包含当菱形中的条件为假时要执行的算法步骤。
你为选择控制结构选择的条件必须始终能评估为真或假,这样才能用它来选择执行真矩形或假矩形中的步骤。
真矩形中必须始终有算法步骤。如果没有任何步骤可选,那么在算法中包含选择结构就没有意义。然而,假矩形中并不总是需要有步骤。
例如,在穿毛衣的算法中:
- 如果温度低于10度(条件为真),我会穿毛衣,所以我把“穿毛衣”放在真矩形里。
- 如果温度大于等于10度(条件为假),我什么也不做,所以假矩形里不需要放任何东西。
一个选择控制结构通常会占据一个独立的面板。如果在真或假的情况下需要执行多个算法步骤,你可以扩展真矩形或假矩形,让它们使用自己的面板。必要时,两者都可以扩展。


选择控制结构支持基于情况的分解,因为它允许独立创建解决方案的每个部分。
修改算法:应用选择结构 🛠️
现在,让我们修改一份黑客游戏版本2的算法副本,来创建版本3的算法。
你已经拥有了显示失败结局消息所需的步骤。你本可以创建一个“显示成功结局”面板,它与“显示失败结局”面板几乎相同,只是显示的结果不同。然后,你可以根据用户的猜测来选择使用哪一组算法步骤序列。
然而,软件开发中有一个原则叫做“不要重复你自己”(Don‘t Repeat Yourself, DRY)。如果你发现自己多次编写相同的算法步骤,就应该停下来,看看是否能修改算法,让公共步骤只编写一次。
在这个案例中,这些算法步骤之间唯一的区别在于显示的消息(“显示失败结局”的第2、3行 vs “显示成功结局”的第2、3行)。因此,我们可以采取更好的方法:根据玩家的猜测,创建一个不同的结局消息(失败或成功),然后只显示这个消息。这是一个解决方案泛化的例子,因为成功和失败结局中的公共部分被泛化并集中在一处使用。
以下是修改步骤:
- 将“显示失败结局”修改为更通用的“显示结局”。
- 在“显示结局”步骤之前,添加一个名为“创建结局”的步骤。
- 最后,展开“创建结局”步骤,并使用选择结构来创建适当的结局消息。

通过这种方式,我们构建了一个更高效、更易于维护的算法,它能够根据条件动态决定游戏走向。
041:Python if语句与布尔类型 🐍

在本节课中,我们将学习一种新的语句——if语句,以及一个新的Python类型——布尔类型。我们还将介绍三个特殊的标记:NEWLINE、INDENT和DEDENT。这些概念是控制程序流程的基础。
概述
想象一个包含X轴和Y轴坐标的地图,X轴向右增大,Y轴向上增大。当用户输入一个X和Y坐标值时,程序需要判断该坐标点位于哪个象限(西北、东北、西南或东南)。解决这个问题的关键在于判断X和Y值是大于0还是小于0。
如果X值小于0,坐标点在西方;如果X值大于0,坐标点在东方。同理,Y值小于0表示南方,Y值大于0表示北方。
到目前为止,我们见过的所有Python语句都是按顺序从上到下执行的。为了解决上述问题,我们需要程序能够根据条件决定是否显示“北”、“南”、“东”、“西”这些方向词。这就需要引入if语句来实现条件执行。
if语句简介
if语句是一种复合语句,它可以根据条件决定是否执行其内部的语句。一个if语句可以执行其内部的语句,也可以跳过它。
以下是解决象限判断问题的程序示例:
x_str = input("Enter an X coordinate: ")
x = int(x_str)
y_str = input("Enter a Y coordinate: ")
y = int(y_str)
if y < 0:
print("South")
if y > 0:
print("North")
if x < 0:
print("West")
if x > 0:
print("East")
运行程序,输入X为100,Y为-50,程序会输出“South”和“East”,这是正确的结果。
语句结构:简单语句与复合语句
一个包含其他语句的语句称为复合语句。不包含其他语句的语句称为简单语句。
if语句是复合语句。- 表达式语句、赋值语句和导入语句是简单语句。
每个简单语句末尾的换行符会产生一个NEWLINE标记,我们稍后会讨论。
在Python中,可以将简单的复合语句写在一行,但本课程中所有复合语句都将写在多行。
if语句的语法
if语句的简化语法图如下:
if_statement ::= “if” expression “:” suite
- 头部:关键字
if、表达式和冒号:共同构成了if语句的头部。其中的表达式常被称为条件。 - 代码组:
suite是一个缩进的语句块,从新的一行开始。
三个特殊标记:NEWLINE、INDENT、DEDENT
NEWLINE、INDENT和DEDENT是三个独立的Python标记,不属于运算符、标识符、分隔符、字面量或关键字中的任何一类。它们用于分隔语句,并界定代码组的开始和结束。
NEWLINE:当词法分析器遇到由按下回车键产生的行尾字符时,会创建一个NEWLINE标记来分隔语句。INDENT:为了标记一个代码组的开始,需要在冒号后换行,并在下一行开头使用空白字符(空格或制表符)。词法分析器会将这些开头的空白字符转换为一个INDENT标记,无论有多少个空白字符。DEDENT:DEDENT标记标识一个代码组的结束。代码组之后的语句必须与包含该代码组的if语句头部保持相同的缩进级别。词法分析器会将这种对齐的缩进转换为一个DEDENT标记。
示例程序中的四个if语句都具有有效的语法。以第5-6行的if语句为例,词法分析器会生成以下标记序列:
if -> y -> < -> 0 -> : -> NEWLINE -> INDENT -> print -> ( -> "South" -> ) -> NEWLINE -> DEDENT。
布尔类型
if语句使用了一种新的Python类型——布尔类型,简称Bool。
布尔类型只有两个对象:
True:表示真。False:表示假。
许多Python表达式会求值为这两个对象之一。例如,程序第5行的表达式 y < 0 会根据变量y绑定的整数值,求值为True或False对象。
为了方便,Python使用两个关键字True和False来指代这两个布尔对象。尽管它们是关键字,但行为类似于字面量。当解释器遇到这些关键字时,会使用内存中相应的布尔对象。
请注意,if、import、from也是关键字,但它们被用作分隔语句的标点符号,不会被对象替换。
if语句的语义
if语句的执行语义如下:
- 对头部中的条件表达式进行求值。结果是一个布尔对象(
True或False)。 - 如果条件对象是
True,则执行代码组suite中的语句。如果条件对象是False,则跳过整个代码组。
代码组suite的语义是:按顺序执行其中的每个语句。
让我们将语义应用到示例程序上:
- 第5-6行:条件
y < 0求值(y为-50),结果为True,因此执行代码组,打印“South”。 - 第7-8行:条件
y > 0求值,结果为False,因此跳过代码组,不打印“North”。 - 第9-10行:条件
x < 0求值(x为100),结果为False,跳过代码组。 - 第11-12行:条件
x > 0求值,结果为True,执行代码组,打印“East”。
如果运行程序并输入X和Y都为0,则不会有任何输出。因为所有四个if语句的条件都求值为False,所有代码组中的语句都不会被执行。
代码组中的多条语句
if语句的语法和语义支持在代码组中包含多条语句。
例如,如果将程序修改为:
if y < 0:
print("South")
print("Bottom")
当输入为100和-50时,输出将包含“South”、“Bottom”和“East”。因为第6行和第7行的两条语句(都在同一个代码组中)以及第13行的语句都被执行了。
请注意,一个代码组的开始只有一个INDENT标记,结束只有一个DEDENT标记。词法分析器不会在代码组内部的语句之间创建INDENT或DEDENT标记。
重要:缩进时应使用统一数量的空格(通常是4个)。如果缩进空格数不一致,很可能会遇到“缩进错误”。
总结
本节课我们一起学习了:
if语句的语法和语义,它是一种用于条件执行代码的复合语句。- 布尔类型,它只有
True和False两个值,是条件表达式的结果类型。 - 三个用于代码结构化的特殊标记:
NEWLINE、INDENT和DEDENT。

理解这些概念是编写能够根据不同情况做出决策的程序的关键第一步。
Python编程与问题解决:第5章:if语句的elif和else子句

在本节课中,我们将学习Python中if语句的更多细节。具体来说,我们将了解当if语句中的表达式求值结果不是布尔对象时会发生什么,并学习如何使用可选的elif和else子句来扩展if语句的功能。
非布尔条件表达式
上一节我们介绍了if语句的基本用法,本节中我们来看看当if语句的条件表达式结果不是布尔值时,Python会如何处理。
在Python中,当if语句中的表达式求值结果为一个对象时,该对象总是会被解释为一个布尔值,即使它本身不是布尔类型。
大多数对象会被解释为True,即使它们并不等于True对象本身。只有少数对象会被解释为False,包括:
False对象- 空字符串
"" - 整数对象
0 - 浮点数对象
0.0 None类型对象- 以及其他一些我们尚未接触到的对象类型。
例如,字符串对象"hello"在if语句中会被解释为True,因此其对应的代码块会被执行。
在计算机科学中,一个求值为布尔值的表达式被称为条件。由于在Python的if语句中,所有对象无论其类型如何,都会被解释为True或False,因此if语句中的表达式也被称为条件。
使用else子句
如果我想在条件为True时执行一套语句,在条件为False时执行另一套不同的语句,可以使用if语句的else形式。
以下是包含可选else子句的if语句的修订语法:
if condition:
# if 子句的语句组
else:
# else 子句的语句组
当使用else关键字时,我们说这个if语句有两个子句:if子句和else子句。if子句包含一个条件和一个语句组,而else子句只包含一个语句组,没有显式的条件。
使用elif子句
if语句还有一种更通用的形式,可以使用elif子句。elif子句可用于在同一个if语句中检查多个条件。
以下是包含可选elif子句和可选else子句的if语句的修订语法:
if condition1:
# 条件1为真时执行的语句组
elif condition2:
# 条件2为真时执行的语句组
else:
# 所有条件都不为真时执行的语句组
这些语义意味着:
- 如果存在
else子句,那么if语句中恰好会有一个语句组被执行,其余语句组将被跳过。 - 如果没有
else子句,并且至少有一个条件为True,那么恰好会有一个语句组被执行。 - 如果没有
else子句,并且所有条件都不为True,那么不会有任何语句组被执行。
总结

本节课中我们一起学习了Python if语句的两个重要扩展。我们首先了解了当if语句的条件表达式求值结果为非布尔对象时,Python会如何将其解释为布尔值。接着,我们学习了如何使用elif子句在同一个if语句中检查多个条件,以及如何使用else子句为所有条件都不满足的情况提供一个默认的执行路径。这些结构使得我们的程序能够根据更复杂的情况做出不同的决策。
043:Python关键字运算符、短路求值、一元表达式与运算顺序 🐍

在本节课中,我们将学习四个核心概念。首先,我们将介绍那些行为类似于运算符的关键字。其次,我们将描述布尔运算符使用的一种特殊求值技术,称为短路求值。第三,我们将引入一种新的表达式类型,称为一元表达式。最后,我们将描述Python在处理包含多个运算符的计算时所使用的运算顺序。
关键字作为运算符 🔑
上一节我们回顾了运算符的基本概念。现在,让我们来看看Python中的关键字如何扮演运算符的角色。
回想一下,诸如加号(+)和小于号(<)这样的运算符标记,用于在二元表达式中对两个操作数应用一个运算符函数。Python共有19个运算符标记。有趣的是,一些Python关键字也可以用作运算符。
让我们更仔细地审视所有的关键字。你已经知道,像 from、import 和 if 这样的关键字被用作分隔符,为语句添加标点。实际上,33个关键字中有26个被用作分隔符,以区分语句的不同部分。
你也见过,像 True 和 False 这样的关键字被用作字面量,绑定到特定的对象。实际上,None 也是一个关键字字面量,它绑定到一个类型为 NoneType 的对象,这个对象通常由不需要返回有意义对象的函数调用返回。
剩下的四个关键字 and、is、not 和 or 被用作运算符。而关键字 in 根据上下文,既可以用作运算符,也可以用作分隔符。
我将通过添加可以用作二元运算符的关键字运算符,来修订二元运算符的语法图。
请注意,除了 and、in、is、or 之外,还有两个关键字运算符对 not in 和 is not,它们各自可以作为一个单一的二元运算符使用。
虽然我添加了一些新的二元运算符,但二元表达式的语义规则并没有改变。
短路求值 ⚡
上一节我们介绍了关键字运算符。本节中,我们将重点探讨布尔运算符 and 和 or 使用的特殊求值技术——短路求值。
例如,我将二元表达式语义应用于表达式 False and unbound。
- 第一步,左侧表达式是关键字字面量
False,它求值为布尔对象False。 - 第二步,运算符是
and。虽然第一个操作数的类型是布尔型,但由于操作数是False,情况变得特殊。
通常,运算符和操作数类型都需要用来确定运算符函数。然而,当运算符是 and 时,实际上会忽略第一个操作数的类型。and 运算符总是调用同一个运算符函数,无论第一个操作数是什么类型。
在第三步中,解释器应用这个 and 运算符函数。and 运算符函数使用了一种称为短路求值的技术,其结果对象可能不依赖于第二个操作数。
以下是 and 运算符函数的语义:
- 如果第一个操作数被解释为
False,则第一个操作数就是结果对象。 - 否则,将第二个表达式求值作为结果对象。
在第一步中,第一个操作数是 False。因此,这个第一个操作数对象 False 被返回。第二步被跳过,所以第二个表达式甚至没有被求值。
如果第二个表达式被求值,由于我没有将标识符 unbound 绑定到任何对象,它将会显示一个语义名称错误。
and 运算符函数还有另一个有趣的特性。在第一步中,第一个操作数是空字符串 "",你可能还记得在 if 语句条件中,它被解释为 False。and 运算符函数的语义以同样的方式解释非布尔对象,因此空字符串被解释为 False。所以,空字符串就是结果对象。如果我使用 0 或 0.0 代替空字符串,表达式将分别求值为 0 或 0.0。同样,第二步被跳过,因此第二个表达式没有被求值,也没有显示语义名称错误。
如果第一个操作数没有被解释为 False,则会对第二个表达式进行求值。例如,如果我求值 "hello" and "goodbye",解释器将返回 "goodbye"。在第一步中,第一个操作数是字符串 "hello",它被解释为 True(因为它没有被解释为 False),因此应用第二步。在第二步中,第二个表达式求值为字符串 "goodbye",该字符串被用作结果对象。
or 运算符函数也使用短路语义。如果第一个操作数被解释为 True,则第一个操作数就是结果对象。否则,将第二个表达式求值作为结果对象。
一元表达式 ➕➖
上一节我们探讨了短路求值。现在,让我们来看看一种新的表达式形式。
请注意,无论是运算符标记 ~ 还是关键字运算符 not,它们本身都不能用作二元运算符。相反,两者都是一元运算符,作用于单个操作数。
以下是一元表达式的语法图。
这是一个更通用的表达式语法图,现在包含了一元表达式。
加号(+)和减号(-)运算符标记既可以用作二元运算符,也可以用作一元运算符。一元表达式的语义与二元表达式的语义相似,只是它只对一个操作数进行求值。
例如,我将一元表达式语义应用于 -27。
- 第一步,表达式是字面量整数
27,它求值为int对象27。 - 第二步,一元运算符
-和类型int用于确定数值取反函数,该函数会反转其整数操作数对象的符号。 - 第三步,将数值取反函数应用于
int对象27,以获得int对象-27。
请注意,-27 不是一个字面量整数。它是一个一元表达式,应用减号运算符到一个整数对象以获得结果对象。
运算符优先级与运算顺序 📊
上一节我们介绍了一元表达式。最后,我们来学习当表达式中出现多个运算符时,Python如何决定运算顺序。
如果一个表达式中出现多个运算符,Python解释器必须选择运算的顺序。例如,在表达式 3 + 4 * 5 中,有两个运算符 + 和 *。
如果Python解释器从左到右按顺序应用这些运算符,那么 3 + 4 等于 7,7 * 5 等于 35。但 35 不是正确答案。23 才是,因为Python在应用 + 运算符之前先应用 * 运算符,类似于数学中的运算顺序。
Python有一个运算符优先级表,它定义了在同一表达式中应用多个运算符时应遵循的顺序。表中位置靠下的运算符具有更高的优先级,会先于表中位置靠上的运算符被应用。
例如,* 在 + 之前应用,因为它在表中位于 + 的下方。这就是为什么 3 + 4 * 5 求值为 23,而不是 35。
你应该认识这个表中的许多运算符,例如运算符标记 +、*、<,以及关键字运算符 and 和 or。
总结 📝

在本节课中,我们一起学习了四个重要的Python概念。我们描述了那些行为类似于运算符的关键字。我们还探讨了布尔运算符使用的一种特殊求值技术,称为短路求值。我们引入了一种新的表达式类型,称为一元表达式。最后,我们了解了Python在处理包含多个运算符的计算时所使用的运算顺序。掌握这些概念对于编写正确且高效的Python代码至关重要。
044:程序破解版本3

概述
在本节中,我们将学习如何为“程序破解”游戏的第三个版本添加注释,并准备使用Python的if语句来实现算法中的选择结构。我们将重点关注如何将算法步骤转化为代码注释,并为后续实现选择逻辑做好准备。
添加算法注释
上一节我们完成了算法设计。本节中,我们来看看如何将这些算法步骤转化为代码中的注释。
编程“破解版本3”的第一步,是添加与算法新步骤相对应的注释。
以下是需要添加的注释步骤:
- 我将为“选择菱形”添加一个占位注释。当你翻译算法并添加
if语句的头部时,需要替换它。 - 我将使用“检查 guess 是否等于 password”作为“guess等于password菱形”的注释。当你添加
if头部(例如if guess == hunting)时,应删除此注释,因为它将变得多余。
实现选择结构
我们使用Python的if语句来实现选择结构。
- 算法中的“True(真)”框对应
if子句的语句组。 - 算法中的“False(假)”框对应
else子句的语句组。
你需要为“C成功步骤”创建一个注释,并为“Cate失败步骤”创建另一个注释。因为算法中的每个矩形都应在代码中有对应的注释。
准备结果消息
在接下来的活动中,你将使用选择结构来决定显示哪个结果消息。
为结果字符串使用良好的标识符名称。我将为你选择一个作为开始。
结果消息的第一行始终是玩家的猜测。第二行则根据猜测成功与否而不同:成功时为“exiting debug mode”,失败时为“login failure terminal locked”。
我们将第二行称为outcome_line2,因为这个名称足够通用,适用于两种情况。
在版本2中,直接显示字面字符串“login failure terminal locked”就足够了。但在这个版本中,必须通过引用outcome_line2这个标识符来显示合适的字符串。
我将更新代码以使用这个标识符。我会在计算其x坐标的语句和draw_string函数调用中,将字面量“login failure”替换为标识符outcome_line2。
现在,这段代码将显示outcome_line2所绑定的任何字符串。
请注意,如果我现在运行程序,将会产生错误,因为outcome_line2尚未被绑定。
后续步骤
在下一个活动中,你将创建if语句、绑定outcome_line2,并翻译算法中“显示结果”步骤的其余部分。
如果你遇到困难,请复习关于if语句的编程语言课程。
总结

本节课中,我们一起学习了如何将破解算法的步骤转化为代码注释,为Python选择结构(if/else)的实现做好了准备,并定义了用于显示不同结果消息的变量。我们为下一阶段实现完整的猜测逻辑奠定了基础。
045:版本3代码回顾与软件质量测试


在本节课中,我们将回顾“黑客”游戏的第三个版本代码。我们将学习如何使用调试器来观察if语句的执行过程,并介绍一个新的软件质量测试概念——代码反映设计。
🎼 调试技术:使用断点
上一节我们讨论了程序的基本结构。本节中,我们来看看如何更高效地调试程序,特别是if语句。
程序底部的if语句是我们的重点。为了直接观察它的执行,我们可以使用一种新的调试技术:断点。我们可以在if语句旁添加一个断点,这样就不需要逐行执行代码来找到它。
断点是一个调试功能,它使调试器在指定的代码行暂停执行,并等待进一步的调试指令。这允许你无需跟踪每一行代码就能使用调试器。
在代码行号旁添加的红色圆点是一个视觉提示,表示此处有一个断点。
调试过程演示
现在,我将按下工具栏中的“运行到断点”按钮。这个操作会让程序正常执行,直到遇到一个断点或需要用户输入的语句。
程序会显示标题、密码列表和密码输入提示,然后暂停以允许我输入密码。在正常情况下,如果我输入密码,游戏会立即显示结果。
我将输入密码“hunting”。调试器会评估清屏操作,并在遇到if语句处的断点时停止。它暂停下来,让我决定下一步要做什么。
if语句将检查我的猜测是否等于秘密密码。请注意,在堆栈数据面板中,变量guess被绑定为“hunting”。
由于guess和“hunting”相等,当我按下“单步跳过”按钮时,下一个被高亮显示的语句是if子句中的outcome_line2赋值语句。
我再按两次“单步跳过”按钮,直到prompt赋值语句被高亮显示。此时,在堆栈数据面板中,outcome_line2和outcome_line3已作为标识符被添加到局部命名空间,并绑定到表示成功的字符串。
下一次我按下“单步跳过”按钮时,调试器不会高亮显示else子句,即使它在下一行。在一个if语句中,最多只会执行一个代码块。只要if语句的某个条件为真,与该条件关联的代码块就会被执行,所有其他条件和代码块都会被忽略。
所以,当我按下“单步跳过”按钮时,调试器完全跳过了else子句及其代码块,直接高亮显示了x_space赋值语句。
🔄 测试错误密码的情况
让我们通过追踪一个错误密码的输入过程,来看看我们基于情况分解的第二种情况。
我将用“停止”按钮结束当前的追踪,然后再次按下“运行到断点”按钮。这次,当程序提示输入密码时,我将输入一个错误的猜测。
if语句被高亮显示。在堆栈数据面板中,guess被绑定为我输入的错误密码。由于guess不等于“hunting”,条件语句的求值结果为false。
当我再次按下“单步跳过”按钮时,调试器会跳过if的代码块,直接跳转到else子句代码块的第一条语句。因为else不需要被求值,它的代码块会在条件为false时自动执行。



📐 软件质量测试:代码反映设计

现在,让我们讨论这个版本“黑客”游戏的软件质量测试。之前我们介绍了如何编写良好的注释和使用描述性的标识符。新的软件质量测试部分叫做“代码反映设计”。
代码反映设计有两个测试标准:
以下是第一个测试标准:
- 算法中的每个矩形框都能被翻译成代码中的一系列简单语句。
以下是第二个测试标准:
- 算法中的控制结构与代码中的控制结构存在一一对应关系。


算法控制结构与代码控制结构的一一对应关系意味着:算法中的每一个控制结构,在代码中都有且仅有一个对应的控制结构。
算法中的一个矩形框可以被翻译成多行代码,但算法中的一个菱形决策框在程序中总是被翻译成一行代码。
你的代码应该反映你的设计,这样在下一个版本中修改算法时,只需要翻译算法中发生改变的部分即可实现代码更新。
总结
本节课中,我们一起学习了如何使用断点调试技术来观察if语句的执行流程,理解了条件分支的执行逻辑。我们还介绍了“代码反映设计”这一重要的软件质量测试概念,它要求代码结构清晰对应算法设计,以便于未来的维护和更新。恭喜你完成了“黑客”游戏的第三个版本。
046:第4版黑客游戏解决方案问题

概述
在本节课中,我们将学习如何改进“黑客游戏”第3版代码的质量。我们将重点解决两个核心问题:重复代码和重复数据。通过引入循环控制结构和使用标识符,我们可以显著减少代码量,提高代码的可读性和可维护性。
重复代码问题与循环结构
上一节我们完成了游戏功能的开发,本节中我们来看看代码中存在的重复问题。
在“黑客游戏”第3版的解决方案代码中,存在大量重复的语句,用于显示标题、密码和结果消息。这些代码几乎完全相同,只是存在微小的差异。最初,通过复制粘贴来重用显示字符串的代码似乎很方便,但这导致了近200行的代码量。这违反了“不要重复自己”的软件开发原则。
通常,如果你发现自己编写了多次相同的代码语句,就应该修改程序,使公共语句只编写一次。
我们称这些连续相同或非常相似的语句为相邻重复语句组。在这个版本中,你将学习如何使用一种称为重复控制结构或循环的新结构来消除它们。
循环是一段代码语句序列,它们会被重复执行,直到满足特定条件为止。
使用重复控制结构可以使你的代码更短,从而更容易编写和阅读。在本版本中,你将使用这些控制结构,将近200行代码替换为少于90行。
使用重复控制结构来替换重复代码行是一种新的抽象形式,称为控制抽象。控制抽象用控制结构取代默认的顺序程序流程,以减少重复。
以下是循环结构的一个基本代码示例:
for i in range(5):
print(f"这是第 {i+1} 次显示")


重复数据问题与标识符
解决了代码重复问题后,我们再来看看数据重复的问题。
第二个需要改进的解决方案是消除重复数据。例如,字面量浮点数 0.3 在程序中重复出现了23次。这是一个问题,因为如果我想要更短的延迟,就需要修改23个独立的 sleep 函数调用中的参数。同样,如果我想要更改密码的X坐标,就需要修改13个不同的 drawstring 函数调用中的第二个参数。
虽然我可以使用查找和替换功能来一次性更改所有的 sleep 延迟,这很简单。但是,如果我尝试使用查找和替换来更改密码的X坐标(例如将 0 改为其他值),它不仅会更改13处 drawstr 调用,还会更改程序中其他36个地方的 0。
因此,我必须有一种方法来指定密码 drawstring 调用中的 0 的意图与其他36个 0 的意图不同。为了解决这个问题,我可以使用一个标识符来表示密码的X坐标,并将其绑定到 0。这样,要更改密码的X坐标,我只需要更改绑定它的那一个赋值语句。
通常,如果一个字面量出于相同目的被多次使用,就应该用一个标识符绑定它,以便可以在一个地方更改这个字面量。
你能在“黑客游戏”第3版代码中找到其他应该用标识符绑定的重复字面量吗?
版本4的变更与测试
由于你在本版本中所做的更改仅影响代码质量,因此不需要观察或试玩“黑客游戏”第4版。描述和测试计划也没有变化。
你刚刚在游戏创建过程中完成了五项任务,并且只用了短短几秒钟,恭喜你。
编码完成后,别忘了使用之前的测试计划来确保游戏功能仍然正确。


总结
本节课中我们一起学习了如何提升代码质量。我们通过引入循环结构解决了重复代码的问题,使代码更简洁。同时,我们通过使用标识符绑定重复使用的字面量,解决了重复数据的问题,提高了代码的可维护性。这些改进是软件工程中“不要重复自己”原则的具体实践。
047:为黑客游戏版本4创建算法 🎮

在本节课中,我们将学习算法构建中的一个新概念:确定重复(或称确定迭代)。我们将了解它如何工作,并利用它来简化黑客游戏中的显示逻辑。
概述:什么是确定重复?
上一节我们介绍了控制结构的基本概念。本节中,我们来看看一种新的控制结构——确定重复。
控制结构决定了算法中的步骤何时执行。确定重复会将一个算法步骤(或一系列步骤)重复执行特定的次数。
由于步骤被重复执行,所以称为“重复”。“确定”意味着在重复开始之前,我们就已经知道需要重复多少次。
公式:
重复执行 [步骤] [N] 次
例如:
- 制作蛋糕时,食谱要求你搅拌面糊20次。这就是确定重复。
- 如果食谱要求你搅拌面糊直到没有结块为止,这就不是确定的,因为你不知道在停止搅拌前需要搅拌多少次。


可以将确定重复理解为:为序列中的每一个项目重复执行一个步骤。
- 你有一篮子洗好的衣服。对于篮子里的每一件衬衫,你都会把它收起来。
- 在搅拌蛋糕的例子中,你对于序列1到20中的每一个数字,都会搅拌一次面糊。
在算法构建器中表示确定重复
确定重复在算法构建器中由一个 for图标表示。
for图标中已经包含了单词 for 和 in。一个普通的矩形通过箭头连接到for图标。这个普通矩形中的步骤就是将要被重复执行的步骤。for图标则指明了这个步骤应该重复多少次。
为了完成for图标,你需要从词汇表中选择两个项目:
- 目标元素:你想在重复步骤中使用的元素。在收衣服的例子中,就是你想收起来的“衬衫”。
- 序列:包含目标元素的集合。在收衣服的例子中,就是“洗衣篮”。
完成后的for图标会显示为:for [目标] in [序列]。
例如,for shirt in laundry_basket。然后,你可以在下方的矩形中添加步骤 put away shirt。这个算法将为洗衣篮里的每一件衬衫执行“收好衬衫”这个步骤。每次重复时,都会使用序列中的下一个目标衬衫。
构建更复杂的重复序列
你可以扩展for图标下的普通矩形,以重复执行一系列步骤,而不仅仅是一个步骤。
例如,我可以将“收好衬衫”扩展为一个步骤序列:拿起衬衫 -> 打开抽屉 -> 放入衬衫 -> 关上抽屉。
你甚至可以在这个重复序列中加入更复杂的逻辑,比如添加一个选择控制结构,来判断每件衬衫是应该折叠还是熨烫。
在黑客游戏版本4中应用确定重复
现在,让我们将确定重复应用到黑客游戏的算法中。
在你之前的算法中,“显示结果”部分需要六个矩形,对应六行结果文本。我们将删除所有这六个矩形,改用确定重复。
以下是具体步骤:
- 填写
for图标:我们需要选择目标和序列。- 游戏需要显示每一行结果,所以目标元素是
outcome_line(结果行)。 - 每一行结果都是完整结果的一部分,所以序列是
outcome(结果)。 - 因此,
for图标应填写为:for outcome_line in outcome。
- 游戏需要显示每一行结果,所以目标元素是
- 填写要重复的步骤:我们想要显示每一行结果。所以,在
for图标下的矩形中,我们应该添加display和outcome_line。但这里需要注意:结果行需要在窗口中央显示。因此,正确的步骤是添加display和centered outcome_line。
你的任务:
请用确定重复控制结构,替换“显示标题”和“显示密码列表”的步骤。此外,在“显示密码列表”的面板中,请额外添加一个步骤,用于从列表中选择正确的密码。
总结

本节课中,我们一起学习了确定重复(确定迭代) 这一核心控制结构。我们了解到,它用于将某个步骤重复执行预先确定的次数,通常表示为“为序列中的每个项目执行某操作”。我们探讨了如何在算法构建器中使用for图标来实现它,并实践了如何用更简洁的确定重复逻辑,来重构黑客游戏中显示多行文本的算法。掌握这一概念,能让你写出更高效、更易读的代码。
048:Python序列与下标索引 🎼

在本节课中,我们将学习Python中一个重要的概念——序列类型,以及如何通过下标索引来访问序列中的元素。
概述
序列是Python中一类特殊的数据类型,它代表一个有限、有序的元素集合。本节课我们将以字符串类型为例,详细介绍序列的特性,并学习如何使用下标索引表达式来获取序列中的特定元素。
什么是序列?
一个序列是一个有限、有序的元素组,其中每个元素都有一个介于0到(元素总数-1)之间的数字索引。
Python中没有名为“sequence”的单一类型。相反,“序列”是一个类别,它包含了多种不同的Python类型。Python的字符串类型(str)就是其中一种序列。
每个字符串都是一个由索引字符组成的有限有序组。例如,字符串对象 "Hello" 就是一个字符序列:
H是索引为 0 的元素。e是索引为 1 的元素。l既是索引为 2 的元素,也是索引为 3 的元素。o是索引为 4 的元素。
请记住,Python的索引总是从0开始,而不是从1开始。
下标索引表达式
为了引用序列中的某个元素,我们可以使用一种新的Python表达式——下标索引表达式。它使用两个分隔符:左方括号 [ 和右方括号 ]。
Python的下标索引类似于数学中使用下标的方式。例如,要引用字符串 "Hello" 在索引1处的元素,可以使用以下表达式:
"Hello"[1]
这个表达式读作“Hello下标1”。
在许多编程语言中,字符串的元素类型是 char(字符)。然而,Python中没有char类型。当我们使用下标索引引用字符串中的一个字符时,解释器返回的是一个单元素的字符串对象。
下标索引的语法与语义
以下是下标索引表达式的简化语法图:
primary_expression [ index_expression ]
其简化的语义规则如下:
- 计算左侧表达式,得到主对象。
- 计算括号内的表达式,得到索引对象。
- 如果主对象不是一个序列,则报告“下标”错误。
- 如果索引对象不是一个小于序列长度的非负整数,则报告“索引”错误。
- 引用主对象在指定索引处的元素。
实例分析
上一节我们介绍了下标索引的规则,本节中我们通过几个例子来看看这些规则是如何应用的。
以下是几个正确与错误用例的分析:
用例一:正确索引
表达式:"Hello"[1]
- 主对象是
"Hello"。 - 索引对象是
1。 "Hello"是字符串类型,属于序列,无错误。- 整数
1是非负整数,且小于字符串长度5,无错误。 - 返回索引
1处的元素,即单元素字符串"e"(注意不是索引0的"H")。
用例二:对非序列对象使用下标(错误)
表达式:27[1]
- 主对象是
27。 - 索引对象是
1。 - 主对象是整数类型(
int),不是序列。因此会报告语义类型错误:“‘int’ object is not subscriptable”,计算停止。
用例三:索引不是整数(错误)
表达式:"Hello"["1"]
- 主对象是
"Hello"。 - 索引对象是字符串
"1"。 "Hello"是序列,无错误。- 索引对象是字符串,不是整数。因此会报告语义类型错误:“string indices must be integers”,计算停止。
用例四:索引越界(错误)
表达式:"Hello"[5]
- 主对象是
"Hello"。 - 索引对象是
5。 "Hello"是序列,无错误。- 整数
5是非负整数,但它不小于字符串长度5(最大有效索引是4)。因此会报告语义索引错误:“string index out of range”,计算停止。
索引必须严格小于序列长度的原因在于,序列的索引范围是从0到(序列长度 - 1)。
负索引
在标准的语义规则中,步骤4指出解释器会对所有负整数报告索引错误。然而,Python内置的序列类型可以处理一些负索引。
例如,在表达式 "Hello"[-1] 中,返回的是最后一个元素 "o"。其原理是将负索引 -1 加上字符串长度 5,得到索引 4,然后返回该索引处的元素 "o"。
使用索引 -n 可以得到倒数第 n 个元素,这在某些情况下很有用。但是,如果使用的负索引绝对值过大,仍然会导致索引错误。例如,"Hello"[-6] 就是一个错误,因为 "Hello" 只有5个字符,不存在倒数第6个元素。
总结
本节课中我们一起学习了Python中的序列类型和下标索引表达式。
- 我们了解到序列(如字符串)是一个有限、有序的元素集合,可通过从0开始的数字索引访问。
- 我们掌握了使用
序列对象[索引]这种语法来获取序列中的特定元素。 - 我们详细分析了下标索引的语义规则,并通过实例理解了正确使用索引以及常见错误(如对非序列下标、索引非整数、索引越界)的发生原因。
- 最后,我们还了解了Python支持使用负索引来方便地访问序列末尾的元素。

理解这些概念是有效处理字符串及其他序列数据的基础。
049:元组与列表类型 📚

在本节课中,我们将要学习Python中两种新的序列类型:元组(Tuple) 和列表(List)。同时,我们也会介绍一种新的表达式——括号表达式(Parenthesized Expression)。这些概念是构建更复杂数据结构的基础。
已掌握的序列类型:字符串
在开始之前,你已经使用过一种序列类型:字符串(String)。字符串的元素必须都是字符。
元组(Tuple)类型
元组是一种序列,其元素可以是任意类型。例如,一个元组可以包含字符串“kiwi”和“orange”、整数27以及函数len。
与字符串可以通过字面量(如"hello")直接创建不同,元组没有专用的字面量标记来创建。
如何创建元组:表达式列表
我们使用一种称为表达式列表(Expression List)的结构来创建元组。表达式列表由多个用逗号分隔的表达式组成。
以下是一个创建包含“kiwi”、“orange”、27和len的元组的表达式列表:
"kiwi", "orange", 27, len
现在,让我们更新表达式的语法图,使其包含表达式列表。
表达式语法图(修订版):
expression ::= ... | expression_list
表达式列表语法图:
expression_list ::= expression ("," expression)* [","]
表达式列表的语义规则
表达式列表的语义规则如下:
- 从左到右依次求值每个表达式,得到表达式对象。
- 如果列表中没有逗号(即只有一个表达式),则结果对象就是该单个表达式对象。
- 如果列表中有逗号,则结果对象是一个元组,其中包含所有求值后的表达式对象。
让我们应用这个规则来创建 ("kiwi", "orange", 27, len) 这个元组:
- 步骤1:对四个表达式分别求值,得到四个对象:字符串“kiwi”、字符串“orange”、整数
27和函数len。 - 步骤2:跳过,因为表达式列表中有逗号。
- 步骤3:创建一个包含这四个对象(“kiwi”、“orange”、
27、len)的元组对象。
创建单元素元组和空元组
要创建只包含一个元素的元组,需要在单个表达式后加一个尾随逗号。例如:
"kiwi",
其创建过程是:
- 步骤1:对单个表达式求值,得到字符串“kiwi”对象。
- 步骤2:跳过,因为表达式列表中有逗号。
- 步骤3:创建一个包含单个“kiwi”对象的元组。
如果尝试只用一个逗号来创建空元组,会导致语法错误。因此,我们需要另一种方法。
上一节我们介绍了如何使用表达式列表创建元组,但创建空元组需要不同的方法。本节中,我们来看看如何通过括号表达式来实现。
括号表达式(Parenthesized Expression)
我们将括号表达式添加到表达式的语法图中。
表达式语法图(再次修订):
expression ::= ... | expression_list | parenthesized_expression
括号表达式语法图:
parenthesized_expression ::= "(" [expression_list] ")"
括号表达式的语义规则
括号表达式的语义规则是:
- 如果括号内没有表达式列表,则结果对象是一个空元组。
- 如果括号内有表达式列表,则对其求值,并将结果作为结果对象。
因此,我们可以使用一对空括号 () 来创建一个空元组。
除了创建元组,括号在Python中的主要作用与在数学中类似:改变运算符的正常优先级。在Python运算符优先级表中,括号位于最底部,意味着它们可以赋予任何运算最高的优先级。
例如,如果你想先执行加法再执行乘法,可以将加法运算符及其操作数放在括号内:
(5 + 2) * 5 # 结果是 35
如果没有括号,表达式 5 + 2 * 5 将按照默认优先级(先乘除后加减)计算,结果为 23 而不是 35。
我们已经了解了元组序列类型和用于创建空元组的括号表达式。现在,让我们来看看另一种非常相似的序列类型——列表(List)。
列表(List)类型
列表与元组相似,其元素也可以是任意类型。例如,一个列表可以包含字符串“kiwi”和“orange”、整数27以及函数len。
与元组一样,列表也没有专用的字面量标记来创建。
如何创建列表:列表显示
创建列表使用一种称为列表显示(List Display)的表达式,它使用逗号作为分隔符,并用左右方括号 [ 和 ] 作为定界符。
以下是一个列表显示表达式,用于在内存中创建 ["kiwi", "orange", 27, len] 这个列表:
["kiwi", "orange", 27, len]
让我们再次更新表达式的语法图,使其包含列表显示。
表达式语法图(最终修订版):
expression ::= ... | expression_list | parenthesized_expression | list_display
列表显示简化语法图:
list_display ::= "[" [expression_list] "]"
列表显示的语义规则
列表显示的简化语义规则是:
- 如果方括号内没有表达式列表,则结果对象是一个空列表
[]。 - 如果方括号内有表达式列表,则对其求值,并返回一个按顺序包含所有这些对象的列表对象。
让我们将这个规则应用到 ["kiwi", "orange", 27, len] 这个列表显示上:
- 步骤1:方括号内有表达式列表,因此结果对象不是空列表。
- 步骤2:对四个表达式求值,得到“kiwi”字符串、“orange”字符串、
27整数和len函数对象。 - 步骤3:创建一个按顺序包含这四个元素的列表对象。
你可能会疑惑,为什么Python要有元组和列表这两种看起来如此相似(除了创建方式不同)的类型?这个问题的答案,我们将在下一节课中揭晓。
总结 🎯
在本节课中,我们一起学习了:
- 元组(Tuple):一种元素类型任意的序列,使用表达式列表(逗号分隔)创建,或用括号表达式
()创建空元组。 - 括号表达式:主要作用是改变运算优先级,也可用于创建空元组
()。 - 列表(List):另一种元素类型任意的序列,使用列表显示(方括号
[]包裹的表达式列表)创建。
核心创建方式总结如下:
- 元组:
item1, item2, ...或(item1, item2, ...)或() - 列表:
[item1, item2, ...]或[]

理解这两种基本数据结构是进行有效Python编程的关键一步。
050:序列元素替换与可变性

在本节课中,我们将学习如何使用一种特殊的赋值语句来修改序列中的元素,并理解可变类型与不可变类型之间的核心区别。
序列元素的替换
上一节我们介绍了序列的基本概念和索引操作。本节中,我们来看看如何修改序列中的元素。
要修改序列中的元素,需要使用一种支持下标表达式作为目标的赋值语句。回顾一下普通赋值语句的语法,其目标通常是一个简单的标识符。
以下是包含下标表达式的赋值语句语法:
target[expression] = expression
其执行语义如下:
- 计算右侧表达式,得到结果对象。
- 计算左侧表达式,得到主对象(即序列本身)。
- 计算方括号内的表达式,得到索引对象。
- 检查主对象是否为可变序列。
- 检查索引是否有效。
- 将序列中指定索引处的元素重新绑定为结果对象。
例如,我们可以修改列表中的一个元素。首先,创建一个列表并显示其原始内容。
my_list = [‘kiwi‘, ‘orange‘, ‘banana‘, ‘pear‘]
print(my_list) # 输出: [‘kiwi‘, ‘orange‘, ‘banana‘, ‘pear‘]
现在,使用赋值语句将索引1处的元素‘orange‘替换为‘apple‘。
my_list[1] = ‘apple‘
应用上述语义规则:
- 右侧表达式‘apple‘被求值。
- 左侧表达式
my_list被求值,得到列表对象。 - 方括号内的表达式
1被求值。 - 主对象是列表,属于可变序列。
- 索引
1是有效索引。 - 列表索引1处的元素被重新绑定为‘apple‘。
最后,显示修改后的列表以查看结果。
print(my_list) # 输出: [‘kiwi‘, ‘apple‘, ‘banana‘, ‘pear‘]
可以看到,列表索引1处的元素已成功从‘orange‘替换为‘apple‘。
可变类型与不可变类型
并非所有序列类型都允许修改其元素。例如,字符串的元素就不能被修改。列表允许修改元素,如我们刚才所做的。
以下是两种类型的关键定义:
- 可变类型:对象的值可以被改变的类型。列表是可变类型,因为可以通过替换、添加或删除元素来改变列表对象的值。
- 不可变类型:对象的值不能被改变的类型。字符串和元组是不可变类型。
例如,给定一个元组:
my_tuple = (‘a‘, ‘b‘, ‘c‘)
如果尝试使用下标赋值来修改元组的元素,Python会报告类型错误。
my_tuple[1] = ‘z‘ # 错误: ‘tuple‘ object does not support item assignment
字符串和元组不能添加、删除或替换元素。
向序列添加元素
append方法可用于向可变序列的末尾添加一个元素。该方法接收两个参数:第一个是特殊参数self(即序列本身),第二个是传递给方法的参数,即要添加到序列末尾的元素。
例如,我们可以向my_list列表的末尾添加一个浮点数43.2。

my_list.append(43.2)
print(my_list) # 输出: [‘kiwi‘, ‘apple‘, ‘banana‘, ‘pear‘, 43.2]
如果对元组使用append方法,操作会失败,因为不能对任何不可变类型应用append方法。
my_tuple.append(‘d‘) # 错误: ‘tuple‘ object has no attribute ‘append‘
元组和列表类型之间的唯一区别就是:元组是不可变的,而列表是可变的。此外,int(整数)、float(浮点数)和bool(布尔)类型也是不可变的。你不能改变这些类型对象的值。
总结
本节课中我们一起学习了如何使用支持下标表达式的赋值语句来修改序列(如列表)中的元素。我们详细区分了可变类型(如列表,其值可被修改)和不可变类型(如字符串、元组、整数、浮点数、布尔值,其值不可被修改)。理解这一区别对于有效且正确地操作Python中的数据结构至关重要。
Python编程与问题解决:06:for循环语句 🚀

在本节课中,我们将学习一种新的语句——for语句,它通常被称为for循环。for循环用于将一组语句重复执行确定的次数。
概述
for语句常用于处理序列中的每个元素。序列可以是一个列表、字符串或其他可迭代对象。在许多情况下,我们需要对序列中的每个元素执行相同的操作,for循环为此提供了简洁的解决方案。
for语句的基本结构
for语句是一种复合语句。其最简单的形式是将一个目标标识符依次绑定到序列的每个元素上。
以下是for语句的简化语法图:
for 目标标识符 in 表达式:
语句组
它由以下部分组成:
for:关键字。- 目标标识符:一个变量名。
in:关键字。- 表达式:一个计算结果为序列对象的表达式。
::冒号。- 语句组:需要重复执行的代码块,其语法与
if语句的语句组相同。
for语句的执行语义
for语句的执行遵循以下步骤:
- 计算表达式:对
in后面的表达式进行求值,得到一个结果对象。 - 检查序列:如果结果对象不是一个序列,则报告错误。
- 循环执行:将目标标识符依次绑定到序列的每一个元素上,并针对每一次绑定,执行一次语句组。
示例程序分析
让我们通过一个具体的程序来理解for循环。这个程序分析一个给定的单词序列,并根据单词的长度打印出对每个单词的看法。
word_list = ["Python", "chair", "Edmonton"]
for word in word_list:
if len(word) < 6:
print("I do not like the word " + word)
else:
print("I like the word " + word)
程序执行过程如下:
- 初始化:变量
word_list被绑定到一个包含三个字符串的列表对象["Python", "chair", "Edmonton"]。 - 进入
for循环:- 第一次迭代:
word被绑定到"Python"。len("Python")等于6,不小于6,因此if条件为假,执行else子句,打印"I like the word Python"。 - 第二次迭代:
word被绑定到"chair"。len("chair")等于5,小于6,因此if条件为真,执行if子句,打印"I do not like the word chair"。 - 第三次迭代:
word被绑定到"Edmonton"。len("Edmonton")等于8,不小于6,因此if条件为假,执行else子句,打印"I like the word Edmonton"。
- 第一次迭代:
- 循环结束:序列中的所有元素都已处理完毕,程序执行完毕。
错误示例
上一节我们看到了for循环如何正确工作。本节中我们来看看如果表达式的结果不是序列会发生什么。
如果我们错误地将一个整数作为for循环的序列,例如:
for word in 27: # 错误!27不是序列
print(word)
根据for语句的语义:
- 表达式
27被求值,得到整数对象27。 - 整数对象
27不是一个序列,因此Python会报告一个错误,提示‘int‘ object is not iterable(int对象不可迭代)。
重要提示:目前,我们只需确保for语句中in后面的表达式计算结果为序列对象(如列表、字符串)。虽然存在一些“可迭代”的非序列对象,但现阶段我们暂不深入讨论。
总结

本节课中我们一起学习了for语句。for循环是一种强大的工具,用于将一组操作重复执行确定的次数,特别适用于遍历序列中的每个元素。我们掌握了它的基本语法、执行语义,并通过实例和错误分析加深了理解。记住,for循环的核心是 for 变量 in 序列: 这个结构。
052:程序破解版本4开发教程 🎮

概述
在本节课程中,我们将基于之前完成的算法设计,开始编写“程序破解”游戏的第四个版本。我们将学习如何将算法步骤转化为实际的Python代码,重点是利用for循环和列表来消除代码重复,使程序结构更清晰、更易于维护。
上一节我们完成了版本4的详细算法设计。本节中,我们来看看如何将这些设计步骤逐一实现为可运行的代码。
从复制代码开始
设计完成后,即可开始编码。我已将“破解版本3”的代码复制并另存为“破解版本4”。
我还为算法中的每个步骤添加了注释。当你开始编写此版本时,也必须复制“破解版本3”的代码,并为新的算法步骤(包括控制结构)添加注释。
例如,控制密码列表的for图标,可以转化为注释 # for password in password_list。
当你编写控制结构的头部代码时,应移除这些注释。你可能还需要添加额外的注释来阐明你的程序。
实现第一个for循环
算法中每一个确定的重复结构,在你的代码中都将成为一个for语句。
我将首先创建用于显示标题头的for语句。为此,我需要一个供for语句迭代的序列。
我将创建一个包含所有三个标题字符串(包括空行)的列表。
如果你不记得for语句或列表类型,应该复习一下关于for语句和列表的课程。
我将创建一个名为header的新标识符,并将其绑定到标题列表。现在,我可以通过编写 for header_line in header: 这行代码,将算法中的for图标转化为代码。
复用并修改代码套件
for语句的代码套件可以从版本3中回收利用。用于显示每个标题和密码字符串的相同四行代码仍然适用,只需要稍作修改。
我将把旧代码中用于显示调试模式的部分放入套件中,并删除旧的显示标题代码的其余部分。
现在需要对这段代码做哪些修改?如果我现在运行程序,它会将“调试模式”字符串显示三次(针对header列表中的每个字符串各一次)。
而我想要显示的是列表中的每个字符串。因此,我将把drawstr调用中的“调试模式”字符串替换为目标标识符header_line。
现在当我运行程序时,它会显示每个header_line,更新窗口,暂停0.3秒,并为下一次迭代递增line_y。这与版本3的功能相同,但现在是代码自身在重复,而不是你通过编写多个相邻的重复语句来手动重复。
完成程序并优化
添加其他for语句来完成这个程序。不要忘记使用下标从密码列表中选择密码。
检查是否有任何重复的字面量(例如暂停时间0.3、X坐标0或正确密码“hunting”)出现超过一次。如果有,请将一个标识符绑定到该字面量。
总结

本节课中,我们一起学习了如何将设计好的算法转化为结构化的Python代码。我们重点实践了使用for循环和列表来封装重复操作,从而提升代码的简洁性和可读性。记住,在编码过程中,及时用标识符替代重复出现的字面量,是编写高质量、易维护程序的重要习惯。
053:版本4代码回顾与软件质量测试 🧐

在本节课中,我们将回顾“黑客”游戏版本4的代码,并学习如何使用调试器追踪for循环语句。我们还将介绍两个新的软件质量测试:字面量测试和重复代码测试。
代码回顾:使用调试器追踪循环
上一节我们完成了版本4的算法设计,本节中我们来看看如何通过调试器来验证循环的工作流程。
首先,在显示标题的第一个for语句旁设置一个断点,然后按下调试按钮。
# 设置断点的示例位置
for header_line in header: # <-- 在此处设置断点
# ... 显示标题的代码
游戏窗口会出现,但内容是空的。因为按下了调试按钮,调试器会在断点处暂停,并高亮显示for语句所在行。
按下“单步跳过”按钮,进入for语句,调试器会高亮显示将绘制第一个标题字符串的draw_string函数调用。

此时,在栈数据面板中,标识符header_line已被添加,并绑定到列表header的第一个元素"DEBUG MODE"。展开header列表,可以看到它包含三个元素:"DEBUG MODE"、"1 ATTEMPT(S) LEFT"和一个空字符串""。这证实列表已正确初始化,长度为3,且索引从0到2。

继续按“单步跳过”按钮,直到执行完y坐标的赋值语句,"DEBUG MODE"会显示在游戏窗口中。
当在if语句末尾按下“单步跳过”时,调试器会跳转到if之后的下一条语句。但在此处,按下按钮后,调试器会返回到for语句的顶部,并再次高亮for header_line in header:。这样,目标标识符header_line就可以被重新绑定到header列表的下一个元素。
再次按下“单步跳过”,栈数据中的header_line已更新为"1 ATTEMPT(S) LEFT"。调试器对于缓慢追踪循环、观察标识符如何变化非常有用。
继续按几次“单步跳过”,直到header_line被绑定到空字符串"",并且y赋值语句再次被高亮。

当再次按下“单步跳过”时,调试器会最后一次返回到for语句顶部。即使我们知道列表中没有更多元素了,解释器仍会检查列表中是否还有元素。再次按下按钮后,for语句块之后的第一行代码(用于创建密码列表)会被高亮。

循环追踪的快捷方式
接下来,我们在显示密码的第一个for语句处设置另一个断点,以展示循环追踪的一个便捷快捷方式。
当断点设置在循环的任意一行时,按下调试按钮会追踪一次完整的循环迭代,并停在包含断点的同一行。这使得快速追踪整个循环迭代变得容易。
按下调试按钮后,两个标识符被添加到栈数据中:password(当前绑定到"PROVIDE")和password_list(绑定到一个长度为14的列表)。
多按几次调试按钮,可以看到password被重新绑定到列表中的下一个字符串,然后显示在窗口中。
关于使用追踪来理解for语句的内容就介绍到这里。
版本4的软件质量测试


接下来,我们讨论为此版本添加的两个软件质量测试。
1. 字面量测试
以下是字面量测试的要求:
- 每个具有共同意图的字面量(除了
0、1、2、-1、0.0和空字符串"")在程序中应恰好出现一次。 - 允许使用标识符代替这些特殊值,但不是必须的。
例如,在我们的代码中,我们用标识符line_x替换了字面量0,使其意图更清晰。我们保留了用于计算一半距离的字面量2,因为除以2很常见。而0和1作为列表起始索引的常见用法通常不会被替换。
之前我们讨论过,如果不小心,查找和替换字面量可能会引入错误。我们也讨论了在整个程序中更改一个字面量的多个实例是多么繁琐。
在你的代码中,请确保为每个使用超过一次的字面量绑定了标识符。你的代码中,每个具有特定意图的字面量应该只包含一个实例。这使你的代码更短,并且在更改字面量值时更不容易出错。
2. 重复代码测试


重复测试确保相邻的重复语句组已被重复控制结构(如循环)替换。这使你的代码更短、更易读、更易修改。
对于此版本,你应该有三个for语句,它们替换了版本3代码中的三组相邻重复语句:
- 一个替换了标题显示。
- 一个替换了密码列表显示。
- 一个替换了结果消息显示。
同时,请确保你的代码反映了之前版本的软件质量测试:
- 程序和代码块应有注释。
- 你的标识符名称应具有描述性。
- 你的代码应反映新算法的设计。
恭喜,你完成了“黑客”游戏的版本4!

本节课中我们一起学习了如何使用调试器逐步追踪for循环的执行过程,以理解标识符的绑定和变化。我们还介绍了版本4需要通过的两种软件质量测试:字面量测试旨在减少重复字面量,提高代码可维护性;重复代码测试强制使用循环结构消除重复代码,使程序更简洁高效。确保你的代码满足这些要求,并为后续开发打下坚实基础。
054:Hacking版本4的解决方案问题

在本节中,我们将分析Hacking游戏第4版中存在的解决方案问题,并探讨在第5版中如何通过引入新的控制结构和数据抽象技术来改进它们。
🎼 版本5的新特性概述
Hacking版本5将为游戏添加新功能。在编写代码之前,你需要创建一个描述、一个功能测试计划和一个算法。
首先,我们来讨论一下在这个版本中应该改进的解决方案问题。
版本4中的问题与改进方向
单一猜测的限制
在一个关于猜测密码的游戏中,只允许一次猜测是相当令人遗憾的。在版本5中,你将添加使用新的重复控制结构——while语句——来输入多个密码猜测的功能。
引入While循环
while语句是第二种循环类型,你将在课程的其余部分广泛使用它。它的作用与for循环类似,都是重复执行一系列步骤。
在之前的模块中,我们使用for循环来实现确定次数循环。回想一下,确定次数循环会遍历一个具有固定长度的显式序列的元素。for循环总是使用确定次数循环,因为它需要一个可迭代对象。
相比之下,while循环使用不确定次数循环,因为它使用一个条件而不是一个可迭代对象。只要其条件为真,while循环就会执行其步骤。你将在接下来的编程语言视频中了解更多关于while循环的知识。
确定次数循环和不确定次数循环都是控制抽象问题解决技术的例子。
重复描述与数据抽象
注意,你之前的描述包含了重复的属性,例如“它是绿色背景黑色文字”和“它使用小字体”。这是一个重复自己的例子,这会导致描述更长且更难修改。
相反,你应该将不同对象的共同属性分组在一起。例如,描述可以包含一个对象“每个文本对象”,来指代窗口中出现的所有文本。这个对象的属性将是“绿色背景黑色文字”和“使用小字体”。这样,这些共享属性就不需要为每个单独的显示对象进行描述。
将共同的对象属性分组到一个单元中,是一种新型抽象——数据抽象——的例子。
有多种方法可以对共同属性进行分组。我们已经使用数据抽象将单个对象的所有属性分组在一起,例如标题或结果的所有属性。这允许我们使用单个名称来描述、测试或引用整个标题或结果对象。
然而,当不同的对象具有共同属性时,我们也希望使用数据抽象来为这些共享属性赋予一个通用名称。例如,狗和猫有共同的属性,比如吃肉。因此,我们可以创建一个名为“食肉动物”的术语来包含这些共同属性。
由于所有显示对象共享它们的颜色和字体大小,我们在描述中引入“每个文本对象”作为一个显式的公共对象。这样,我们就可以将共享的颜色和字体属性移动到这个单一的公共对象中。
现在,观察包含了多次猜测功能的Hacking版本5。

总结

在本节中,我们一起学习了Hacking游戏第4版解决方案中的两个主要问题:单一猜测的限制和描述中的重复。我们探讨了如何通过引入while循环来实现不确定次数循环,从而允许多次猜测。同时,我们学习了数据抽象的概念,通过将共同属性(如颜色和字体)分组到“每个文本对象”这样的抽象单元中,可以使描述更简洁、更易于维护。这些改进为版本5的开发奠定了基础。
055:观察黑客攻击游戏版本5 🔍
在本节中,我们将一起分析“黑客攻击”游戏的第五个版本。我们将重点关注此版本与之前版本的核心区别,特别是游戏机制中引入的“尝试次数”限制功能。

概述
“黑客攻击”游戏版本5在用户交互和游戏反馈方面做出了重要改进。最显著的变化是游戏界面现在会动态显示玩家剩余的密码猜测尝试次数。这个机制增加了游戏的策略性和紧张感,玩家不再拥有无限次尝试机会。
游戏界面与机制变化
上一节我们介绍了版本4的游戏流程。本节中,我们来看看版本5引入的新元素。
首先,游戏窗口的标题栏(Header) 现在会显示一个计数器,用于指示玩家剩余的尝试次数。在游戏开始时,这个数字被设定为 4。这意味着玩家现在有四次机会来猜测正确的密码,而不是之前版本中的单次机会。
以下是游戏初始状态的逻辑描述:
max_attempts = 4
attempts_left = max_attempts
游戏流程演示
密码列表和密码输入提示的显示方式与之前的版本保持一致。然而,当玩家输入一个错误的密码时,游戏的行为发生了关键变化。
- 系统会显示一个新的密码输入提示,允许玩家再次尝试。
- 标题栏中显示的剩余尝试次数会减少1。
例如,在第一次猜测错误后,剩余尝试次数会从4变为3。其逻辑可以用以下公式表示:
attempts_left = attempts_left - 1
如果玩家继续输入错误的密码,这个过程会重复。每次错误猜测后,剩余尝试次数都会递减,并且游戏窗口的右下角会显示一条新的“锁定警告”信息,提醒玩家尝试次数正在减少。
尝试次数耗尽与游戏结果
当剩余尝试次数减少到1时,玩家只剩下最后一次猜测机会。此时,锁定警告信息会变得更加醒目,提示玩家谨慎操作。
如果玩家在最后一次尝试中输入了正确的密码,游戏会像版本3一样,显示成功的结局画面和消息。反之,如果所有尝试次数都用尽仍未猜中密码,游戏则会显示失败的结果。
以下是判断游戏结束的核心逻辑:
if password_guess == correct_password:
display_success_outcome()
elif attempts_left == 0:
display_failure_outcome()
练习与观察建议
为了充分理解版本5的游戏机制,建议您亲自运行游戏并进行多次尝试。
以下是您可以进行的测试组合:
- 尝试连续输入错误密码,观察尝试次数的减少和锁定警告的出现。
- 在剩余不同尝试次数时输入正确密码,观察游戏的成功流程。
- 尝试耗尽所有四次机会,触发游戏失败结局。
请注意观察,每次不成功的猜测后,剩余尝试次数都会准确递减,并且最终会根据猜测结果显示相应的成功或失败提示信息。
总结

本节课中我们一起学习了“黑客攻击”游戏版本5的核心更新。我们了解到,该版本通过引入有限尝试次数机制(初始为4次)显著改变了游戏体验。标题栏的动态计数器、每次错误猜测后的次数递减以及新增的锁定警告信息,共同构成了一个更完整、更具挑战性的游戏反馈循环。这一机制为后续更复杂的游戏逻辑设计奠定了基础。
056:描述黑客游戏第五版 🔐

在本节课中,我们将学习如何为支持多次密码猜测的黑客游戏第五版构建描述。我们将分析游戏可能出现的四种不同行为路径,并使用数据抽象来简化描述过程。
构建描述框架
欢迎回到黑客游戏第五版的描述构建环节。
既然游戏现在支持多次密码猜测,我们需要一个与之匹配的描述。该描述必须能够描述出对应不同密码猜测组合的多种游戏路径。
游戏行为分析
对于任何猜测组合,游戏都可能出现四种不同的行为。你将使用一个选择结构对象来描述这四种行为中的每一种。
正如在解决方案讨论中提到的,你应该使用数据抽象来分组一些共有的对象属性。
以下是需要寻找的共有属性:
- 所有密码猜测共享的一个共有属性。
- 所有显示对象共享的两个共有属性。
总结

本节课中,我们一起学习了如何为支持多次输入的黑客游戏第五版构建描述框架。我们明确了需要描述的四种核心游戏行为,并引入了数据抽象的概念来寻找和分组对象间的共有属性,这将帮助我们更高效、清晰地构建游戏描述。
057:为黑客游戏第5版创建测试计划


欢迎回到黑客游戏第5版的测试计划构建器。
在本节课中,我们将学习如何为更复杂的程序版本创建测试计划,重点关注识别和测试“边界情况”。我们将以黑客游戏第5版为例,详细说明如何设计涵盖多种路径的测试。
概述
之前,我们为第3版和第4版设计的测试计划主要测试了游戏中的两条路径。然而,对于第5版,情况变得更加复杂。根据版本描述,程序现在需要处理因多次猜测尝试而产生的四种不同行为。因此,我们的测试计划需要覆盖更多路径,特别是那些只在特定条件下发生的“边界情况”。
什么是边界情况测试?
测试那些可能只在非常特定情况下发生的行为,被称为边界情况测试。
通常,边界情况是指当某个参数(例如尝试次数)取最小值或最大值时,程序所表现出的行为。
为了更清晰地理解,让我们看一个计算器应用的例子。测试一个通用行为,比如用100除以20,是常规测试。我们还需要测试结果不是整数的情况,以及分子或分母为负数的场景。然而,最有趣的边界情况是除以零。
由于除以零的结果是未定义的,这被视为一个需要单独、独特测试的边界情况。对此测试的适当响应应该是显示错误或警告信息,而不是给出一个错误的数字或导致程序崩溃。
识别黑客游戏的边界情况
接下来,我们将使用基于案例的分解方法,来识别黑客游戏中的一些独立边界情况。
以下是几个关键的边界情况:
- 输入空字符串:因为空字符串包含零个字符,这是字符串长度的“最小值”。
- 输入非常长的字符串:虽然字符串没有明确的“最大”字符数限制,但我们可以测试一个非常长的字符串来观察程序行为。
- 输入正确密码但使用小写字母:游戏密码可能是区分大小写的。要了解程序在此情况下的正确行为,你应该实际运行程序进行观察。
构建第5版测试计划
现在,我们来看看为第5版构建测试计划的具体要求。
你的测试计划必须运行程序三次,包括两次成功运行和一次失败运行。
以下是三次运行的具体设计:
- 第一次成功运行:使用密码列表中提供的一个错误答案,然后输入一个正确答案。
- 第二次成功运行:先使用三个错误的边界情况(例如空字符串、超长字符串、小写密码),然后输入正确答案。
- 失败运行:进行四次尝试,每次尝试都使用一个错误的边界情况。
总结
本节课中,我们一起学习了边界情况测试的概念及其重要性。我们以黑客游戏第5版为例,实践了如何识别边界情况(如空输入、超长输入、大小写问题),并据此构建了一个包含三次不同程序运行的详细测试计划。这个计划旨在确保程序在各种极端和特殊输入下都能做出正确的响应。


058:创建算法(黑客游戏版本5)

在本节课中,我们将学习如何在游戏创建过程中,为黑客游戏的第五个版本设计算法。我们将重点引入一种新的重复控制结构——不定次重复,以处理玩家拥有多次猜测机会的情况。
概述
在之前的版本中,我们使用了定次重复,这要求我们在执行循环前就知道需要重复的次数。然而,在版本5中,玩家最多有四次机会猜测密码,但我们无法预知玩家会在第几次猜中。因此,我们需要一种更灵活的重复结构。本节我们将学习不定次重复(也称为不定次迭代),它允许我们在某个条件为真时,重复执行某个步骤。
从定次重复到不定次重复
上一节我们介绍了定次重复。本节中我们来看看不定次重复。定次重复要求在执行前明确知道重复次数,例如“搅拌面糊20次”。但在许多情况下,我们无法预知次数,例如“搅拌面糊直到没有结块”。这时就需要使用不定次重复。
在算法构建器中,不定次重复由一个 while 图标表示。它由一个常规矩形(放置要重复的步骤)和一个条件判断图标(放置循环条件)通过箭头连接组成。
其核心逻辑可以用以下伪代码描述:
while (条件为真):
执行步骤
这意味着,只要条件保持为真,步骤就会一直重复执行。如果初始条件就不为真,那么循环内的步骤一次也不会执行。
设计版本5的猜测逻辑
现在,我们将不定次重复应用到黑客游戏版本5的算法中。在之前的版本中,算法流程是:提示玩家猜测,然后根据猜测正确与否显示结果。在版本5中,对于正确猜测的行为保持不变。但对于错误猜测,我们需要处理一系列新的事情。
以下是错误猜测后需要发生的步骤列表:
- 显示剩余尝试次数并将其减1。
- 提示玩家进行新的猜测。
- 如果玩家只剩最后一次尝试,则显示警告信息。
我们需要让以上这一系列步骤,在玩家猜测错误且尚未用尽四次尝试的情况下,重复执行。
开始构建算法
要开始构建算法,请将原来的“提示输入猜测”步骤替换为一个名为“获取猜测”的步骤。然后,展开“获取猜测”的面板,在其中使用不定次重复结构来组织逻辑。
你需要在展开的面板中重新加入“提示输入猜测”的步骤。构建时,请始终参考你的游戏描述和功能测试计划。如果遇到困难,可以回顾课程资料或在课程论坛中提问。
总结


本节课中,我们一起学习了不定次重复控制结构。我们了解到,当重复次数无法预先确定,而是取决于某个运行时的条件时,就应该使用 while 循环。我们分析了黑客游戏版本5的需求,即玩家拥有最多四次猜测机会,并据此开始设计使用不定次重复来管理多次猜测流程的算法。掌握这一概念,将使你能够处理更复杂、更动态的程序逻辑。
059:while语句详解 🔄

在本节课中,我们将学习一种新的控制结构——while语句。它是一种重复执行代码块的方式,但与之前学过的for语句不同,while语句的重复次数不是由序列长度决定的,而是由一个布尔表达式控制的。我们将通过一个具体的程序示例来理解其工作原理。
从for语句到while语句的过渡
上一节我们介绍了for语句,它可以遍历一个序列(如单词列表),并对每个元素执行一次操作。然而,有些场景下我们无法预先知道需要处理多少个元素。例如,我们希望程序能持续检查用户输入的单词并给出评价,直到用户主动要求停止。
for语句适用于已知序列的情况,而while语句则适用于这种“不确定重复次数”的场景。它通过检查一个条件表达式的真假来决定是否继续执行。
while语句的语法结构
while语句是一种复合语句。其语法结构如下:
语法图核心概念:
while 表达式:
语句组
- 头部:以关键字
while开始,后跟一个表达式和一个冒号:。 - 语句组:与
if和for语句类似,是一个缩进的代码块,包含需要重复执行的语句。
程序示例:交互式单词评价器
以下是一个使用while语句的程序。它会不断请求用户输入单词,并根据单词长度给出“喜欢”或“不喜欢”的评价,直到用户输入一个空字符串(直接按回车键)为止。
word = input()
while word != "":
if len(word) < 6:
print("I do not like the word " + word)
else:
print("I like the word " + word)
word = input()
运行示例:
- 输入
grape,输出:I do not like the word grape - 输入
banana,输出:I like the word banana - 输入
(直接按回车),程序结束。
while语句的执行语义(工作原理)
while语句的执行遵循一个明确的流程,我们称之为“不定重复控制结构”,因为无法提前预知其语句组会被执行多少次。
以下是其简化的执行步骤:
- 评估条件:计算
while头部表达式的值,得到一个结果对象。 - 判断与执行:
- 如果该结果对象被解释为
True,则执行while的语句组(代码块)。执行完毕后,返回第1步。 - 如果该结果对象被解释为
False,则跳过整个while语句的语句组,继续执行程序后面的代码。
- 如果该结果对象被解释为
让我们将这个流程应用到上面的程序示例中,跟踪其执行过程:
- 第一轮循环:
word初始值为"grape"。- 步骤1:条件
word != ""评估为True(因为"grape"不等于空字符串)。 - 步骤2:条件为真,执行语句组。
len("grape")为5,小于6,执行if分支,打印"I do not like the word grape"。- 执行
word = input(),等待用户输入。假设用户输入"banana"。
- 返回步骤1。
- 第二轮循环:
word现在为"banana"。- 步骤1:条件
"banana" != ""评估为True。 - 步骤2:执行语句组。
len("banana")为6,不小于6,执行else分支,打印"I like the word banana"。- 执行
word = input(),等待用户输入。假设用户直接按回车,输入空字符串""。
- 返回步骤1。
- 结束循环:
word现在为""。- 步骤1:条件
"" != ""评估为False(因为空字符串等于空字符串)。 - 步骤2:条件为假,跳过整个
while语句的语句组。循环结束,程序执行完毕。
总结
本节课中,我们一起学习了一种新的重复控制结构——while语句。
- 核心作用:它用于在条件为真时重复执行一段代码,适用于重复次数不确定的场景。
- 关键区别:与
for语句遍历固定序列不同,while语句由一个布尔表达式控制循环。 - 工作流程:遵循“评估条件 -> 真则执行并返回 -> 假则跳出”的循环逻辑。
- 重要应用:常用于处理交互式输入、监控状态变化或执行需要满足特定条件才停止的任务。

通过理解while语句的语法和语义,你现在可以编写能够动态响应用户输入或程序状态变化的更灵活的程序了。
060:循环示例与range类型

在本节课中,我们将通过具体示例深入探讨for循环和while循环的应用,并介绍一种新的序列类型——range。我们将学习如何利用循环处理列表数据,以及如何使用range对象配合索引来修改列表中的元素。
循环语句示例回顾
上一节我们介绍了for循环和while循环的基本概念。本节中,我们来看看如何将它们应用于更实际的编程场景。
首先,回顾一个之前的程序。该程序会遍历一个单词列表,并根据每个单词的长度输出不同的评价。
# 示例:基于单词长度的评价
words = ["computer", "apple", "python"]
for word in words:
if len(word) > 6:
print(f"I like the word '{word}'.")
else:
print(f"I do not like the word '{word}'.")
在关于while循环的课程中,我们看到了一个允许用户自行输入单词并进行评价的程序。
构建动态单词列表
以下是一个结合了之前两个程序功能的新程序。它允许用户输入任意数量的单词,将它们保存到一个列表中,然后使用for循环分析这个列表。
# 动态构建列表并分析
word_list = [] # 创建一个空列表
word = input("Enter a word (or press Enter to finish): ")
while word != "":
word_list.append(word) # 使用append方法将单词添加到列表末尾
word = input("Enter a word (or press Enter to finish): ")
print("Your word list is:", word_list)
for word in word_list:
if len(word) > 6:
print(f"I like the word '{word}'.")
else:
print(f"I do not like the word '{word}'.")
这个程序的关键新语句是第5行对列表方法append的调用。此外,第1行创建了一个空列表,而不是固定的单词列表。第8行是一个熟悉的print调用,用于显示列表内容。
运行程序时,它会提示输入单词。用户可以持续输入,直到输入一个空字符串为止。然后,程序会根据每个单词的长度显示“喜欢”或“不喜欢”的评价。
在之前的while循环示例中,程序虽然绑定了每个输入的单词,但在下一次循环迭代中,前一个单词就被覆盖了。使用append方法则允许我们将所有输入的单词保存到列表中。
核心概念:在创建对象列表时,常见的做法是:
- 从一个空列表开始。
- 使用
append()方法一次添加一个新元素。
修改列表元素的尝试
现在,我们来看两个程序(尝试一和尝试二)。每个程序都使用while循环输入一个单词列表并打印它,然后尝试将列表中的每个单词替换为其大写形式,并打印修改后的列表。
两个尝试都使用了for循环和字符串方法upper()。哪个程序会成功呢?
尝试一的代码如下:
# 尝试一:直接修改循环变量
word_list = []
word = input("Enter a word: ")
while word != "":
word_list.append(word)
word = input("Enter a word: ")
print("Original list:", word_list)
for item in word_list:
item = item.upper() # 试图修改循环变量item
print("Modified list (Attempt 1):", word_list)
运行尝试一,输入“computer”和“Apple”后,程序会显示两个相同的列表。尝试一未能将单词转换为大写。
尝试二的代码如下:
# 尝试二:通过索引修改列表元素
word_list = []
word = input("Enter a word: ")
while word != "":
word_list.append(word)
word = input("Enter a word: ")
print("Original list:", word_list)
for index in range(0, len(word_list)):
item = word_list[index]
word_list[index] = item.upper() # 通过索引直接修改列表元素
print("Modified list (Attempt 2):", word_list)
再次运行并输入相同的单词,这次单词列表包含了大写单词。尝试二成功了。
语义分析:为何尝试一失败?
让我们对两个程序中的for循环进行语义分析,看看为什么尝试一没有改变单词,而尝试二成功了。
在尝试一的for循环中:
for item in word_list:将标识符item依次绑定到列表word_list中的每个元素(如“computer”、“Apple”)。- 在循环体内,执行
item = item.upper()。这会将标识符item重新绑定到新的大写字符串对象(如“COMPUTER”)。 - 然而,这并没有改变原始列表
word_list中的元素。item只是一个临时变量,指向列表元素的一个副本(对于不可变对象如字符串,可以理解为指向了同一个对象,但重新赋值后指向了新对象)。 - 循环结束后,打印
word_list,列表内容没有变化。
核心问题:直接对for循环中的迭代变量赋值,不会影响原始序列中的元素。

引入range类型
尝试二使用了一种新的序列类型——range,配合下标赋值,成功地更新了列表。
range是一个不可变的序列类型。最简单的range对象包含两个值之间的所有整数。我们可以使用range()函数来创建一个range对象。
range()函数接受两个整数参数,返回一个包含从第一个参数到(第二个参数-1)的整数序列的range对象。
公式:range(start, stop) 生成序列 [start, start+1, ..., stop-1]
例如:
my_range = range(3, 7)
# my_range 包含 3, 4, 5, 6
由于range是序列,其元素索引从0到长度-1,并且可以使用下标访问。例如,my_range[0] 返回索引0处的元素,即3。
range对象可以在for循环中用于遍历一个序列的索引,从而允许你读取或修改该序列的单个元素。
尝试二如何成功?
在尝试二的for循环中:
for index in range(0, len(word_list)):生成一个从0到len(word_list)-1的整数序列(例如,对于两个单词的列表,是[0, 1])。- 循环变量
index依次被绑定为0和1,即列表的索引。 - 在循环体内:
item = word_list[index]通过下标获取列表当前索引处的元素。word_list[index] = item.upper()通过下标赋值,直接将列表word_list在index位置的元素重新绑定为新的大写字符串对象。
- 这样,列表中的元素就被逐个、直接地修改了。
核心操作:sequence[index] = new_value 这种形式的下标赋值,是修改可变序列(如列表)中元素的有效方式。
总结
本节课中我们一起学习了:
- 如何使用
while循环和append()方法动态构建列表。 - 直接修改
for循环迭代变量无法改变原始列表元素。 - 引入了
range类型,它是一个不可变的整数序列。 - 掌握了通过
range()函数生成索引序列,并结合下标赋值(list[index] = value)来有效修改列表元素的方法。
理解range和下标操作是进行列表遍历和修改的基础,在后续处理更复杂的数据结构时非常有用。
061:编程实现黑客小游戏版本5 🔐
在本节课中,我们将学习如何为黑客小游戏实现版本5。这个版本的核心改进是允许玩家进行多次猜测尝试,而不是仅有一次机会。我们将通过引入循环控制结构和变量来管理尝试次数,并学习如何在屏幕上动态更新和显示相关信息。

概述
在之前的版本中,玩家只有一次猜测机会。本节我们将扩展游戏功能,允许玩家进行有限次数的尝试。这涉及到初始化尝试次数、在每次猜测后更新该次数、以及当尝试用尽时显示锁定警告。我们将使用循环结构来实现这一重复过程,并学习如何计算屏幕坐标来定位文本。
编程步骤详解
以下是实现版本5所需遵循的具体步骤。
1. 添加新注释
与往常一样,首先为程序中的每个新算法步骤添加新的注释,包括新的控制结构。
2. 绑定尝试次数标识符
由于此版本包含多次猜测尝试而非仅一次,我将把一个标识符绑定到初始尝试次数上。
attempts_left = 3 # 初始尝试次数
3. 更新标题字符串
我将更新引用尝试次数的标题字符串,使其使用这个标识符,而不是硬编码的字符串“一次尝试剩余”。
我需要创建一个新的字符串,将尝试次数与字符串“次尝试剩余”连接起来。为此,我将应用内置的 str() 函数将 attempts_left 对象从整数转换为字符串,以便它能与标题字符串连接。
header = str(attempts_left) + " 次尝试剩余"
4. 递减剩余尝试次数
每次玩家进行猜测后,我需要减少剩余的尝试次数。在第一次猜测之后,我将重新绑定 attempts_left 为 attempts_left - 1。
attempts_left = attempts_left - 1
5. 清理冗余注释
如果一个算法步骤可以翻译为一条单独的语句,并且该语句本身已足够具有描述性,那么你应在编写该语句后移除对应的注释。
例如,注释“递减尝试次数”现在已是冗余的,因此我将移除它。
6. 实现循环结构
版本5算法的每个“不确定重复”实例,在你的代码中都将成为一个 while 循环语句。在你实现多次尝试功能时,请牢记这一点。
while attempts_left > 0:
# 游戏主逻辑循环
7. 计算锁定警告的显示位置
你必须确定锁定警告在屏幕右下角的显示位置。使用屏幕和字符串的尺寸来计算这个位置。
假设屏幕宽度为 screen_width,字符串宽度为 text_width,那么右下角的x坐标可以这样计算:
warning_x = screen_width - text_width - margin # margin 为边距

总结
本节课中,我们一起学习了如何为黑客小游戏编程实现版本5。我们引入了变量来管理玩家的尝试次数,使用字符串拼接和类型转换来动态生成反馈信息,并通过循环结构控制了游戏的重复流程。最后,我们还探讨了如何根据屏幕尺寸计算文本的显示位置。掌握这些步骤后,你就能够创建一个允许有限次尝试、并能在尝试用尽时给出适当反馈的交互式游戏了。
062:版本5代码审查与软件质量检查

在本节中,我们将通过调试器追踪一个while循环的执行过程,并回顾版本5代码的软件质量要求。
概述
我们将使用调试器逐步执行“黑客”游戏版本5的代码,观察变量状态的变化和程序流程的控制。同时,我们将重申并检查代码应满足的软件质量标准。
代码追踪:使用调试器分析循环
以下是版本5的代码。我在while循环旁设置了一个断点,并通过按下调试按钮开始追踪。
游戏暂停,以便我输入第一个猜测。我将输入一个错误的答案。
追踪停止,调试器高亮显示了while循环的头部。
在堆栈数据中,你可以看到attempts_left从4减少到了3。
你还可以看到guess已被绑定到输入的字符串。
while循环的条件有两个表达式,作为and运算符的操作数。
我的猜测不等于“hunting”,因此第一个操作数的结果为True。
第二个表达式检查attempts_left是否大于0,结果为True。
and运算符计算True和True,得到True。因此,while循环的代码块将被执行。
当我按下“单步跳过”按钮时,下一个被高亮的语句是一个draw_string调用,它使用了X坐标line_x和Y坐标string_height。
你可以在堆栈数据中看到这些坐标的值。我将再次按下“单步跳过”按钮。
调试器高亮了if语句的头部。由于没有调用window.update,attempts_left的新值尚未在窗口中显示。
此代码中移除了window.update调用,以演示计算机图形学中的一个常见做法。
当你进行多个可以同时显示的draw_string调用时,你可以在它们之后跟随一个单独的update调用。
在本例中,window.update调用位于input_string函数内部,你稍后会看到这个更新。
if语句头部检查attempts_left是否等于1。由于attempts_left等于3,if条件评估为False。
因此,当我再次按下“单步跳过”按钮时,调试器跳过了if代码块,并高亮了下一个input_string函数调用。
我将再次单步跳过以执行input_string调用。
input_string调用内部的update函数显示了自上次更新以来绘制的所有内容,包括attempts_left。
现在,我将输入另一个错误答案,并继续单步跳过几次,直到调试器返回到while语句的顶部。
再次检查while条件。从堆栈数据中可以看到,guess不等于密码,因此条件中的第一个表达式为True。
由于剩余尝试次数为2,大于0,条件中的第二个表达式也为True。
因为True和True的结果是True,解释器将再次执行while循环的代码块。
我将按下调试按钮,并输入第三个错误答案。
你可以在堆栈数据中看到attempts_left为1。现在,我将单步执行代码,直到调试器到达if语句。
attempts_left等于1,因此if条件评估为True,接下来将执行if代码块。
我将按下“单步跳过”按钮,直到到达if代码块的最后一条语句。
命名空间中新增了3个标识符,并显示在堆栈数据中。
warning_string是锁定警告信息。warning_x和warning_y是该字符串的X和Y坐标。
为了返回到while循环头部,我将再次按下调试按钮,并输入另一个错误答案。

attempts_left现在等于0。
guess仍然不等于“hunting”,但attempts_left为0,不再严格大于0。
由于True和False的结果是False,while条件评估为False,因此该语句将不再执行。
我将按下调试按钮以完成程序的运行。
软件质量检查
在此版本的“黑客”游戏中,我们不会添加新的软件质量测试。然而,你应该借此机会确保你的代码通过已经见过的所有软件质量测试。

以下是需要检查的项目列表:
- 注释:你的代码应包含程序注释和块注释。
- 标识符命名:你的标识符应具有描述性,并使用小写字母和下划线分隔。
- 代码反映设计:你的代码应反映你的设计。每个算法框应转化为几行顺序代码,每个算法控制结构应转化为一个代码控制结构。
- 字面量使用:除了规定的例外情况,每个具有共同意图的字面量应只出现一次。
- 重复代码消除:最后,检查你是否已用重复控制结构替换了相邻的重复语句组。
总结

在本节中,我们使用调试器逐步追踪了“黑客”游戏版本5中while循环的执行,观察了条件判断和变量状态的变化。我们还回顾了确保代码质量的几个关键方面,包括注释、命名规范、设计与代码的一致性、字面量管理以及消除代码重复。恭喜你完成了“黑客”游戏的版本5。
063:版本5解决方案的问题与版本6介绍


在本节课中,我们将分析“黑客”游戏版本5解决方案中存在的代码质量问题,并介绍版本6的核心改进目标:通过创建用户自定义函数来提升代码质量。我们将学习函数如何帮助消除非相邻的重复代码,并理解解决方案泛化的概念。
🎼 欢迎来到黑客游戏版本6
你即将完成“黑客”游戏的开发。你的前一个版本只缺少两个功能:将密码嵌入随机符号中,以及在玩家猜测错误时提供提示。然而,在版本6中,你并不会实现这些新功能。与版本4类似,版本6只关注代码质量的提升。我们将引入一个对提高代码质量至关重要的语言特性。这个版本的核心是关于创建你自己的函数。
😊 用户自定义函数简介
你已经使用过许多函数,包括Python内置函数和从各种模块导入的函数。现在,你将创建自己的用户自定义函数。
函数是编程中非常重要的一部分。它们允许你将代码分解为自包含的片段。这使得代码更易于阅读、编写、修改和重用,因为它允许你一次专注于一个相关的代码段。你可以将使用内置函数和导入函数的所有好处,应用到使用你自己的函数上。
函数调用是控制抽象的一个例子。函数调用会执行函数内部的语句,然后返回到调用语句。调用函数时,你无需理解其代码的内部细节。函数的代码可以编写一次并调用多次,因此它是DRY(Don‘t Repeat Yourself)原则的应用。
版本5解决方案中的问题
以下是黑客游戏版本5的解决方案。请注意,程序中仍然存在用于显示字符串的重复代码段。
这组四个语句在程序中重复了三次,只有微小的差异。此外,用于输入字符串的重复的两个语句组也出现了两次。
你在版本4和版本5中无法移除这些重复,因为它们彼此不相邻。当你有一组执行相同功能但被其他不相关的代码行分隔开的语句时,你就有了非相邻的重复语句组。非相邻的重复语句组无法使用重复控制结构(如循环)来移除。因此,我们将用用户自定义函数来替换它们。


函数作为解决方案泛化

用户自定义函数是解决方案泛化的另一个例子。回想一下,解决方案泛化会改变解决方案,以便你可以在许多不同的上下文中重用它。

编写函数的人正在执行解决方案泛化。而调用函数的人正在使用控制抽象。
在版本5中,用于显示字符串的四行代码被使用了三次:一次用于标题,一次用于密码,一次用于结果。你将创建的函数之一,将把这12行代码转换成一个仅五行的函数,该函数可用于显示任何字符串。
版本6的开发任务
你不需要为版本6创建新的描述或功能测试计划。游戏的功能与前一版本完全相同。你也不需要创建新的算法。在算法层面,你的解决方案没有改变。你将直接进入编辑代码的阶段。

总结

本节课中,我们一起学习了版本5代码中存在的非相邻重复问题,并明确了版本6的改进方向:通过创建用户自定义函数来重构代码。我们理解了函数如何作为控制抽象和解决方案泛化的工具,能够将分散的重复逻辑封装起来,从而使代码更简洁、更易维护。在接下来的实践中,你将把重复的显示和输入代码块转化为可重用的函数。
064:Python函数定义 🧱

在本节课中,我们将学习如何定义自己的函数。我们将了解什么是用户定义函数,它与内置函数有何不同,并详细探讨函数定义语句的语法和语义。
概述 📋
到目前为止,我们已经使用了Python标准库中的许多函数,例如 print() 和 len()。然而,有时我们需要执行一些标准库中没有的特定操作。这时,我们就需要创建自己的函数,即“用户定义函数”。本节课将介绍如何使用 def 关键字来定义函数,并解释函数执行时Python解释器如何处理命名空间。
函数与程序块
一个完整的Python程序由“块”组成。Python中有三种类型的块:
- 模块:通常是一个
.py文件。 - 函数:可重用的代码块。
- 类:将在未来的课程中介绍。
解释器总是从主模块(__main__)开始执行,但其他块(如函数)也会在调用时被求值。
用户定义函数 vs. 内置函数
Python中的函数分为两种:
-
用户定义函数:其代码完全由Python语句编写。例如,标准库
copy模块中的copy函数。- 其类型为
function。
- 其类型为
-
内置函数:并非用Python实现,而是用实现Python解释器的语言(通常是C语言)编写的。这包括
builtins模块中的函数(如print),也包括其他标准库模块中用C实现的函数(如time.sleep)。- 其类型为
builtin_function_or_method。
- 其类型为
我们将采用更广泛的定义:所有非Python实现的函数都称为内置函数。
函数定义语句
函数定义是一种新的复合语句。以下是其简化的语法图:
def function_name():
# 语句套件
- 函数头:以关键字
def开始,后跟函数名(一个标识符)、一对括号()和冒号:。 - 参数列表:括号内是可选的参数列表。本节课我们只讨论无参数的函数。
- 套件:缩进的语句块,定义了函数被调用时要执行的操作。
命名空间与标识符解析规则
每个块(模块、函数)都有自己的“命名空间”,即存储其标识符(变量名、函数名)与对象绑定关系的地方。
绑定标识符的规则(简化版):
当在一个块中绑定标识符(如通过赋值 = 或函数定义 def)时,该绑定会存储在当前块的本地命名空间中。
解引用标识符的规则(广义版):
当需要获取一个标识符的值时,解释器按以下顺序查找:
- 在当前函数的本地命名空间中查找。
- 如果未找到,则在全局命名空间(即模块的命名空间)中查找。
函数定义语义详解
当我们执行一个函数定义语句 def banner(): ... 时,解释器会执行以下步骤:
- 创建函数对象:创建一个包含函数套件中代码的函数对象。该对象拥有自己的命名空间。
- 检查并添加名称:如果函数名(如
banner)不在当前本地命名空间中,则添加它。 - 绑定名称:将函数名
banner绑定到刚创建的函数对象上,绑定发生在当前本地命名空间(对于主模块,就是全局命名空间)。 - 链接全局命名空间:在函数对象的命名空间中,保存一个指向其所在全局命名空间的引用。这至关重要,它使得函数内部能够访问外部(全局)的标识符。
关键点:解释器在定义函数时不会执行函数体内的代码。代码只会在函数被调用时执行。
函数执行流程示例
让我们通过一个创建垂直横幅的函数来演示整个过程。
# 1. 定义函数
def banner():
for letter in word:
print(' ' * indent + letter)
# 2. 在主模块中设置变量
word = "blue"
indent = 3
# 3. 调用函数
banner()
# 4. 打印原单词
print(word)
执行步骤分析:
- 定义阶段:解释器遇到
def banner():时,按上述语义创建函数对象,并将名称banner绑定到它。此时不执行for循环。 - 变量赋值:执行
word = "blue"和indent = 3,这两个名称被绑定在主模块的全局命名空间。 - 函数调用:执行
banner()。- 解释器首先在主模块的命名空间中解引用
banner,找到对应的函数对象。 - 开始执行函数体内的代码。此时,本地块变为
banner函数,本地命名空间是banner的命名空间。 - 执行
for letter in word::- 解引用
word。在banner的本地命名空间中找不到word,于是通过保存的引用,到全局命名空间中查找,找到字符串"blue"。因此,word在banner函数内是一个全局标识符。 - 将
letter绑定到'b',此绑定存储在banner的本地命名空间。
- 解引用
- 执行
print(' ' * indent + letter):- 解引用
print。本地没有,在全局找到内置的print函数。 - 计算
' ' * indent:解引用indent,本地没有,在全局找到整数3。因此,indent也是一个全局标识符。 - 计算
... + letter:解引用letter,在banner的本地命名空间中找到当前字母'b'。 - 调用
print(' b')输出。
- 解引用
- 循环继续,为
'l','u','e'重复此过程。
- 解释器首先在主模块的命名空间中解引用
- 函数返回:函数执行完毕。由于没有
return语句,它返回一个None类型的对象。 - 清理与后续:函数调用结束后,在函数执行期间添加到其本地命名空间的所有标识符(如
letter)会被删除以释放内存。控制权返回主模块,执行最后的print(word)语句。
全局标识符的潜在问题与解决方案
在上面的例子中,banner 函数依赖于全局变量 word 和 indent。这被称为使用了全局标识符(在其他语言中常称为全局变量)。
过度依赖全局标识符可能导致代码难以理解和调试,因为函数的行为依赖于其外部的、可能被意外修改的状态。
更安全、更清晰的做法是通过参数将数据传递给函数。这样,函数的所有输入都变得明确。我们将在下一节课学习如何为函数定义参数。
总结 🎯
本节课我们一起学习了:
- 用户定义函数的概念,及其与内置函数的区别。
- 如何使用
def语句来定义自己的函数。 - Python程序的块结构和命名空间模型。
- 标识符的绑定与解引用在多个命名空间中的通用规则。
- 函数定义和调用时,解释器的详细执行步骤。
- 认识到在函数内部依赖全局标识符可能带来的问题,并了解到通过函数参数传递数据是更优的解决方案。

通过定义函数,我们可以将代码组织成可重用的模块,这是构建复杂程序的基础。下一节课,我们将学习如何定义带参数的函数,使它们更加灵活和独立。
065:Python函数参数 🧩

在本节课中,我们将学习如何为函数定义参数列表,以支持在函数调用时传递不同的参数。这将使我们的函数更加灵活和通用。
概述
函数参数允许我们在调用函数时传递不同的值,从而实现代码的复用。例如,内置函数 print 可以多次调用,每次传入不同的参数,如 print("blue") 和 print("red")。用户自定义函数同样支持这一特性。
我将修改横幅显示程序,使其能够通过参数为两个不同的单词显示横幅,并运行该程序。
程序运行成功。两个横幅分别显示,一个为“blue”,另一个为“red”。
为了在函数调用中包含参数列表,必须在函数定义中支持相应的参数列表。
函数定义语法回顾
首先,回顾一下函数定义的语法图。
这是一个简化的参数列表语法图。
banner 函数定义具有有效的语法。接下来,我将展示Python解释器如何解释这个程序。
程序执行过程详解
当程序启动时,局部块是主模块,其命名空间既是局部命名空间也是全局命名空间。
预绑定的标识符 print 绑定在此命名空间中。
主模块中的第一条语句是一个函数定义。
因此,解释器应用函数定义语义,与上一课相同。
解释器创建一个具有自己命名空间的函数对象。
将标识符 banner 添加到局部命名空间,并将 banner 绑定到该函数对象。
同时在 banner 命名空间中绑定一个指向全局命名空间的引用。
请记住,解释器在评估函数定义时,不会评估函数体。
在函数定义语句之后,解释器使用赋值语句语义评估第5至第8行。
在局部命名空间中,word1 被绑定到字符串 "blue"。word2 被绑定到字符串 "red"。
indent1 被绑定到整数 3,indent2 被绑定到整数 5。
然后,评估第9行的函数调用 banner(word1, indent1)。
支持参数和参数的函数调用语义
函数调用语义被修改以支持参数和形参。
在现有的步骤4之前添加了一个新的步骤4,以确保参数列表和形参列表长度相同。
原始步骤4的细节也被添加,成为新的步骤5。
步骤1: 在局部命名空间中解引用标识符 banner,以获取 banner 函数对象。
步骤2: 由于有两个参数表达式,它们被评估以获取两个参数对象:"blue" 和 3。这两个对象被放入一个新的参数对象列表中。
步骤3: 由于表达式对象 banner 不是方法对象,因此没有特殊参数被添加到参数对象列表中。
步骤4: 参数对象列表的长度为2,形参列表的长度也为2,因此不报告错误。
步骤5: 形参 word 被添加到 banner 命名空间,并绑定到参数对象 "blue"。然后,形参 indent 被添加到 banner 命名空间,并绑定到参数对象 3。
每个形参都是在函数调用期间绑定的局部标识符。
请注意,形参标识符 word 和 indent 与函数调用中使用的标识符名称不同。
在这个例子中,形参名称 word 和 indent 不可能同时匹配第一个函数调用中的标识符名称 word1 和 indent1,以及第二个函数调用中的 word2 和 indent2。
标识符名称无关紧要,因为形参标识符是按顺序绑定到参数对象的。
如果形参标识符改为 word2 和 indent2,这些标识符将在第一次函数调用期间被添加到 banner 命名空间,并绑定到 "blue" 和 3。
步骤6: 评估 banner 的函数体。此时局部块是 banner,局部命名空间是 banner 的命名空间。
与上一课类似,函数定义体中的单个语句使用四个语句语义。
步骤一: 评估标识符 word。在此程序中,banner 函数定义语句将标识符 word 作为形参绑定在 banner 的命名空间中。因此,解释器解引用 word 以获取对象 "blue"。
步骤二: 不报告错误。
步骤三: 在局部命名空间中,将标识符 letter 绑定到字符串对象 "blue" 在索引0处的元素,即字符串对象 "B"。

for 循环体第一次被评估。
banner 函数的评估继续,与上一课相同,只是形参 indent 是在 banner 命名空间而不是全局命名空间中解引用的。
字符串 "blue" 被打印为横幅。结果对象是唯一的 None 类型对象。
步骤6结束时,从 banner 命名空间中删除 letter、indent 和 word,因为它们是在函数调用期间添加的。
解释器返回到主程序,并评估下一条语句,即对 banner 的另一个函数调用。
这次调用的评估方式与上一次相同,只是这次在 banner 命名空间中,形参 word 绑定到参数对象 "red",形参 indent 绑定到参数对象 5。
字符串 "red" 被打印为横幅。返回相同的 None 类型结果对象,并且再次从 banner 命名空间中删除 letter、indent 和 word。
总结
在本视频中,我概括了函数定义的语法和语义,使其包含支持函数调用参数的参数列表。通过使用参数,我们可以编写更通用、可复用的函数,根据传入的不同值执行相应的操作。
066:Python主函数与标识符作用域 🐍

在本节课中,我们将学习一个重要的编程惯例——使用主函数(main function),并理解标识符作用域(Identifier Scope)的概念。这将帮助我们编写更安全、更易于维护的代码,避免因意外使用全局变量而导致的错误。
概述
上一节我们介绍了函数参数。本节中,我们来看看如果在函数定义中错误地使用了全局标识符会发生什么,以及如何通过引入主函数来防止这类错误。
从错误案例开始
回顾函数参数课程中的程序。如果在函数定义的for语句头部错误地使用了标识符word1,而不是参数word,会发生什么?
运行程序后,程序会显示两个横幅,内容都是“blue”,缩进分别为3和5。
# 错误示例:在banner函数内部错误地引用了全局变量word1
def banner(word, indent):
for i in range(indent):
print(" ", end="")
print(word1) # 错误:应为word,却写成了word1
word1 = "blue"
word2 = "red"
indent1 = 3
indent2 = 5
banner(word1, indent1)
banner(word2, indent2)
在两次调用banner函数时,解释器在求值for语句头部时,会解引用word1而不是word。由于word1在函数定义语句中未被绑定,它是一个全局标识符,解释器会在全局命名空间中解引用它,从而在两次调用中都获得字符串对象“blue”。
引入主函数惯例
许多有经验的Python程序员使用一个惯例来防止意外使用此类全局标识符:他们引入另一个用户自定义函数,称为main。
以下是使用主函数的相同程序:
def banner(word, indent):
for i in range(indent):
print(" ", end="")
print(word1) # 这里仍然有错误
def main():
word1 = "blue"
word2 = "red"
indent1 = 3
indent2 = 5
banner(word1, indent1)
banner(word2, indent2)
main()
新的main函数包含了之前主模块中的所有语句(除了banner函数定义)。word1、word2、indent1和indent2之前被绑定在主模块的命名空间中(这意味着它们可以在banner中被用作全局标识符),现在它们位于main函数的命名空间中,因此不能再在banner或任何其他用户定义函数中被意外引用。
我们称这个函数为main,因为它包含了之前主模块中的大部分语句。
主函数的一般规则
通常,你应该将所有语句从主模块移动到main函数中,除了以下语句:
import语句- 函数定义
- 类定义(将在后续课程介绍)
重要的是要认识到主模块和主函数是不同的。在上面的程序中,仍然存在一个主模块,它只包含两个函数定义语句(一个用于main,一个用于banner)。
如果直接运行上面的程序,你会发现没有输出。记住,函数定义不会执行其代码块内的语句,需要函数调用来执行main函数的代码块。
理解错误报告
在程序末尾添加对main()的调用并运行,会得到一个NameError,指出word1未定义。这个错误报告了我在banner内部意外使用了标识符word1。
这是一个有用的错误,因为它阻止了我使用一个标识符,而如果我没有使用主函数,这个标识符本应是一个全局标识符。通常,让解释器报告一个错误,比由于程序员的错误而让程序静默地计算出错误答案要好得多。
以下是错误产生的原因分析:
- 程序启动时,主模块的命名空间既是局部命名空间也是全局命名空间。
- 解释器依次处理主模块中的语句:定义
main函数,定义banner函数,然后调用main()。 - 执行
main()时,在其命名空间中绑定了word1、word2等变量。 - 当
main函数调用banner(word1, indent1)时,解释器开始执行banner函数的代码。 - 在
banner函数内部,解释器尝试求值表达式word1。由于标识符word1没有绑定在banner函数的局部命名空间中,解释器会去全局命名空间(即主模块的命名空间)中查找,但依然没有找到,因此报告NameError。
核心机制:将标识符word1从全局命名空间移动到main函数的命名空间中,防止了在banner函数中意外引用一个全局标识符。
标识符作用域的概念
编程语言使用称为作用域的概念来限制标识符的可见性。标识符绑定的作用域指的是在代码的哪些地方可以使用该绑定。由于Python代码被划分为块,因此作用域就是所有解引用该标识符时会使用该绑定的代码块。
例如,在上述使用主函数的程序中,word1绑定在main函数命名空间中,其作用域仅限于main函数块。它的作用域不包含banner函数块,因此,在banner函数块中引用word1会产生NameError。
而在之前没有主函数的程序中,word1绑定在主模块的全局命名空间中,其作用域既包括主模块块,也包括banner函数块。banner函数中对word1的引用会使用标识符的语义规则,从而意外地访问到一个全局标识符。
作用域与封装
在本课程中,我们将使用主函数来从全局命名空间中移除所有标识符(除了导入的标识符和用户定义的函数名)。标识符作用域是封装的一个例子。
封装是一种限制对组件实现访问的技术,使得该组件的实现独立于其他组件如何使用它。
考虑这个场景:你是一个新开发者,正在处理一个非常庞大的代码库。你想添加新功能,但已经有成千上万个标识符正在被使用。如果没有有限的作用域,如果你在你的函数中重新绑定了其中一个标识符,可能会对使用该标识符的其他函数和模块产生灾难性的影响。你将需要 meticulously 检查每一个现有的函数和模块,以确保你没有使用别人在其代码中使用的相同标识符。这将是令人不快且容易出错的。
因为作用域限制了标识符的可见性,你可以在你的函数中使用任何你想要的标识符,而不会干扰其他函数中的相同标识符。你将每一个标识符绑定从主模块移动到主函数,都减少了该标识符绑定的作用域,因此它不能在用户定义的函数中被意外访问。
总结
本节课中我们一起学习了:
- 使用主函数的惯例:将程序的主要逻辑放入一个名为
main的函数中,并在脚本最后调用它。这有助于将变量从全局作用域中隔离出来。 - 标识符作用域:一个变量(标识符)在程序中可以被访问到的区域。在函数内部定义的变量通常具有局部作用域。
- 封装的好处:通过限制标识符的作用域,我们可以避免命名冲突,使代码模块更独立,从而提高代码的可维护性和安全性。


通过采用主函数和正确理解作用域,你可以写出更健壮、更清晰的Python程序。
068:Python副作用 🎮

在本节课中,我们将要学习Python编程中的一个重要概念:副作用。我们将了解函数除了使用return语句返回值之外,如何通过修改可变对象来影响程序的其他部分。
概述 📋
函数通常通过return语句返回结果。然而,函数也可以通过修改传入的可变对象(如列表)来产生副作用,使得这些修改在函数外部可见。本节将通过一个具体的程序示例,详细解析副作用的工作原理及其语义。
副作用的基本概念
上一节我们介绍了函数通过返回值传递结果。本节中我们来看看函数影响程序的另一种方式:副作用。
副作用是指函数在执行过程中,修改了其作用域之外的对象状态。这通常通过修改作为参数传入的可变对象来实现。
以下是产生副作用的关键条件:
- 函数接收一个可变对象(如列表、字典)作为参数。
- 函数内部修改了这个对象的内容。
- 函数调用结束后,通过原始引用访问该对象时,能观察到其内容已被改变。
程序示例解析
现在,我们通过一个具体的程序来理解副作用是如何工作的。该程序统计一系列单词中大小写字母的数量。
def count_case(string, counts):
for letter in string:
if letter.islower():
counts[0] += 1
if letter.isupper():
counts[1] += 1
def main():
word_list = ["Hello", "wORLD", "123Ab"]
count_list = [0, 0]
for word in word_list:
count_case(word, count_list)
print(word, count_list)
main()
函数count_case的作用
count_case函数接收两个参数:一个字符串string和一个列表counts。
counts[0]用于累计小写字母数量。counts[1]用于累计大写字母数量。
函数遍历字符串中的每个字符,使用.islower()和.isupper()方法判断其大小写,并相应地对counts列表中的元素进行递增操作。这个函数没有return语句,它的作用完全体现在对传入的counts列表的修改上。
程序执行流程
在main函数中:
- 创建单词列表
word_list和计数器列表count_list(初始值为[0, 0])。 - 遍历
word_list中的每个单词。 - 对每个单词调用
count_case(word, count_list)。 - 每次调用后,打印当前单词和更新后的
count_list。
运行此程序,输出结果将展示count_list如何随着每个单词的处理而累积变化。
语义分析:理解副作用的发生
让我们深入程序的执行过程,看看副作用在命名空间层面是如何发生的。
当程序启动时,解释器创建主模块的命名空间。函数main和count_case被定义,它们的名称被绑定到相应的函数对象。
第一次调用count_case
当main函数第一次循环,调用count_case(“Hello”, count_list)时:
- 参数
string被绑定到字符串对象”Hello”。 - 参数
counts被绑定到main函数中count_list所引用的同一个列表对象。
在count_case函数内部执行counts[0] += 1时,发生以下关键步骤:
- 通过
counts这个引用,找到其指向的列表对象。 - 修改该列表对象中索引
0处的绑定(从整数0重新绑定为整数1)。
这个修改发生在列表对象自身的“内部命名空间”里。由于main函数中的count_list和count_case函数中的counts指向的是同一个列表对象,因此通过count_list访问该对象时,就能观察到索引0的值已经变为1。
这就是副作用:一个函数通过某个引用(counts)修改了对象,而这个修改通过另一个引用(count_list)在函数外部被观察到。
后续执行与清理
count_case函数结束后,其局部命名空间(string, counts, letter)被销毁,并返回None(被main函数忽略)。控制权回到main函数,它打印出已被修改的count_list。
main函数继续循环,对下一个单词”wORLD”再次调用count_case。此时,counts参数再次被绑定到同一个count_list列表对象,并继续在其当前值([1, 1])基础上进行累加。
当main函数执行完毕,其局部变量被清理,程序结束。
总结 🎯
本节课中我们一起学习了Python中的副作用概念。
我们了解到,函数影响程序不仅可以通过return语句返回一个新对象,还可以通过修改传入的可变对象参数来产生副作用。关键在于,函数内用于修改对象的引用和函数外观察该对象的引用,必须指向内存中的同一个对象。

副作用是一种强大的工具,但需要谨慎使用,因为它使得函数的行为不那么透明,可能让代码更难理解和调试。通常,明确使用返回值是更清晰的做法。然而,在需要对大型数据结构进行原地修改以提升效率时,副作用机制非常有用。
069:使用函数改进代码质量

概述
在本节课中,我们将学习如何通过使用函数来改进“黑客”游戏第六版的代码质量。我们将看到,函数作为一种控制结构,可以在不改变核心算法的情况下,让代码变得更清晰、更模块化,并且易于测试。
从算法面板到函数
上一节我们完成了第五版的设计。本节中我们来看看如何将第五版的算法结构转化为第六版的函数结构。
将代码转换为函数的过程,与你一直以来的工作——将算法分解为面板——非常相似。算法中的几乎每一个面板都将成为代码中的一个函数,而面板的标题将成为对应函数的名称。
此外,还会有两个函数在算法中没有对应的面板。
函数用于创建清晰、可读的代码。它们将代码分割成小段,使得每一段都可以独立编写和测试。它们还能整合具有相似功能的语句组。
创建第一个函数:create_window
我将从主程序面板的第一个矩形“创建窗口”开始。
“创建窗口”将成为我第六版代码中的一个函数。我将把 # 创建窗口 的注释替换为函数头 def create_window()。然后,我将把所有属于这个函数体的语句缩进到函数头下方。
以下是创建窗口函数的代码:
def create_window():
# 设置窗口尺寸和标题
window_size = (600, 400)
window_title = "黑客游戏 v6"
window = graphics.create_window(window_size, window_title)
return window
引入 main 函数
为了限制全局标识符的意外使用,我将在黑客游戏中引入一个 main 函数。
我算法中的主面板将成为程序中的 main 函数。我需要在 main 函数的函数体中调用 create_window,所以我现在就创建 main 函数。
在 create_window 函数上方,我将写一个新的函数头:def main():。在这个 main 函数内部,我将添加一个对 create_window 的调用。
def main():
window = create_window()
单元测试的重要性
现在我已经写了两个函数,我应该测试它们。我运行程序,但在函数下方的代码中得到了一个关于 window 的名称错误。
主模块中的代码无法访问 create_window 函数内部的 window 绑定。然而,我仍然需要测试我的新函数。为此,我可以删除其余代码,但我会将其粘贴到一个新文档中,因为我稍后还需要使用大部分代码。
每次编写新函数时,都需要测试该函数。这个过程被称为单元测试。每一个小的代码单元都应该在用于完整程序之前进行独立测试,以确保其正常工作。在本课程中,我们通常将每个函数视为一个单元,并在编写时测试每个函数。
为了测试 create_window,我只需要在程序底部添加一个对 main 的调用,因为 main 已经包含了对 create_window 的调用。
if __name__ == "__main__":
main()
这次,当我运行程序时,没有出现任何错误。但是,由于我移除了关闭窗口的代码,窗口不再关闭。我稍后会处理这个问题。
创建 display_header 函数
接下来,我将继续编写下一个函数。在 create_window 之后的下一个步骤是“显示标题”,所以我接下来将编写这个函数。
我将首先在函数体中写下 def display_header():,然后粘贴显示标题的旧版本5代码。
def display_header(window):
header_line = "尝试破解以下密码:"
line_x = 50
line_y = 50
graphics.draw_string(window, (line_x, line_y), header_line)
graphics.update_window(window)
time.sleep(1)
line_y += 30
然后,我在 main 中添加对 display_header 的调用,并再次运行程序以测试新函数。
一个名称错误报告说 window 未定义,因为它没有在 display_header 中绑定。这个问题有两个方面。首先,标识符 window 的作用域被限制在 create_window 函数内。我需要返回 window 对象,以便在 create_window 外部可以访问它。其次,window 对象必须在另一个函数 display_header 内部被访问。
我将向 display_header 添加一个 window 参数,以便我可以将 window 对象作为参数传递。我将在 create_window 中添加一个 return 语句:return window。我将在 main 中,将 create_window 函数调用的返回对象绑定到一个名为 window 的新标识符。然后,我可以将该 window 对象作为参数传递给 display_header。我还将向 display_header 函数定义添加一个名为 window 的参数。

def create_window():
# ... 之前的代码 ...
return window

def main():
window = create_window()
display_header(window)
def display_header(window):
# ... 函数体 ...
我再次运行程序,它正常工作了。一个窗口打开,标题被显示出来。
参数与标识符的作用域
尽管我在 main、create_window 和 display_header 函数中使用了相同的标识符 window,但我可以在这些独立的代码块中使用任何名称。我将通过将 main 中的名称改为 wilma 来说明这一点。程序仍然可以工作。
def main():
wilma = create_window()
display_header(wilma)
我会把它改回来,因为 wilma 对于窗口来说不是一个非常具有描述性的标识符。
抽象通用功能:display_line 函数
下一步是显示密码列表。注意,display_header 和 display_password_list 都使用了那四行熟悉的代码:调用 draw_string、调用 update_window、暂停以及更新 line_y 的赋值语句。


我们之前说过,函数可以整合具有相似功能的代码。如果我打算反复使用那四行代码,我应该编写一个包含这些行的函数。然后,我就可以在想要使用该代码时调用这个函数。
和往常一样,我将从函数头开始,在最后一个函数定义下方写下 def display_line():。我将从 display_header 中剪切并粘贴显示字符串的四行代码到 display_line 的函数体中。
我希望这个函数足够通用,可以显示标题、密码或结果行。这是解决方案泛化的一个例子。它应该能够显示我作为参数传递的任何字符串。我将用标识符 string 替换 draw_string 调用中的 header_line,然后在函数头中添加 string 作为参数。我还将添加一个 window 参数,以便我可以绘制和更新窗口。为了更容易记住参数顺序,我将始终把 window 参数放在第一位。
显示每个字符串还需要一个位置。这个函数必须在显示当前字符串后更改位置的 Y 坐标。为了更改函数中的参数对象,我将对可变参数对象使用副作用。我将使用一个包含两个坐标整数的列表作为参数。
我将把标识符 location 添加到 display_line 的参数列表中。我将用 location[0] 和 location[1] 替换 draw_string 调用中的 line_x 和 line_y。我还将用 location[1] 替换更新赋值语句中的 line_y。
标识符 pause_time 和 string_height 在 display_header 中被绑定,但只在 display_line 中使用。我将把这些赋值语句移到它们被需要的 display_line 中。
为了完成这个函数,我必须在函数顶部添加一个块注释,描述函数的行为以及每个参数的用途,包括其类型。
def display_line(window, string, location):
"""
在指定窗口的指定位置显示一个字符串,然后暂停并更新垂直位置。
参数:
window: 图形窗口对象。
string (str): 要显示的文本。
location (list): 一个包含两个整数的列表 [x, y],代表显示位置的坐标。
函数会修改此列表,将 y 坐标增加固定的行高。
"""
pause_time = 1
string_height = 30
graphics.draw_string(window, (location[0], location[1]), string)
graphics.update_window(window)
time.sleep(pause_time)
location[1] += string_height
现在,我可以在 display_header、display_password_list 和 display_outcome 中使用我的 display_line 函数。
重构 display_header 以使用 display_line
我将更新 display_header 中的代码以使用这个函数。当添加一个实现了注释中描述的所有功能的函数调用时,你应该用该函数调用替换注释。例如,我将用函数调用 display_line 替换冗余的注释 # 显示标题行。
我需要三个顺序正确的参数。我将使用 window 作为窗口参数,header_line 作为字符串参数。最后一个参数必须是位置,它是一个包含显示所需 X 和 Y 坐标的列表。
我还没有在程序中创建位置列表。如果一个对象需要在多个函数中使用,则必须在能够将该对象作为参数传递给所有需要它的函数的代码块中创建它。我应该在 main 函数中创建位置列表,因为它在 main 中调用的三个不同函数中使用:display_header、display_password_list 和 get_guesses。它也在这些函数调用的其他函数中使用。
我将把这个位置列表传递给 display_header。最后,我将修改 display_header 以包含一个 location 参数,并移除 line_x 和 line_y 的赋值语句。
def display_header(window, location):
header_line = "尝试破解以下密码:"
display_line(window, header_line, location)
这个函数完成了。
你的任务
现在轮到你了,你必须创建以下函数并在 main 函数中调用它们:
display_password_listget_guessesend_game
在 get_guesses 内部,你必须调用 prompt_for_guess 和 check_warning 函数。在 end_game 内部,你必须调用 display_outcome 和 prompt_for_end 函数。
你还必须为你调用的每个函数创建一个函数定义。然而,prompt_for_guess 和 prompt_for_end 非常相似,你应该创建一个名为 prompt_user 的单一函数来替代它们。创建 prompt_user 类似于创建 display_line。
你会发现标识符 attempts_left 在多个用户定义的函数中被使用。你必须决定在哪里绑定它,以及如何使其在需要的任何地方都可用。
每次创建新函数时都应该测试它。不要忘记为你编写的所有函数(包括我写的函数)包含描述性注释。
总结

本节课中,我们一起学习了如何通过函数来重构和优化“黑客”游戏的代码。我们了解了如何将算法面板映射为函数,如何通过参数传递数据,以及单元测试的重要性。我们还学习了抽象通用代码(如 display_line 和 prompt_user)来消除重复,并使代码更易于维护。记住,良好的函数设计是编写清晰、健壮和可测试代码的关键。
070:代码审查 - 黑客游戏版本6

在本节课中,我们将学习如何审查和追踪黑客游戏版本6的代码。我们将重点关注用户自定义函数的追踪方法,并介绍版本6引入的新软件质量测试标准。
概述
上一节我们完成了黑客游戏版本5的构建。本节中,我们将深入分析版本6的代码结构,学习如何逐步追踪用户自定义函数的执行过程,并理解版本6新增的代码规范要求。
代码结构与算法对应

版本6的代码始于与版本5相同的程序注释。接下来是两个自版本2以来一直使用的导入语句。

第一个显著变化是 main 函数的定义。在编程演示中我们曾提到,算法中的几乎每个面板都需要被翻译成一个函数,而版本6正是这样做的。
以下是 main 函数定义与算法步骤的对应关系:
- 前两个赋值语句:不在算法中,因为它们是简单的赋值语句。
- 后续五个语句:每个都对应算法中的一个矩形(即一个步骤),因为它们都是函数调用。
这种对应关系非常方便。让我们开始追踪代码。
追踪用户自定义函数
我将通过设置断点和单步执行来演示如何追踪代码。


-
开始追踪:首先,我按下“步入”按钮开始追踪。追踪从导入语句开始,按下“步过”按钮可以跳过它们。整个函数定义会被一步求值,仅将函数名添加到程序的命名空间中,其内部的语句组不会被执行。
-
设置断点:我在程序最底部的
main函数调用处设置了一个断点。接着,我按下调试按钮,使程序运行到该断点处。 -
“步过”与“步入”的区别:
- 如果此时按下“步过”按钮,调试器将一步求值整个
main函数及其调用的所有函数,我无法追踪单个函数调用。如果main函数调用后还有其它语句,“步过”会先运行完整个游戏,然后高亮显示那条后续语句。 - 为了检查
main函数内部的语句,我需要按下“步入”按钮。
- 如果此时按下“步过”按钮,调试器将一步求值整个
-
进入main函数:按下“步入”后,
main函数内的第一个赋值语句被高亮。我“步过”前两个赋值语句,来到main中的第一个函数调用:create_window。在进入此函数前,请注意栈数据中已有两个标识符:
attempts和location。函数(或代码块)拥有自己的命名空间。由于我们目前在main函数定义内部,“局部变量”区域代表该函数的命名空间。 -
进入
create_window函数:当我按下“步入”按钮进入create_window函数时,其第一条语句被高亮。此时,栈数据中location和attempts不再出现,因为它们不属于create_window函数的命名空间,而是属于main函数。我“步过”
create_window中熟悉的前四条语句,直到return语句被高亮。此时,栈数据显示create_window的命名空间中有了window标识符。 -
观察返回值:再次按下“步入”按钮,
return语句仍被高亮,栈数据中显示了函数的返回值是一个window对象。注意,返回值与window对象具有相同的内存地址,因此它们是同一个窗口对象。 -
返回main函数:按下“步过”按钮后,
display_header函数调用被高亮。查看栈数据,由于赋值语句已将window添加到main函数的命名空间,window现在出现在局部变量中。main函数中的window绑定到了create_window函数的返回对象,它与create_window中的window指向同一对象(地址相同)。在步入
display_header函数前,请注意attempts绑定为 4,并记下location和window对象的地址。 -
进入
display_header函数:步入后,即使该函数内的语句尚未求值,其局部命名空间中已有三个标识符:attempts,location,window。这是因为当函数被调用时,它的每个参数会按顺序被添加到命名空间并绑定到对应的实参。可以看到attempts是 4,另外两个对象的内存地址也相同。我“步过”
header赋值语句,此时header已被添加到局部命名空间。接着,我展开location列表,注意到索引 0 和 1 已绑定值,然后步入for语句,再步入其中的display_line函数调用。 -
进入
display_line函数:在display_line函数定义中,pause_time赋值语句被高亮。栈数据显示window、location和string参数已被添加到局部命名空间。同时注意,此函数的最后一条语句将location[1]的值更改为了字符串'height'。 -
观察副作用:我按下“步出”按钮完成此函数的求值,返回到调用函数
display_header,for语句的头部被高亮。查看栈数据,可以看到location[1]已被重新绑定为字符串'height'。再次按下“步出”按钮,求值返回到main函数,password赋值语句被高亮。请注意,location[1]现在绑定的是 57,因为在我按下“步出”按钮之前,第四条语句又求值了display_line两次。display_line中对location的修改就是副作用的体现。
版本6的新软件质量测试
让我们停止追踪,介绍黑客游戏版本6新增的软件质量测试。
1. 用户自定义函数的注释测试
除了在程序开头和逻辑代码块上方放置注释外,你现在必须为每个用户自定义函数(除了 main)添加注释。
main函数不需要单独注释,因为它已由程序注释描述,且没有参数。- 每个函数注释必须:
- 位于函数开头。
- 使用一两个动词描述函数的功能。
- 从函数定义处缩进一级。
- 描述每个函数参数。
- 每个函数参数应使用一行注释来描述其作用和类型。
2. 主函数部分测试
新增了两个关于主函数的测试:
- 程序包含一个名为
main的用户自定义函数。 - 除了导入语句,只有一行代码不属于任何用户自定义函数,且该行代码是对
main函数的单次调用。
你可能在其他地方看到不包含 main 函数的 Python 程序。然而,在本课程中,你编写的每个程序都必须包含一个 main 函数定义和一次对该函数的调用。这将减少因意外访问全局标识符而导致的错误。
在前五个版本中,你使用的所有标识符都是全局标识符。从现在开始,你将在用户自定义函数中使用局部标识符。拥有多个局部命名空间可以限制标识符的作用域,这使得多个程序员可以更独立地工作,因为任何标识符都可以在函数内部局部使用。
3. 用户自定义函数测试
新增了一个名为“用户自定义函数”的测试部分。
- 功能单一性:你的每个函数应该足够简单,无需长篇大论来描述。一个好的测试方法是检查它是否执行一个可以用带有一两个动词的句子描述的逻辑任务。
- 复杂度限制:第二个测试也是为了确保函数的简单性。每个函数应包含不超过 5 个参数,以及不超过 12 条语句。这些数字有些随意,但对于本课程来说是一个不错的选择。
总结
本节课中,我们一起学习了如何追踪黑客游戏版本6中用户自定义函数的执行流程,深入理解了函数调用栈、命名空间和副作用。同时,我们详细了解了版本6引入的三类新软件质量测试:用户自定义函数的注释规范、主函数的定义与调用规范,以及函数本身的复杂度和单一职责要求。请确保你的代码同时遵循新的和之前的软件质量测试标准。

恭喜你,已经完成了黑客游戏的版本6。只剩下最后一个版本了。😊
Python编程与问题解决:第8章:第1节:黑客游戏V6解决方案回顾与V7展望

在本节中,我们将回顾黑客游戏第6版(V6)的完成情况,并展望第7版(V7)需要实现的新功能。我们将总结已掌握的技能,并明确下一步的改进目标。
上一节我们完成了黑客游戏V6的开发。在开始V7的“破解”工作之前,让我们先看看你已经取得了哪些进展。
你从一个简单的基于文本的游戏开始,并将其转变为一个图形化游戏。你修改了设计和代码,加入了多次猜测、选择、循环和用户自定义函数。为了实现这些,你学会了使用许多不同的Python表达式和七种不同的Python语句,其中包括四种控制结构:if、for、while和函数定义。
你也学习了三种问题解决策略:问题分解、算法和抽象。你将在下一个游戏以及未来创建的任何项目中运用所有这些技术。
但在开始下一个游戏之前,你必须完成黑客游戏的最终版本。
黑客游戏目前剩余的解决方案问题主要涉及缺失的功能。在一个密码猜测游戏中,如果玩家没有得到关于猜测与秘密密码接近程度的反馈,游戏将很难进行下去。此外,为游戏添加能营造氛围的图形元素是使其更有趣的好方法。
在黑客游戏的最终版本中,你将练习迄今为止学到的所有技能。你将为此游戏添加最后两个功能:为错误猜测提供提示信息,以及将所有密码字符串嵌入到随机的符号字符中。
你还将在接下来的活动中制定新的描述、功能测试计划和算法。你的描述必须根据观察游戏时将看到的两个新功能进行调整,你的功能测试也需要调整,以测试这些更改可能产生的任何边界情况。
总结

本节课我们一起回顾了黑客游戏V6的成果,明确了V7版本需要增加的两个核心新功能:猜测提示和图形化氛围增强。这标志着我们将综合运用之前学到的所有编程语句、控制结构和问题解决策略,来完成这个项目的最终优化。
072:观察黑客游戏最终版 🎮

在本节课程中,我们将观察黑客游戏的最终版本。这个版本包含了之前所有版本的功能,并引入了一些新的视觉元素和反馈机制,使游戏体验更加完整。
在上一节中,我们分析了游戏的核心逻辑。本节中,我们将仔细查看这个最终版本的具体表现,特别是新增加的功能。
首先,请注意密码列表中出现的符号字符,这些字符在之前的任何版本中都不曾出现。
以下是游戏启动后的初始界面示例:

接下来,我们通过几次尝试来观察游戏的新反馈机制。
我将输入一个错误答案 finding。此时,游戏界面会展现出本版本的第二个新特性:在屏幕右上角出现了一条提示信息。
这条提示信息表明我的猜测是错误的,并显示了我的猜测与秘密密码在相同位置上有多少个字母匹配。
对于 finding 和 hunting,提示信息显示有 4 个字母在匹配位置上正确。
然后,我将尝试用小写字母输入 hunting。这次,提示信息显示为 0 out of 7 个字母匹配。
这个结果很好地从视觉上证实了我们在之前版本中做过的一项测试:游戏对字母的大小写是敏感的。
最后,为了观察另一个功能,我将输入 putting 作为最后一个错误答案。
这次尝试给出了 5 out of 7 个字母匹配的结果。同时,在窗口的右下角出现了锁定警告提示,这是多次输入错误后触发的机制。
最终,输入正确的密码 hunting 将带领我们进入熟悉的成功结局屏幕。

当你在接下来的活动中运行这个游戏时,请密切关注密码列表中使用的符号字符。
以下是需要注意的几个观察点:
- 注意使用了哪些符号。
- 观察每次运行程序时,每一行选择的符号是否相同。
- 留意符号放置的位置。例如,每个密码前后选择的符号数量是否相同?密码前和密码后的符号数量会变化吗?

本节课中,我们一起观察了黑客游戏的最终版本。我们看到了新增加的符号字符、实时匹配度提示以及锁定警告机制。这些功能共同构成了一个更具交互性和反馈性的完整游戏体验。在接下来的实践中,请仔细体会这些设计细节。
073:创建第7版黑客算法

概述
在本节课中,我们将学习如何创建最终版本的黑客算法。我们将为算法添加两个新功能:将密码嵌入随机符号中,以及显示提示信息。我们将创建新的面板来实现这些功能,并修改现有的面板结构。
嵌入密码到随机符号中
上一节我们介绍了基本的密码显示逻辑,本节中我们来看看如何将密码嵌入到随机符号中,以增加破解难度。
之前的算法使用了一个确定的重复控制结构来显示每一行密码。现在,我们需要将重复的步骤从“显示密码行”改为“显示嵌入的密码”。
由于每个密码都需要被嵌入,密码列表后显示的空行不能再作为列表中的一个元素。如果它仍然是列表元素,空行也会被嵌入随机符号中。因此,我们将在选择密码之前,添加一个步骤来显示空行。
“显示嵌入的密码”这个动作本身比较复杂,需要先为纯文本密码添加符号,然后再显示它。这两个动作都足够复杂,需要各自独立的算法步骤。由于不能在控制结构中连续放置两个步骤,“显示嵌入的密码”必须扩展成一个新的面板。
此外,我们还需要创建另一个新的面板来扩展“嵌入密码”步骤,因为它将需要许多程序语句,包括控制结构。
以下是实现嵌入功能时需要注意的几个要点:
- 每个嵌入的密码字符串包含7个密码字母和13个随机符号,这些符号从一个包含18个符号的集合中选取。
- 由于每行包含13个符号,密码前会出现0到13个随机数量的符号,其余符号出现在密码之后。
- 例如,这里有三个嵌入的密码:第一个密码前有5个符号,后有8个;第二个密码前有0个,后有13个;第三个密码前有13个,后有0个。
- 你需要计算13个符号中有多少个添加在密码之前,多少个添加在密码之后。
随机符号中密码被添加进去的分割点,我们称之为分割索引。例如,在上面的几行中,分割索引分别是5、0和13。
你应该使用两个独立的确定重复控制结构:一个用于选择和连接出现在密码之前的随机符号,另一个用于处理出现在密码之后的随机符号。
显示提示信息
在介绍了密码嵌入功能后,我们接下来看看如何为用户显示提示信息。
为了实现提示功能,你同样需要两个新的面板:在“获取下一个猜测”面板的适当位置添加一个“显示提示”步骤,并将其展开。
显示提示的两行文字本身是直接的,但你需要使用两个不同的控制结构来检查猜测中的字母是否与密码中的字母匹配。
当你将猜测中的一个字母与正确密码中的一个字母进行比较时,不要忘记确保你正在检查的索引没有超出密码的末尾。
总结

本节课中,我们一起学习了如何构建最终版的黑客算法。我们重点探讨了两个核心新功能:将密码嵌入随机符号和显示提示信息。我们了解到,这需要通过创建新的算法面板和修改现有结构来实现。在嵌入密码时,关键在于计算分割索引并使用独立的循环来构建密码前后的符号串。在下一项活动中,你将完成这最后一个黑客算法的构建。
074:程序破解版本7 🎮
在本节课中,我们将学习如何编写破解游戏的最终版本。这个版本将要求我们创建更多的函数定义,并对现有函数进行修改。我们将重点关注如何将密码嵌入到一个随机符号字符串中,并实现显示提示的功能。

嵌入密码函数
上一节我们介绍了程序的基本结构,本节中我们来看看如何编写 embed_password 函数。这个函数负责将真实的密码嵌入到一个由随机符号组成的字符串中。
函数定义如下:
def embed_password(password, size):
"""
将密码嵌入到一个指定长度的随机符号字符串中。
:param password: 需要嵌入的密码字符串。
:param size: 嵌入后字符串的总长度。
:return: 嵌入后的字符串。
"""
在函数内部,我们首先需要创建一个包含所有可能随机符号的字符串,用于填充密码周围的空间。
fill = "!@#$%^&*()_+-=[]{}|;:,.<>?/~`"
接下来,我们创建一个空字符串 embedding,用于逐步构建最终的嵌入字符串。
embedding = ""
计算分割索引是关键步骤。分割索引决定了在密码之前放置多少个随机符号。我们不能简单地选择一个0到20之间的随机数,因为这可能导致最终字符串长度超过20。我们需要确保密码前的符号数量最多为 size - len(password)。
password_size = len(password)
split_index = random.randint(0, size - password_size)
以下是构建嵌入字符串的步骤:
首先,我们使用一个循环来添加分割索引前的随机符号。
for index in range(0, split_index):
random_char = random.choice(fill)
embedding = embedding + random_char
然后,我们将密码本身添加到嵌入字符串中。
embedding = embedding + password
最后,我们使用另一个循环来添加剩余的随机符号,直到字符串达到指定的总长度。
for index in range(split_index + password_size, size):
random_char = random.choice(fill)
embedding = embedding + random_char
完成循环后,函数返回构建好的嵌入字符串。

return embedding
为了使用 random.randint 和 random.choice 函数,我们需要在程序开头导入 random 模块。

import random
更新显示密码列表函数
现在我们已经有了 embed_password 函数,接下来需要更新 display_password_list 函数。这个函数现在需要调用 embed_password 来生成每个显示项,而不是直接显示密码。
我们需要修改函数,使其接收密码列表和嵌入字符串的长度作为参数。
def display_password_list(passwords, size):
"""
显示嵌入后的密码列表。
:param passwords: 密码列表。
:param size: 每个嵌入字符串的长度。
"""
在函数内部,我们遍历密码列表,对每个密码调用 embed_password 函数,并打印结果。
for password in passwords:
embedded = embed_password(password, size)
print(embedded)
编写显示提示函数
最后,我们需要编写一个新的函数 display_hint。这个函数的作用是根据玩家的猜测,给出一个提示,指出密码中有多少个字母在正确的位置上。
函数定义如下:
def display_hint(password, guess):
"""
比较密码和猜测,给出位置正确的字母数量提示。
:param password: 正确的密码。
:param guess: 玩家的猜测。
:return: 位置正确的字母数量。
"""
在函数内部,我们初始化一个计数器。
correct_positions = 0
然后,我们遍历猜测字符串的每个位置(假设猜测和密码长度相同)。
for i in range(len(guess)):
if i < len(password) and guess[i] == password[i]:
correct_positions += 1
循环结束后,函数返回计数器的值。
return correct_positions
在游戏的主循环中,在玩家每次猜测后,我们可以调用这个函数来提供反馈。
hint = display_hint(secret_password, player_guess)
print(f"提示:有 {hint} 个字母在正确的位置上。")

本节课中我们一起学习了如何构建破解游戏的最终版本。我们创建了 embed_password 函数来将密码隐藏在一串随机字符中,更新了 display_password_list 函数来展示这些嵌入的字符串,并编写了 display_hint 函数来为玩家提供有价值的反馈。通过这些步骤,我们使游戏的核心逻辑更加模块化和清晰。
075:识别《黑客》游戏第7版中的解决方案问题与改进

在本节课中,我们将回顾《黑客》游戏第7版的最终状态,识别潜在的解决方案问题,并探讨如何进一步改进游戏以提升可玩性。

完成《黑客》游戏第7版
你已经完成了《黑客》游戏第7版的破解工作。我们不再追踪第7版的代码,也没有新的软件质量测试需要引入。
尽管如此,你仍需确保你的代码能够通过之前所有的软件质量测试。
由于这是《黑客》游戏的最终版本,只剩下一件事要做:识别任何潜在的解决方案问题或可以进行的改进。


识别解决方案问题
在解决方案问题方面,实际上并不存在明显的问题。你已经成功地为完整的《黑客》游戏添加了所有必需的功能。
你的功能测试计划已经完全覆盖了游戏行为的测试,并且你的代码已经达到了软件质量标准。
探讨游戏改进方案
虽然游戏功能完整,但仍有改进空间。当前的《黑客》游戏重玩性并不高。如果你玩过一次并知道了正确答案,游戏就失去了挑战性。
以下是几个可以提升游戏体验的改进方向:
- 随机化密码与词表:游戏可以从密码列表中随机选择一个密码作为正确答案。此外,你可以创建一个庞大的密码总表,然后从中随机选择13个用于游戏。这样,每一局游戏都会使用随机的单词组合和随机的正确答案。
- 动态调整难度:你可以根据玩家在之前游戏中的表现来调整猜测次数。例如,一个总能三次猜中密码的玩家,其总尝试次数可以从4次减少。而对于不常获胜或无法在4次内获胜的玩家,可以给予更多尝试机会。
- 增加挑战性:另一种增加游戏难度的方法是,擦除密码列表中部分密码的字符,让玩家需要猜测缺失的字母是什么。
你可以尝试实现上述任何一项改进,或者思考其他可能的优化方案。
课程总结

在本节课中,我们一起回顾了《黑客》游戏第7版的完成情况,确认了其解决方案的稳定性,并探讨了多个提升游戏可玩性和挑战性的改进方向。恭喜你,你已经完成了《黑客》游戏的开发。
076:Poke the Dots游戏介绍 🎮

在本节课中,我们将要学习如何创建名为“Poke the Dots”的新游戏。我们将从理解游戏的整体功能开始,并介绍版本规划的核心概念,特别是事件处理技术。
游戏创建任务重启 🚀
你已经完成了“Hacking”游戏,现在回到了游戏创作流程的第一个任务。距离你上次接触“创建游戏”任务已经有一段时间了。
你将要制作的下一个游戏叫做“Poke the Dots”。在创建这个游戏的过程中,你将学习如何处理多个玩家操作,并让图形对象动起来。为此,你将学习新的编程概念,例如类和用户自定义方法。
在开始制作“Poke the Dots”的第一个版本之前,必须将最终游戏的完整功能集分解为多个版本功能集。
版本功能集规划 📋
我们使用一种名为“功能选择”的问题解决技术来创建这些功能子集。与“Hacking”游戏一样,我将为你提供“Poke the Dots”的版本功能集。
我们之前讨论过为游戏创建版本功能集的两个考虑因素。
以下是这两个核心考虑因素:
- 平衡功能集:所有版本应该花费大致相同的精力来实现。
- 功能依赖性:依赖于其他功能的功能必须在之后实现。
例如,在“Hacking”游戏中,显示提示信息是你最后实现的功能之一。它要求你已经实现了提示用户猜测并将该猜测与正确答案进行比较的功能。你需要学习 if 语句、for 循环和多个 ua_game 函数来编写提示信息的代码。试图一次性学习和实现所有这些功能对于一个版本来说工作量太大了。
了解功能的依赖性和复杂性,可以让你以一种可控的方式规划版本,使制作每个版本的时间和精力保持在可管理的范围内。

版本一:事件处理 🖱️

“Poke the Dots”的第一个版本将专注于一项名为事件处理的新技术。
游戏中有许多类别的事件,包括鼠标点击和按键按下。事件处理允许你识别这些事件,并在必要时对其做出反应。
为了处理事件,你将使用 pygame 库。回想一下,ua_game 是基于 pygame 库构建的。你将继续使用 ua_game,但也需要直接使用 pygame 的一些功能。
在接下来的几节课中,你将观察并试玩完整的“Poke the Dots”游戏。你还将为“Poke the Dots”版本一进行观察、试玩、描述并创建测试计划。
理解游戏任务:观察与试玩 👀🎮
你现在回到了游戏创作流程中的“理解游戏”任务。使用经验分解法来理解完整游戏的前两个步骤是:观察游戏和试玩游戏。
首先,让我们看看完整的“Poke the Dots”游戏。
当窗口打开时,屏幕上有两个圆点在移动。当圆点碰到屏幕边缘时,它们会反弹。在屏幕的左上角有一个记分板,游戏运行的每一秒分数都会增加。当两个圆点相撞时,游戏结束。
与“Hacking”不同,我不会按回车键来结束游戏,而是按关闭图标来关闭窗口。
我将再次启动游戏。这次,圆点从屏幕上的不同位置开始。我会重新启动游戏几次,以确认每次圆点都从不同的位置开始。
接下来我要做的是点击鼠标左键。当我这样做时,圆点会从屏幕上消失,然后重新出现在一个随机位置。每次我点击鼠标按钮时,它们都会这样做。
最后,注意圆点的速度:红点和蓝点的移动方式不同。红点在垂直方向上移动得更快,而蓝点在水平方向上移动得更快。
你现在应该自己试玩几次完整的“Poke the Dots”游戏,以熟悉游戏的各个方面。
总结 📝

本节课中,我们一起学习了“Poke the Dots”游戏的引入和规划。我们回顾了游戏创作流程,并介绍了通过功能选择来规划版本功能集的方法,重点考虑了平衡性和依赖性。我们明确了版本一的核心是学习事件处理技术。最后,我们通过观察和试玩,熟悉了完整游戏的基本行为,包括圆点移动、碰撞、计分和鼠标交互。在接下来的课程中,我们将开始为第一个版本制定具体的实现计划。
077:观察“戳点游戏”版本一 🎮

在本节课程中,我们将观察并分析“戳点游戏”的第一个版本。我们将了解该版本与完整游戏之间的核心区别,并学习如何为其制定描述与功能测试计划。
游戏演示与观察 👀
现在,我将向你展示“戳点游戏”的第一个版本。当游戏窗口打开时,界面上没有显示记分板。
两个圆点仍在移动和反弹。然而,当两个圆点相互接触时,游戏并不会结束。
我仍然可以通过点击关闭图标来关闭游戏窗口。
互动探索任务 🕵️
在接下来的活动中,请你自己运行并体验“戳点游戏”版本一,以熟悉其行为。
请尝试找出完整游戏与此第一版本之间的其他差异。以下是几个需要你验证的关键问题:
以下是几个需要你验证的关键问题:
- 圆点的移动轨迹是否仍然是随机的?
- 玩家是否仍然可以通过点击鼠标按钮来移动圆点?
- 每次你重启游戏时,圆点的初始移动方向是怎样的?
制定文档与测试计划 📝

在你熟悉了“戳点游戏”版本一之后,请为这个版本制作一份描述文档和功能测试计划。
078:创建“戳点”游戏第一版算法 🎮

在本节课中,我们将学习如何为“戳点”游戏的第一版创建算法。我们将重点关注游戏动画的核心概念——帧循环,并了解它与之前“黑客”游戏的主要区别。
概述
“戳点”游戏与“黑客”游戏有两个主要区别。首先,“戳点”中的圆点会在游戏窗口内持续移动,直到它们相互碰撞。其次,游戏更新不仅基于玩家输入(如鼠标点击),还基于游戏条件(如圆点碰到窗口边缘)和时间变化。为了处理持续动画和玩家操作,我们将引入一个名为“帧循环”的新概念。
帧循环:游戏动画的核心
帧循环是程序中的一个控制结构,它通过反复更新游戏对象和重绘游戏窗口内容来模拟运动。一帧就是一张包含所有应显示对象的静态图像。每次执行帧循环,程序还必须检查玩家是否执行了任何操作并做出相应响应。
“戳点”游戏将以每秒90帧的速度运行,这意味着每秒将有90张静态图像被绘制到窗口。
以下是帧循环的基本概念公式:
while 游戏未结束:
处理事件()
绘制游戏()
更新游戏()
创建算法的主要步骤
现在,让我们开始创建算法。我们将遵循一个可复用的四步模板,这是“使用模板”问题解决技术的一个例子。

以下是创建游戏算法的四个主要步骤:
- 创建窗口:与我们在“黑客”游戏中所做的一样。
- 创建游戏:创建游戏所需的所有对象。
- 运行游戏:包含帧循环。
- 关闭窗口:与“黑客”游戏结束时一样。
不同的游戏可以通过改变“创建游戏”和“运行游戏”这两个步骤的子步骤来实现。
展开“创建游戏”步骤
在“创建游戏”步骤中,我们需要初始化游戏所需的所有对象。对于“戳点”游戏的第一版,这包括:
- 创建时钟:时钟负责控制游戏的速度或帧率。在未来的版本中,它还将用于更新记分牌。
- 创建小圆点:定义圆点的属性,如位置、颜色、大小和速度。
- 创建大圆点:定义另一个圆点的属性。
在编写创建圆点的代码时,你只需要一个函数,但算法中需要为每个圆点明确一个步骤。


展开“运行游戏”步骤
“运行游戏”是放置帧循环的地方。我们希望窗口保持打开状态,直到玩家点击关闭图标。
以下是“运行游戏”步骤的详细分解:
while 玩家未选择关闭:
运行一帧()


“运行一帧”步骤本身又包含三个关键子步骤:
- 处理事件:跟踪任何玩家操作。
- 绘制游戏:绘制当前帧。
- 更新游戏:为下一帧更新所有游戏对象。
对象在绘制后才被更新,因此它们最初的位置会被显示出来。
详细分解“绘制游戏”
“绘制游戏”步骤相对简单,它包含三个操作:
- 绘制小圆点
- 绘制大圆点
- 更新显示
“更新显示”步骤将更新窗口以显示刚刚绘制的所有对象。与“黑客”游戏每绘制一个字符串就更新一次显示不同,在“戳点”中,我们可以绘制所有对象,然后每帧更新一次显示,这样效率更高。
详细分解“处理事件”
每个图形库都有一个函数,可以返回一个事件序列。这个序列包含自上次调用该函数以来发生的所有事件。我们将每帧调用一次这个函数,然后使用确定的循环来处理序列中的所有新事件。
在“处理事件”步骤中,我们使用一个 for 循环来遍历事件列表。对于列表中的每个事件,我们调用“处理单个事件”函数。
“戳点”游戏的第一版只处理一类事件:点击窗口上的关闭图标。我们称这类事件为“关闭事件”。
以下是处理单个事件的逻辑:
if 事件类别 == 关闭:
记住玩家已选择关闭
“玩家已选择关闭”是一个布尔值,它控制着外层的 while 循环。只要这个值为假,游戏就会继续运行下一帧。
详细分解“更新游戏”
“更新游戏”步骤负责更新游戏状态,为下一帧做准备。它包括:
- 移动小圆点
- 移动大圆点
- 控制帧率(确保游戏在不同机器上以一致的速度运行)
我们将详细展开“移动小圆点”的步骤,“移动大圆点”的逻辑与之相同,因此无需重复。
圆点的表示与移动逻辑
在屏幕上,一个圆由大量像素组成。为了高效地表示和移动圆,我们使用两个对象:圆心点(一个包含两个整数坐标的序列)和半径(一个整数)。当圆移动时,半径保持不变,只有圆心改变。改变两个整数远比改变圆周上的大量像素要快。
移动圆点的逻辑是更新其圆心的X和Y坐标。每个坐标的更新公式为:
新坐标 = 旧坐标 + 速度分量
我们使用一个 for 循环来遍历圆心坐标的索引(0代表X,1代表Y),并更新每个坐标。
边界碰撞检测与反弹
我们需要检查圆点是否移出了窗口。如果圆点的边缘移出了窗口,我们需要让它从窗口边缘反弹回来。
反弹的逻辑是:否定导致其移出边界的那个方向上的速度分量。
以下是三个例子:
- 如果圆点仅以每帧+2像素的速度向右移动并碰到右边缘,则其X方向速度(索引0)被取反,变为每帧-2像素。
- 如果圆点仅以每帧-1像素的速度向左移动并碰到左边缘,则其X方向速度被取反,变为每帧+1像素。
- 如果圆点仅以每帧+3像素的速度向下移动并碰到底部边缘,则其Y方向速度(索引1)被取反,变为每帧-3像素。
总结

本节课我们一起学习了为“戳点”游戏第一版创建完整算法的过程。我们引入了帧循环的核心概念来管理持续动画和玩家输入。算法遵循了创建窗口、创建游戏对象、运行游戏(包含帧循环)和关闭窗口的四步模板。我们详细探讨了如何表示和移动圆点,以及如何处理边界碰撞使其反弹。在开始编写代码实现版本1之前,请确保观看接下来的几节编程语言视频。
079:import语句的变体 📦

在本节课中,我们将学习Python中import语句的几种变体。这些变体可以帮助我们解决程序中可能出现的名称冲突问题,并且允许我们从子模块(即位于其他模块内部的模块)中导入对象。
名称冲突问题
上一节我们了解了基本的import语句。本节中,我们来看看当程序中存在相同标识符时会发生什么。
名称冲突发生在你尝试同时使用同一个标识符来指代不同对象的时候。
例如,下面这个程序使用标识符sleep来指代不同的睡眠模式,如REM(快速眼动)和非REM。我想在打印睡眠模式和“dreams”输出之间有一个暂停。为此,我将从time模块导入sleep函数,并在第一个print函数调用后调用这个sleep函数。
from time import sleep
sleep = input("Enter sleep mode: ")
print(sleep)
sleep(1)
print("dreams")
Python解释器报告了一个类型错误,指出sleep不是一个可调用对象。
发生了什么?
- 程序开始时,主模块的命名空间包含标识符
input和print,每个都绑定到一个函数对象。 - 当执行
import语句时,标识符sleep被添加到命名空间,并绑定到来自time模块的sleep函数对象。 - 接下来,
input函数返回一个字符串对象(例如“REM”),赋值语句将标识符sleep重新绑定到这个字符串对象。 print函数调用显示这个字符串对象。- 下一条语句是一个函数调用,其函数名是
sleep。解释器解引用sleep标识符,得到一个字符串对象,而字符串不是函数,因此函数调用失败。
当赋值语句重新绑定标识符sleep时,对sleep函数的引用就丢失了。这就是一个名称冲突。
Python使用“最近绑定规则”来解决名称冲突:无论哪个绑定发生得最近,该绑定就会被用来解引用一个标识符。
例如,我将import语句移到程序后面:
sleep = input("Enter sleep mode: ")
print(sleep)
from time import sleep
sleep(1)
print("dreams")
这次,标识符sleep首先被添加到命名空间并绑定到input函数调用的结果对象(“REM”)。然后print函数调用显示“REM”。接着,import语句将sleep重新绑定到time模块的sleep函数。对sleep的函数调用按预期工作。
然而,当评估if语句的条件时,标识符sleep被解引用以获得sleep函数对象,它不等于字符串“REM”,即使用户输入了“REM”。名称冲突依然存在,程序输出不正确。
使用as子句解决冲突
你可以通过为其中一个冲突对象选择不同的标识符来绑定,从而解决名称冲突。
我可以为输入字符串绑定一个不同的标识符,例如sleep_mode,并保留标识符sleep给导入的sleep函数。这需要在所有想引用用户输入的字符串的地方,将sleep改为sleep_mode。
在这种情况下,sleep_mode可能是输入字符串更具描述性的标识符。然而,在一个关于睡眠主题、名为sleep.py的程序中,使用sleep作为暂停函数的名称并不十分贴切。
Python提供了灵活性,可以使用import语句的不同变体来解决由导入名称引起的冲突。这些变体允许你在程序中使用最合适的本地名称来表示每个导入的对象,而不受其他模块中使用的名称的限制。
以下是我们在之前import语句基础上的一般化形式,它添加了一个可选的as子句,为每个导入的名称提供一个替代的本地名称。这个本地名称通常被称为别名。
我将恢复使用sleep来指代输入字符串,并使用import语句的这个新变体,将导入的本地名称从sleep改为pause,这在本程序中更具描述性。
from time import sleep as pause
sleep = input("Enter sleep mode: ")
print(sleep)
pause(1)
print("dreams")
程序输出正确。
以下是带as子句的import语句的语义:
- 程序开始时,主模块的命名空间包含标识符
input和print,每个都绑定到一个函数对象。 - 当执行
import语句时,标识符pause被添加到命名空间,并绑定到来自time模块的sleep函数对象。 - 接下来,
input函数返回一个“REM”字符串对象,赋值语句将sleep标识符绑定到这个对象。 print函数调用显示这个字符串对象。- 下一条语句是一个函数调用,其函数名是
pause。解释器解引用pause标识符以获得一个函数对象,因此程序按预期继续。名称冲突已被消除。
不使用from关键字的变体
import语句的另一个变体不使用from关键字。这是一个新的语法图,它支持你已经见过的from变体和新非from变体。
以下是from变体的语法图(已见过):
import_statement ::= "from" module "import" identifier ["as" identifier]
以下是新的非from变体的语法图:
import_statement ::= "import" module ["as" identifier]
如果使用这个新变体,那么当使用导入模块中的标识符时,必须将其作为该模块的属性来使用。
例如,以下是使用此变体的睡眠程序版本:
import time
sleep = input("Enter sleep mode: ")
print(sleep)
time.sleep(1)
print("dreams")
程序输出正确。
以下是非from变体import语句的语义:
- 程序开始时,主模块的命名空间包含标识符
input和print,每个都绑定到一个函数对象。 - 当执行
import语句时,标识符time被添加到命名空间,并绑定到time模块对象。time模块中的任何标识符都不会绑定到主模块的命名空间,只有模块本身的名称。 - 接下来,
input函数返回一个“REM”字符串对象,赋值语句将sleep标识符绑定到这个对象。 print函数调用显示这个字符串对象。- 下一条语句是一个函数调用,其函数名是
time.sleep(1)。这是一个属性引用。之所以需要属性引用,是因为只有模块名在主模块的命名空间中,而sleep函数的名称不在。
回顾方法调用课程中属性引用的语法和语义:
- 首先,解释器计算表达式
time以获得一个对象(这里是time模块对象)。 - 其次,解释器检查属性
sleep是否在time模块对象的命名空间中。由于sleep是在time模块中定义的函数,sleep在time的命名空间中,因此获得了sleep函数对象。 - 解释器将此函数应用于参数列表(这里是单个整数对象
1),sleep函数暂停一秒钟。程序按预期继续。名称冲突已被消除。
从子模块导入
在Python中,模块可以定义在其他模块内部。位于另一个模块内部的模块称为子模块。
为了从子模块导入对象,我将对模块本身的语法图进行一般化。之前,我们假设模块名是单个标识符,因此我们使用这个语法图表示模块:
module ::= identifier
为了访问子模块中的对象,我可以使用这个一般化的语法,将模块表示为由点分隔的标识符序列:
module ::= identifier ("." identifier)*
例如,标准库包含一个名为os的模块,它提供对操作系统功能的访问。在os模块内部有一个名为path的子模块,其中包含一个名为isfile的函数。isfile函数如果其字符串参数是Python运行时所在文件夹中存在的文件名,则返回True,否则返回False。
我可以使用这个模块的一般化语法来访问此函数:
from os.path import isfile
print(isfile("sleep.py")) # 假设当前文件夹有sleep.py文件,返回True
print(isfile("snobz.py")) # 假设没有此文件,返回False
总结

本节课中,我们一起学习了Python import语句的几种重要变体。我们首先探讨了名称冲突的产生原因和Python的“最近绑定规则”。接着,我们学习了如何使用from ... import ... as ...语句为导入的对象创建别名,从而避免冲突并使代码更清晰。然后,我们介绍了不使用from关键字的import语句,它要求通过模块名加点号的方式来访问其内部对象(属性引用)。最后,我们了解了如何通过点号表示法(如os.path)从子模块中导入对象。掌握这些import语句的变体,能够帮助你更灵活、更清晰地组织代码,有效管理不同模块和命名空间中的标识符。
Python编程与问题解决:09_05_01:pass占位语句 🧩

在本节课中,我们将学习Python中的一个特殊语句——pass语句。它不执行任何操作,但可以作为代码中的占位符,帮助我们构建程序结构,避免语法错误。
在编写代码时,我们常常会先搭建程序框架,某些具体的代码块可能暂时还未想好如何实现。如果这些代码块是空的,Python解释器会报语法错误。pass语句就是为了解决这个问题而存在的。
pass语句的语法与语义
以下是pass语句的语法图。它非常简单,只包含一个关键字 pass。
pass_statement ::= "pass"
pass语句的语义是:不执行任何操作。当Python解释器执行到pass语句时,它会直接跳过,继续执行后续的代码。
pass语句的常见用途
pass语句虽然简单,但在特定场景下非常有用。以下是它的两个主要应用场景。
1. 在复合语句的代码块中占位
在if、while、for或函数定义等复合语句中,代码块(suite)必须包含至少一条语句。如果暂时不想写具体逻辑,可以使用pass占位。
例如,考虑以下程序,我们根据输入决定输出,但尚未想好当输入是"blue"时该输出什么字符串:
color = input()
if color == "red":
print("apple")
elif color == "blue":
# 待完成:决定输出什么
上面的代码是无效的,因为elif子句的代码块是空的。注释不是语句,不能填充代码块。我们可以使用pass语句来避免语法错误:
color = input()
if color == "red":
print("apple")
elif color == "blue":
pass # 待完成:决定输出什么
2. 创建函数存根以进行测试
在编写包含多个函数的程序时,你可能希望先测试部分已完成的函数,而其他函数稍后再实现。这时,可以为未完成的函数创建“存根”。
函数存根是一个函数定义,但其函数体只包含一条pass语句。这允许程序在语法上保持完整,从而可以运行和测试其他部分。
例如,这是上一课中出现的横幅程序,但banner函数暂时只是一个存根:
def banner():
pass # 函数存根,具体实现稍后完成
def main():
result = banner()
print(result)
if __name__ == "__main__":
main()
通过使用pass语句作为banner函数的存根,我们可以先运行和测试main函数,而无需立刻完成banner函数的所有细节。


本节课我们一起学习了pass语句。它是一个不执行任何操作的占位语句,主要用于在复合语句的代码块中避免语法错误,以及为尚未实现的函数创建存根,方便我们分步构建和测试程序。
081:编程“戳点”游戏版本1

在本节课中,我们将学习如何从零开始编程“戳点”游戏的第一版。我们将遵循一个结构化的过程,从编写程序注释和算法步骤开始,逐步实现游戏窗口、游戏对象、事件处理、图形绘制和游戏逻辑。
概述
我们将创建一个简单的游戏,其中包含两个移动的圆点。本节将指导你完成初始化项目、创建游戏窗口、处理用户事件、绘制图形以及实现基础物理(如反弹)的整个过程。
创建程序框架与注释
编程“戳点”游戏版本1的第一步,是创建一个程序注释,并为所有算法步骤添加注释。这与编程“黑客”游戏时使用的过程相同,但这是你在七个版本中首次从空文档开始。添加你的注释,并记得保存文档。

我首先要做的是为每个函数编写一个头部注释。在每个函数体内,我会先使用一个 pass 语句,直到我准备好添加更多代码。我将在程序底部添加对 main 函数的调用,并运行它。

程序运行无误,没有显示任何内容,这符合预期。
创建游戏窗口
上一节我们介绍了如何搭建程序框架。本节中,我们来看看如何创建游戏窗口。
算法步骤中的一些注释与“黑客”游戏类似。例如,你之前创建过打开窗口的函数。我将开始为“戳点”游戏编写 create_window 函数。这次,窗口标题是“Poke the dots”,尺寸是500x400像素。
由于版本1中没有文本,我还不需要设置字体属性,但我会设置背景颜色,尽管默认是黑色。
现在,我将在 main 函数体中添加代码。我会删除 pass 语句,并使用 create_window 来创建一个窗口对象。我将通过运行程序来测试这些函数。
窗口成功打开,进展顺利。
初始化游戏对象与主循环
窗口创建成功后,下一步是在 main 函数中创建游戏对象。在我们的算法中,我们使用一个时钟来控制帧率。在 Pygame 中,Clock 类管理帧率。这个类定义在 Pygame 模块的 time 子模块中。
要创建一个时钟,首先我必须从 pygame.time 导入 Clock。现在,我将在 main 函数中通过调用类名 Clock() 来创建这个对象。
我将通过为“大点”的颜色、半径、中心点和速度创建对象来生成大点。颜色和速度来自版本描述。大点是蓝色的,并且它在水平方向上的移动速度是垂直方向的两倍。另外两个属性,半径和中心点,是通过观察游戏估算出来的。你可以自己估算小点的值。
main 函数中最后两个注释是 play_game 和 close_window。我将为它们分别添加函数调用:一个是普通的函数调用 play_game(),另一个是方法调用 window.close()。
当然,当我运行程序时,除了窗口关闭,没有发生任何新的事情。play_game 函数中唯一的语句是 pass,所以调用这个函数时什么也不会发生。接下来让我们改变这一点。
实现游戏主循环与事件处理
现在我们来编写 play_game 的代码。在“戳点”游戏中,窗口会一直保持打开,直到玩家点击关闭窗口图标。Pygame 将每个玩家动作表示为一个事件,点击关闭图标由一个 QUIT 事件表示。
我将使用一个名为 close_selected 的布尔变量来跟踪是否发生了关闭事件。我将其初始绑定为 False,因为直到玩家采取相应动作前,Pygame 不会创建关闭事件。
handle_events 函数处理 Pygame 创建的所有事件。当发生关闭事件时,我将让它返回 True,这样我就知道何时关闭窗口。因此,我将把 close_selected 重新绑定到 handle_events 的返回对象。
游戏的帧循环应该在 close_selected 为 False 时一直运行,所以 while 循环头是 while not close_selected:。我将在循环体中将 draw_game 和 update_game 的注释替换为它们各自的函数调用。

好的,现在来看 handle_events。由于 handle_events 必须返回玩家是否点击了关闭图标,我必须跟踪这个关闭事件是否发生。

我将使用一个局部标识符 closed 并将其绑定为 False。只有在发生关闭事件后,我才会将其重新绑定为 True,并返回 closed 作为结果对象。
我需要访问我想要处理的事件。Pygame 的 pygame.event 模块中的 get 函数返回自上次调用该函数以来发生的事件列表。我将导入它以便在 handle_events 函数中使用。然而,get 这个名字描述性不强。仅看名字我无法判断这个函数获取的是什么。我将使用别名 get_events 导入这个函数。现在,我在 handle_events 函数中调用 get_events() 并将标识符 event_list 绑定到结果上。
因为 get_events() 返回一个事件序列,所以我需要一个事件循环来遍历这个序列。我将使用一个 for 循环。在这个版本中,我只处理关闭事件。
每个事件都有一个名为 type 的属性,它指的不是其实际的对象类型,而是绑定到一个代表事件类别的对象,例如鼠标点击、按键或关闭。在 Pygame 中,关闭事件由标识符 QUIT 表示。我将从 pygame 导入 QUIT。
现在,我将使用一个 if 语句来检查事件循环当前处理的事件类别是否为 QUIT。我将在 if 语句内将 closed 绑定为 True,并返回 closed 作为结果对象。
当我运行程序并点击关闭图标时,窗口关闭了。
绘制游戏图形
接下来让我们编写 draw_game。正如算法视频中所讨论的,draw_small_dot 和 draw_big_dot 将使用同一个函数来绘制每个点。由于我只创建了大点,我将只为大点调用 draw_dot。
draw_game 的最后一步是调用 update 来更新窗口显示,我将添加这个函数调用。
现在,我来编写 draw_dot。函数头将是 def draw_dot,因为它将被用来绘制任何点。Pygame 包含一个有用的绘制圆的函数,它叫 circle,位于 draw 模块中。但 circle 这个名字描述性不强,所以我会使用别名,将其导入为 draw_circle。
让我们看看这个函数的文档。它有四个必需的参数:一个 surface(表面)、一个 color(颜色)、一个 position(位置)和一个 radius(半径)。surface 是绘制圆的地方。每个窗口都有一个表面,可以通过调用 Pygame 窗口对象的 get_surface 方法来获取。因此,我必须在函数头的参数列表中添加 window。
draw_circle 的下一个参数是 color。Pygame 使用特定的颜色类型来表示颜色。我已经有了想要绘制的颜色的名称字符串,但我不能直接将名称字符串传递给 draw_circle 函数。我将从 pygame 导入颜色类型,就像我从 ui_game 导入 window 一样。现在,我可以使用 Color 函数,以名称字符串作为参数来创建一个颜色对象。
记住,draw_dot 函数必须足够通用以绘制任何点,所以我需要为其颜色字符串、中心点和半径使用参数。我将在 draw_dot 的参数列表中添加相应的参数:color_string、center 和 radius。
我现在可以调用 draw_circle,使用我刚创建的表面和颜色对象,以及中心点和半径。
现在我将返回到 draw_game,并为 draw_dot 添加参数:window、大点的颜色字符串、中心点和半径。这意味着我必须将 window、big_color、big_center 和 big_radius 添加为 draw_game 的参数。由于 draw_game 在 play_game 中被调用,我也将在那里添加这些参数。最后,我将在我已经完成的 play_game 和 draw_game 调用中传递所有这些对象作为参数。
我运行程序。一个蓝点显示出来了,但它不动。游戏需要更新才能让点移动。
实现游戏更新与运动逻辑
我将用移动点和控制帧率的语句替换 update_game 中的 pass 语句。我将使用一个名为 move_dot 的单一函数来移动每个点。由于我只创建了大点,目前我只进行一次调用。
现在我们来编写 move_dot 函数。第一个注释是用于创建从0到1序列的 for 循环占位符,我将使用 range 函数。回想一下,range 函数创建一个从第一个参数到第二个参数减一的序列,所以我必须使用参数 0 和 2 来更新中心的坐标。我将使用下标来访问中心和速度在当前索引(即 for 循环的目标变量)处的值。我将添加 center 和 velocity 作为参数,以便在函数内部使用。
当点碰到窗口边缘时必须反弹。如果我希望点在中心碰到右窗口边缘时反弹,我可以检查点的X坐标是否等于窗口宽度。然而,点实际上是在其边缘碰到右窗口边缘时反弹,即当点的X坐标加上其半径等于窗口宽度时为真。
由于点在一帧内可以移动多个像素,点有可能越过窗口边缘而不仅仅是触碰到它。因此,我将检查点的X坐标加上半径是否大于或等于窗口宽度,以执行从右边缘的反弹。
我将在窗口上调用 get_width 方法来获取其宽度,但为此我需要将 window 添加为参数。我还将添加 radius 作为参数。我将使用与其他函数相同的参数顺序,这样更容易记忆。
回想一下,正速度意味着点向右或向下移动,负速度意味着点向左或向上移动。我将通过取反其速度来使点反弹。
由于 move_dot 有四个参数,我将回到 update_game,并为 move_dot 添加四个对应的参数:window、big_center、big_radius 和 big_velocity。现在我将把这些参数作为参数添加到 update_game 中。
现在,我将在 play_game 中添加 update_game 的参数。虽然 play_game 已经将其中一些作为参数,但它缺少 big_velocity。我将把这个参数添加到 play_game,并在 main 中对 play_game 的函数调用中添加相应的参数。
我运行程序看看是否工作。蓝点移动了并且会反弹。然而,它移动得太快,有拖尾,并且在反弹前向下移动得太远。
反弹条件在X和Y方向都进行了检查,因为它位于 for 循环内部,该循环对索引0(X)和1(Y)都进行了求值。但是,Y方向的条件应该使用窗口高度而不是窗口宽度。
点向下移动太远是因为Y方向的反弹条件是其Y坐标加上半径大于或等于窗口宽度,而不是窗口高度。
为了对索引0检查宽度,对索引1检查高度,我将把宽度和高度放在一个 size 列表中。我再次运行程序。点现在能正确地从底部边缘反弹了。我将把左边缘和上边缘留给你处理。你只需要修改 if 条件,添加一个 or 和一个比较操作。这个比较将在索引为0时检查左边缘,在索引为1时检查上边缘。
为了改变点的速度,我需要控制帧率。我将返回到 update_game 函数,并将帧率注释替换为一个语句。在 Pygame 中,帧率由我在 main 函数中创建的时钟控制。这意味着我必须向 update_game 添加一个 clock 参数。现在我可以使用 Pygame 时钟对象的 tick 方法来设置帧率。这个方法以帧率作为参数,并防止游戏运行得比指定的每秒帧数更快。没有这个方法调用,你的游戏会以计算机允许的最快速度运行,我们已经看到这太快了。我将添加一个赋值语句将 frame_rate 绑定到90,然后我将用参数 frame_rate 调用 clock.tick()。现在我运行程序。
游戏速度好多了。你将在反思环节尝试不同的帧率。
修复图形残留问题
为什么点有拖尾?每次我调用 draw_game,它都会在更新后的位置重新绘制大点。由于我从未清除屏幕,点被一个接一个地绘制在上面。由于点在每一帧中移动的量非常小,这给人一种在窗口中绘制了拖尾的外观。
要解决这个问题,我需要在绘制新点之前擦除前一个点。我将在 draw_game 函数的开头添加一个 clear_window 调用,并再次运行程序。
除了从上边缘和左边缘反弹外,其他一切工作正常,但你会自己修复这个问题。
拖尾问题在算法层面也存在。我将更改算法,使其与程序一致。draw_game 面板有三个步骤:绘制小点、绘制大点和更新显示。在我绘制任何东西之前,我需要清除窗口,所以我将在第一个绘制步骤之前添加一个清除窗口的步骤。
并非编程时犯的每个错误都可以通过简单地更改一行代码来修复。有些问题更深层,可能根源于游戏创建过程的早期迭代,比如算法。点上的意外拖尾就是这类问题的一个很好的例子。我必须运行程序才能认识到这个问题,然后才能修复代码。我在创建算法时没有预见到这个问题。我需要使用图形库才能意识到这个问题的存在。这就是我们使用迭代过程来创建游戏的原因。在单个版本内进行编码然后修复算法的迭代没有明确显示在游戏创建过程图中,但它确实会发生。
总结
本节课中,我们一起学习了如何从零开始构建“戳点”游戏的第一版。我们完成了以下核心步骤:
- 建立框架:创建程序结构,编写函数头部注释。
- 创建窗口:使用 Pygame 初始化游戏窗口并设置属性。
- 处理事件:实现事件循环,响应用户关闭窗口的操作。
- 绘制图形:使用
draw.circle函数在窗口表面上绘制圆点。 - 实现运动:编写
move_dot函数更新点的位置,并使其在窗口边缘反弹。 - 控制游戏:使用
Clock对象控制游戏帧率,管理游戏速度。 - 修复问题:识别并解决了图形残留(拖尾)问题,这涉及到返回修改算法步骤。
核心概念和代码片段总结如下:
- 创建时钟控制帧率:
clock = pygame.time.Clock() - 游戏主循环条件:
while not close_selected: - 获取并处理事件:
for event in pygame.event.get(): if event.type == pygame.QUIT: closed = True - 绘制圆形:
pygame.draw.circle(surface, color, center, radius) - 移动并反弹逻辑(示例为右/下边缘):
size = [window.get_width(), window.get_height()] for idx in range(0, 2): center[idx] += velocity[idx] if center[idx] + radius >= size[idx]: velocity[idx] = -velocity[idx] - 控制帧率:
clock.tick(90)

现在轮到你来编程了:添加小点,并修改代码使两个点也能从上边缘和左窗口边缘反弹。
082:Poke the Dots 版本1代码回顾

在本节课中,我们将通过代码追踪的方式,回顾“Poke the Dots”游戏第一个版本的实现细节。我们将学习如何理解程序的执行流程、检查变量状态,并评估代码的软件质量。
概述
我们将从导入语句开始,逐步追踪程序的执行,观察函数调用时命名空间的变化、事件的处理、游戏帧率的影响以及图形的绘制。最后,我们将根据已建立的软件质量标准来评估此版本代码。
代码追踪与执行流程
首先,通过点击“步入”按钮开始追踪“Poke the Dots”版本1的代码,然后“步过”导入语句。
导入语句会将标识符添加到当前代码块的本地命名空间中。在堆栈数据面板的“局部变量”部分,只会显示部分导入的标识符。然而,你可以通过查看其长度来了解本地命名空间中标识符的数量。
当我步过导入 pygame 中 quit 和 color 的语句后,标识符 quit 在堆栈数据中可见,它被绑定到整数 12,而标识符 color 不可见。当导入绑定到简单对象(如字符串和整数)的标识符时,这些绑定会添加到堆栈数据中相应的命名空间。绑定到更复杂对象(如函数和模块)的标识符不会明确显示,这就是为什么你之前从未在堆栈数据中看到过这些绑定。
我将步过所有函数定义,直到遇到对 main 函数的调用。当我步入 main 函数调用时,本地命名空间现在属于 main 函数。由于 main 函数的命名空间中尚未绑定任何内容,其局部变量的长度为 0。
我将步过 main 函数中的所有语句,直到遇到对 play_game 函数的调用。此时,main 函数的命名空间中已添加了 10 个新的绑定。别忘了,你可以展开包含多个元素的对象(例如列表)以查看这些元素。例如,我可以展开 small_center 列表来查看它的两个元素。现在,小圆点的 X 坐标(即 small_center 列表的第一个元素)绑定为 50,Y 坐标(列表的第二个元素)绑定为 75。
点击“步入”按钮进入 play_game 函数调用。本地命名空间现在是 play_game 函数的命名空间。该命名空间包含该函数 10 个参数中每一个的绑定,因此这里也有 10 个绑定。我将步过 play_game 中的语句,直到遇到对 handle_events 的调用并步入其中。
这次,本地命名空间是空的,因为 handle_events 没有参数。我将步过 handle_events 中的语句,直到 event_list 被添加到本地命名空间。对 get_events 的调用会检索自上次调用 get_events 以来发生的所有事件。当我在堆栈数据面板中展开 event_list 时,你可以看到当前事件列表中的事件。在此版本的“Poke the Dots”中,除了退出事件外,所有这些事件都被忽略。检查事件列表并不能提供太多关于事件的有用信息,但它确实让我看到了正在发生的事件。
点击“步出”按钮返回到 play_game。当我步过对 draw_game 的调用时,圆点出现在游戏窗口中。我将步入 update_game。update_game 中的第一个语句是帧率赋值语句,帧率设置为 90,即每秒最多绘制 90 帧。圆点每秒在每个方向上移动 90 像素乘以速度。例如,如果速度的 X 分量为 2,则圆点每秒在 X 方向上移动 180 像素。
理解帧率的影响
更高的帧率意味着每秒绘制更多帧,因此圆点每秒移动更多像素,这会导致游戏速度更快。相反,更低的帧率会导致游戏速度更慢。我将停止追踪以试验帧率。
如果我将帧率改为 1000 并运行程序,游戏速度会快得多。如果我将帧率改为 5 并再次运行程序,游戏会变得极其缓慢。选择一个合理的帧率是创建可玩游戏的一部分。
我将帧率改回 90。为了记住更高的数字意味着更快的游戏,更低的数字意味着更慢的游戏,我将添加一种称为行内注释的新注释类型。
行内注释用于提供无法从上下文中轻易推断出的澄清信息。例如,为值的递增添加注释没有帮助,因为查看代码中的加法操作可以清楚地表明正在发生加法。然而,帧率值的含义并不明显。虽然标识符 frame_rate 让你知道整数 90 是游戏的帧率,但它并没有告诉你每秒 90 帧意味着什么。注释写着“更大的数字意味着更快的游戏”。
这个行内注释总结了帧率的工作方式,因为从标识符名称或实际的帧率对象本身并不容易看出这一点。
移动圆点的机制
让我们更仔细地看看移动圆点的代码。我将在 move_dot 函数套件中的第一个语句旁边放置一个断点,然后点击调试按钮。
对 move_dot 的第一次调用是针对小圆点的。当我在堆栈数据面板中展开 center 时,我可以看到小圆点的原始中心:X 坐标 50 和 Y 坐标 75。
点击“步过”按钮来执行这个 for 循环语句。for 循环的第一次迭代将 X 坐标增加 1。第二次迭代将 Y 坐标增加 2。这个函数在每一帧中为每个圆点调用。


我将移除这个断点,并在 handle_events 函数的 if 语句套件内放置一个新的断点。
点击调试按钮。游戏开始并正常运行。
点击关闭图标以生成一个退出事件。游戏停止,但窗口尚未关闭。在堆栈数据中,事件列表只有一个元素。我可以看到事件的地址与事件列表中单个元素的地址相同。
我将停止追踪。
绘制函数与默认参数
我想展示给你的最后一段代码是来自 pygame 的 draw.circle 函数。在编程演示中,你在文档中看到这个函数需要四个参数。
draw.circle 函数有第五个参数,称为默认参数。默认参数有一个默认值,如果在调用函数时没有为该参数提供实参,则使用该默认值。此外,省略该参数的实参不会导致错误。
draw.circle 函数的默认参数指定了圆的宽度。如果指定了宽度,则绘制一个具有该宽度的圆环。宽度的默认值是 0,这会导致绘制一个实心圆而不是圆环。
我将在调用 draw.circle 的参数列表末尾添加一个 5,然后运行程序。看,我们现在有了“戳圆环”,而不是“戳圆点”。
我将把代码改回“Poke the Dots”,并检查此版本的软件质量测试。
软件质量评估
所有旧的测试仍然适用。确保你的代码注释得当,并使用了我们已为标识符、字面量、重复和主函数制定的所有准则。
现在让我们看看用户定义函数的测试。虽然每个函数都执行一个单一的逻辑任务,可以用一句话轻松描述,并且它们都没有超过 12 条语句,但我们的几个函数有超过 5 个参数。这使得代码更难阅读和编写。一长串参数列表增加了参数顺序错误的可能性。当参数以不正确的顺序传递时,会发生此类错误,导致报告错误或不正确的程序行为。
然而,在此版本的“Poke the Dots”中,除非使用全局标识符,否则使用超过 5 个参数是不可避免的,但使用全局标识符会违反主函数软件质量测试。所以这不是一个选项。
到目前为止,使用已涵盖的编程特性,你无法修复这个软件质量缺陷。在下一个版本中,你将学习一种称为“类”的新语言特性,它可以用来解决这个问题。
总结

在本节课中,我们一起回顾了“Poke the Dots”游戏版本1的代码。我们追踪了程序的执行流程,观察了函数调用时命名空间的变化,理解了帧率对游戏速度的影响,并学习了 pygame.draw.circle 函数的默认参数用法。最后,我们根据软件质量标准评估了当前代码,并认识到参数过多的问题,为后续引入“类”的概念来解决此问题做好了准备。
Python编程与电子游戏问题解决:09_08_01:Poke the Dots 版本1的解决方案问题与改进

在本节课中,我们将分析 Poke the Dots 游戏版本1的代码,识别其中存在的解决方案问题,并探讨如何通过引入用户自定义类来改进代码质量。
上一节我们介绍了问题识别的重要性,本节中我们来看看 Poke the Dots 版本1的具体代码问题。
让我们查看 Poke the Dots 版本1的代码。其中存在一个主要问题:多个函数需要7到10个参数。这带来了不便,并且代码未能通过一项软件质量测试。用户自定义函数的参数不应超过五个。
那么,我们能做什么呢?
是时候学习用户自定义类了。在本课程中,你一直在使用类。请回忆,类和类型是同义词,你已经使用过许多类型,例如 int、list、pygame.Surface 和 pygame.Color。当你对特定的 Python 对象调用 type() 函数时,它会返回告诉你该对象所属类的信息。
你也已经学习了与类相关联的方法。例如,你已经见过将字符串转换为大写或小写的方法、检查字符串中的字符是否为数字或字母的方法等等。
在 Poke the Dots 版本2中,你将开始学习创建自己的类。
用户自定义类是封装的另一个例子。你之前已经使用封装来限制标识符的作用域。这种封装称为名称封装,因为它管理程序中的名称。
下一种封装类型称为数据封装。数据封装是一种将对象分组以创建新的单一对象的方法。
例如,列表是一组可以作为一个整体进行操作的对象。列表可以被打印、迭代遍历,并作为单个参数传递。len() 和 print() 函数以及 append() 方法都是将列表对象作为一个整体来操作的。
创建用户自定义类是解决方案泛化的另一个例子,因为新的对象类型可以作为解决方案在许多不同问题中重复使用。
由于这个版本的重点是提高代码质量,因此在下一课中,你将修改算法以使用类,而不会制定新的描述或功能测试计划。
总结

本节课中我们一起学习了 Poke the Dots 版本1代码中的主要问题:函数参数过多。我们探讨了通过引入用户自定义类来解决此问题的方向,并回顾了类、类型、方法和封装(包括名称封装和数据封装)的基本概念。在接下来的课程中,我们将应用这些知识来重构代码,提升其质量。
084:为“戳点游戏”版本2创建算法 🎮

在本节课中,我们将学习如何修改算法以使用类。我们将把游戏中的对象(如窗口和点)组织成类,使代码结构更清晰、更易于管理。
从简单对象到复合对象
上一节我们介绍了使用基本变量和列表来组织游戏数据。本节中,我们来看看如何使用类(Class)作为复合对象来访问多个相关的对象。
类可以将多个属性捆绑在一起,形成一个逻辑单元。例如,一个“人”可以拥有姓名、出生日期和身高等属性。使用带有命名属性的类比使用一个通过索引0、1、2来访问的列表要直观得多。
我们可以创建一个Person类,它包含以下三个属性:
name:绑定到一个字符串。birth_date:绑定到一个日期。height:绑定到一个整数。
在代码中,这可以表示为:
class Person:
def __init__(self, name, birth_date, height):
self.name = name
self.birth_date = birth_date
self.height = height
在算法构建器中引入类
将类引入到你的算法中,会用到算法构建器的一个新功能。
首先,将一个“类”图标拖入主面板。我将选择名称Person来为这个例子创建一个类。
这会在主程序面板的顶部创建一个Person类的图标。要编辑Person类,需要选中这个类图标,这会打开一个新的编辑面板。
此时,形状面板会显示两个可以添加到类面板的新选项:“属性”和“方法”。目前你只需要使用“属性”,我们将在后续课程中再讨论“方法”。
我必须为Person类添加属性。例如,name将是我要添加的属性之一。现在,通过将一个“属性”拖入Person类面板并选择name来添加它。
接着,我将添加birth_date和height属性。注意,我不会将person本身作为Person类的一个属性,因为person是这个复合对象的名称,而不是一个人的属性。
为“戳点游戏”创建类
现在轮到你了。你必须为“戳点游戏”创建一个Game类,并将所有将在第二个版本中使用的属性添加到这个Game类中。
例如,在第一个版本中,你创建了一个窗口(window),并在整个游戏过程中使用它。在这个版本中,窗口将成为Game类的一个属性。
请思考在版本1中,你还创建了哪些在游戏中使用的对象,以便将它们添加为Game类的属性。
同时,你还必须创建一个带有适当属性的Dot类。
算法步骤的调整
你算法中的其余步骤不会改变。然而,其中一些步骤的上下文现在有所不同。在你添加了类之后,请重新审视你的算法,以理解步骤的新含义。
例如,“创建小点”这一步,现在指的是创建你的Dot类的一个实例,并为其分配所有必要的属性。
总结

本节课中,我们一起学习了如何为“戳点游戏”引入类的概念。我们了解了类作为复合对象如何更好地组织数据,并在算法构建器中实践了创建Game类和Dot类。通过将游戏中的实体(如窗口和点)定义为类的属性,我们为编写更结构化、更易维护的代码打下了基础。下一节,我们将继续完善这个基于类的游戏算法。
085:Python类定义 🐍

在本节课中,我们将学习Python中一个强大的新语句——类定义。类定义用于创建新的用户自定义类型,它允许我们将多个对象组合成一个复合对象。我们还将学习如何将属性引用作为赋值语句的目标,从而为对象动态添加属性。
概述 📋
类定义是Python中创建自定义数据类型的基础。通过定义类,我们可以将相关的数据和功能封装在一起,使代码更加模块化和易于理解。本节我们将通过一个矩形相交检测的例子,来展示如何使用类来改进程序的结构和可读性。
类定义的基本概念
一个用户定义的类(或类型)允许我们将多个对象组合成一个单一对象。Python的类定义语句用于创建这种新的用户定义类。
考虑下面这个检查两个矩形是否相交的程序。用于检查相交的公式不是本节课的重点,但你可以自行研究其工作原理。
def rectangles_intersect(corner1_x, corner1_y, width1, height1, corner2_x, corner2_y, width2, height2):
return not (corner1_x > corner2_x + width2 or
corner1_x + width1 < corner2_x or
corner1_y > corner2_y + height2 or
corner1_y + height1 < corner2_y)
print(rectangles_intersect(5, 10, 15, 20, 10, 5, 20, 15))
程序报告第一个矩形(左上角在(5,10),宽15,高20)与第二个矩形(左上角在(10,5),宽20,高15)确实相交。尽管我们将其视为计算两个矩形对象是否相交,但程序中并没有显式的矩形对象。相反,程序使用了八个整数对象来描述两个隐式的矩形。
这既模糊了程序的意图,也模糊了实现代码。例如,rectangles_intersect函数的参数列表非常长,在调用和定义时都容易出错。如果程序中存在两个显式的矩形对象,程序将更容易阅读、编写和修改。
使用类定义改进程序
我已经使用类定义语句创建了一个矩形类型,并重写了程序以使用这个类型。
class Rectangle:
pass
def create_rectangle(corner_x, corner_y, width, height):
rec = Rectangle()
rec.x = corner_x
rec.y = corner_y
rec.width = width
rec.height = height
return rec
def rectangles_intersect(r1, r2):
return not (r1.x > r2.x + r2.width or
r1.x + r1.width < r2.x or
r1.y > r2.y + r2.height or
r1.y + r1.height < r2.y)
def main():
rec1 = create_rectangle(5, 10, 15, 20)
rec2 = create_rectangle(10, 5, 20, 15)
if rectangles_intersect(rec1, rec2):
print("overlap")
main()
运行这个程序同样会显示“overlap”。我为每个矩形对象使用了四个属性:x、y、width和height。其中x和y是矩形左上角的整数坐标。
类定义语句的语法和语义
以下是类定义语句的简化语法图:
class <identifier>:
<suite>
标识符被称为类名。
以下是类定义的简化语义规则:
- 使用一个新的局部命名空间和全局命名空间来评估类体(suite)。
- 创建一个类对象,并使用新的局部命名空间作为该类对象的命名空间。
- 如果类名不在原始的局部命名空间中,则添加它。
- 在原始的局部命名空间中将类名绑定到新的类对象。
我将使用矩形程序来展示类定义语义是如何工作的,并强调新的局部命名空间与原始局部命名空间之间的区别。
当程序启动时,局部代码块是主模块,其命名空间既是局部命名空间也是全局命名空间。预绑定的标识符print就绑定在这个命名空间中。
主模块中的第一个语句是函数定义,因此解释器应用函数定义语义。它创建一个具有自己命名空间的函数对象,将标识符main添加到局部命名空间,将main绑定到函数对象,并在main函数的命名空间中绑定一个指向主模块命名空间的引用。
由于接下来的两个语句也是create_rectangle和rectangles_intersect的函数定义,解释器为每个函数重复这些函数定义语义。
下一个语句是类定义,因此解释器应用类定义语义。
第一步,解释器创建一个新的空命名空间作为局部命名空间。它使用这个局部命名空间和当前的全局命名空间来评估类体。
回想一下,函数定义中的类体在函数定义被评估时并不会被评估。相反,函数定义的类体只有在函数被调用时才会被评估。然而,类定义的语义规定,类定义的类体在类定义被评估时就会被评估。由于类体只包含一个pass语句,而pass语句什么都不做,所以类体评估完成。
第二步,创建一个新的类对象,其命名空间就是新的、空的局部命名空间。
第三步,类名Rectangle被添加到原始的局部命名空间,即主模块的命名空间。
第四步,Rectangle在这个原始的局部命名空间(主模块)中被绑定到新的类对象。
主模块中的最后一个语句是对main的函数调用,因此解释器应用函数调用语义。标识符main在局部命名空间中被解引用以获得main函数对象。main函数代码被评估以获得结果对象。
对象创建与属性赋值
现在,局部代码块是main函数,局部命名空间是main函数命名空间。第一个语句是一个赋值语句,其表达式是对create_rectangle的函数调用。
使用熟悉的函数调用语义在全局命名空间中找到函数对象create_rectangle,评估四个参数表达式5、10、15和20,并将它们放入参数列表。然后,解释器将参数corner_x、corner_y、width和height添加到create_rectangle的命名空间,将每个参数绑定到其对应的实参,并评估create_rectangle的代码。
现在,局部代码块是create_rectangle函数,局部命名空间是create_rectangle函数命名空间。create_rectangle的第一个语句是一个赋值语句,其表达式是对名为Rectangle的函数的调用。
当应用函数调用语义时,标识符Rectangle在全局命名空间中被用来查找Rectangle对象,它是一个类。在Python中,类是一种函数。到目前为止,你已经见过几种函数:像len这样的内置函数,像create_rectangle这样的用户定义函数,以及像lower这样的内置方法。类是另一种函数,它返回一个类型为该类的新对象。例如,不带参数调用int()会创建整数对象0,不带参数调用str()会创建空字符串对象。不带参数调用Rectangle()也会创建一个矩形对象,但它没有一个非常美观的人类可读形式。
大多数Python文档使用术语“可调用对象”(callable)而不是“函数”来描述类和方法。然而,无论我们说类是一个函数还是一个可调用对象,它都像函数调用一样被评估,并且它创建一个类型为该类的新对象。
create_rectangle中的赋值语句将标识符rec绑定到由那个Rectangle()调用创建的新矩形对象。
属性引用作为赋值目标
第二个语句是一个赋值语句,其中目标rec.x是一个属性引用。在“方法调用”课程中,你看到了属性引用用作表达式(如"Hello".lower)的语法和语义。在“导入语句的替代形式”课程中,你使用属性引用来访问模块中的函数(time.sleep)和模块中的模块(os.path)。然而,在本课程中,你还没有见过属性引用用作赋值语句的目标。
当前赋值语句的语法图支持的目标要么是标识符,要么是下标表达式。以下是一个广义的赋值语句目标语法图,它也支持属性引用作为目标。
<target> ::= <identifier> | <subscription> | <attribute_reference>
以及现有的属性引用语法图:
<attribute_reference> ::= <expression> "." <identifier>
最后,以下是当目标是属性引用时的赋值语义规则:
- 评估赋值语句的表达式以获得结果对象。
- 评估属性引用表达式以获得基对象。
- 如果标识符不在基对象的命名空间中,则添加它。
- 在基对象的命名空间中将标识符绑定到结果对象。
我将把这个语义规则应用到create_rectangle中的第二个赋值语句。
第一步,表达式corner_x被评估以获得结果对象整数5。
第二步,表达式rec被评估以获得基对象,即一个矩形。
第三步,标识符x不在基对象(一个空的矩形对象)的命名空间中,因此x被添加到这个命名空间。
第四步,矩形对象命名空间中的标识符x被绑定到结果对象整数5。
一个名为x的新属性已被添加到矩形对象,并且这个新属性已被绑定到整数对象5。Python允许程序员在可以引用包含该属性的对象的代码中的任何地方创建新属性。
接下来的三个赋值语句被类似地解释。注意,参数名width和height恰好与属性width和height相同,但这没关系。另外两个属性x和y与参数corner_x和corner_y不同,后者用于访问实参对象。
return语句将矩形对象返回给调用函数main,main将标识符rec1添加到其命名空间,并将rec1绑定到这个矩形对象。
main函数中的下一个赋值语句被类似地解释,使得标识符rec2被绑定到第二个矩形对象。
使用属性改进代码清晰度
main函数中的下一个语句是一个if语句,其条件调用了函数rectangles_intersect。标识符rectangles_intersect在全局命名空间中被解引用,它的两个参数表达式被评估以获得两个矩形对象,并将它们放入参数列表。然后,解释器将参数r1和r2添加到rectangles_intersect的命名空间,将它们绑定到实参矩形,并评估rectangles_intersect的代码。
现在,局部代码块是rectangles_intersect函数,局部命名空间是rectangles_intersect函数命名空间。这个函数包含一个带有复杂表达式的单个return语句。
每个属性引用,如r1.x,都使用“方法调用”课程中的属性引用语义规则进行评估。
第一步,表达式r1在局部命名空间中被评估以获得第一个矩形对象。
第二步,属性x在这个矩形对象的命名空间中,因此它被解引用以获得整数对象5。
类似地,当评估属性引用r2.width时,表达式r2在局部命名空间中被评估以获得第二个矩形对象。属性width在这个矩形对象的命名空间中,因此它被解引用以获得整数对象20。
return语句中表达式的评估结果是布尔对象True。因此,True被返回给调用函数main,并显示字符串“overlap”。
类与列表的对比
你已经看到序列可以用来引用多个对象。我可以在程序中使用列表,而不是创建一个矩形类型。
以下是一个具有相同功能的使用列表的程序:
def create_rectangle_list(corner_x, corner_y, width, height):
return [corner_x, corner_y, width, height]
def rectangles_intersect_list(r1, r2):
return not (r1[0] > r2[0] + r2[2] or
r1[0] + r1[2] < r2[0] or
r1[1] > r2[1] + r2[3] or
r1[1] + r1[3] < r2[1])
rec1_list = create_rectangle_list(5, 10, 15, 20)
rec2_list = create_rectangle_list(10, 5, 20, 15)
if rectangles_intersect_list(rec1_list, rec2_list):
print("overlap")
尽管列表程序更短,但rectangles_intersect函数中的数字索引比属性名x、y、width和height难理解得多。事实上,在我正确翻译之前,我尝试了五次才成功地将前一个矩形程序中的不同组件转换为索引。最后,我制作了一个图表来增加我对翻译正确的信心。
如果我想向这个程序添加更多操作矩形的函数,我需要保留这个图表来记住哪个索引代表哪个组件。在编写了更多操作复合对象的函数的程序中,使用属性而非列表所带来的清晰度提升将变得更加明显。
总结 🎯
本节课我们一起学习了Python中的类定义语句,它用于创建新的Python类或类型。类定义允许我们将多个相关的对象组合成一个复合对象,从而显著提高代码的可读性、可维护性和模块化程度。
我们还学习了如何泛化赋值语句的语法和语义,以支持将属性引用作为赋值目标。这使得我们可以动态地为对象添加和修改属性,为对象提供灵活的数据结构。
通过将隐式使用多个简单对象(如整数)表示复合概念(如矩形)的程序,重构为使用显式类定义和对象属性的程序,我们看到了面向对象编程在表达程序意图和简化代码逻辑方面的优势。与使用列表和数字索引相比,使用具有命名属性的对象使得代码的意图更加清晰,更不容易出错。

掌握类定义是迈向编写更复杂、更结构化Python程序的关键一步。
086:编程“戳点”游戏第二版 🎮

在本节课中,我们将学习如何将“戳点”游戏的第一版代码重构为第二版。我们将引入类的概念,将游戏和点封装成对象,从而简化函数参数传递,使代码结构更清晰、更易于管理。
概述
上一节我们完成了“戳点”游戏第一版的编程。本节中,我们将开始第二版的开发。核心变化是使用类来组织游戏和点的数据与行为。我们将创建一个 Game 类和一个 Dot 类,用对象属性替代之前分散的变量。
开始编程
我将从第一版函数定义的下方开始,编写 Game 类的定义。
定义Game类
首先,我编写类的定义头,即 class Game:。在类内部,我先添加一个 pass 语句占位。和函数一样,每个类也需要一个描述性注释。
class Game:
"""此类的对象代表一个完整的游戏。"""
pass
我还将为每个属性添加注释。在每个注释中,我会将算法中的一个属性描述翻译成有效的Python标识符。
在第一版中,“创建游戏面板”的每一步都被翻译成了 main 函数内的一系列赋值语句。在这一版中,这些赋值语句将被移除,取而代之的是创建一个 Game 对象和两个 Dot 对象。
创建create_game函数
我将编写一个新的函数 create_game,用于创建 Game 对象并为其属性赋值。你将在 create_game 函数内部创建两个 Dot 对象。
在 create_game 函数内部,我通过将 Game 类名作为函数调用来创建一个 Game 对象,并将标识符 game 绑定到这个新对象。
def create_game(window):
game = Game()
我的 Game 对象的第一个属性应该是窗口,因此我将 window 作为参数添加到函数定义中。
现在,我将 game 对象的 window 属性绑定到传入的 window 参数对象。
game.window = window
一个新的 window 属性被添加到了 game 对象,并且这个新属性绑定到了一个窗口对象。
请记住,Python允许程序员在可以引用包含该属性的对象的任何代码位置创建新属性。
参数名称并不重要。如果我使用一个名为 fred 的参数而不是 window,我需要将 game.window 绑定到 fred 对象。然而,fred 不是一个描述性的名称,所以我会将参数改回 window。
我还将 game.close_selected 属性绑定到 False。
game.close_selected = False
两个点是 game 对象的属性,但它们需要自己的属性,因此你将使用一个单独的函数来创建点。
创建Dot对象
你可以创建一个 create_dot 函数,并调用它两次来创建两个点。这类似于调用单个 draw 函数两次来绘制两个点。
我将 game.small_dot 属性绑定到调用 create_dot 的结果。你必须为 create_dot 添加适当的参数。
game.small_dot = create_dot(...) # 参数待补充
game.big_dot = create_dot(...) # 参数待补充
create_game 函数应该返回 game 对象,因此我将添加一个 return 语句。
return game
修改main函数
现在,我将回到 main 函数,移除那些创建点和时钟的旧赋值语句,因为它们将被对 create_game 函数的调用所取代。
我将添加这个调用,并传递 window 作为参数。我还将一个新的标识符 game 绑定到 create_game 的结果。
def main():
window = create_window(...)
game = create_game(window)
现在,我可以将单个 game 对象传递给 play_game 函数,而不是传递那一大堆单独代表游戏每个方面的参数。
play_game(game)
你将在下一个活动中完成 create_game 函数,但你已经可以看到,传递一个包含许多属性的单个对象比传递那么多单独的对象要容易得多。
修改play_game函数
接下来,我将修改 play_game 函数的定义。我要做的第一件事是用单个参数 game 替换冗长的参数列表。
def play_game(game):
由于 game 对象现在作为参数传递,我不再需要局部标识符 close_selected,相反,我可以使用 game 对象的 close_selected 属性。
类似地,handle_events 函数不再需要返回 close_selected 的更新值。相反,我可以通过 handle_events 的副作用来重新绑定 game 对象的 close_selected 属性。
close_selected 属性在可以访问 game 对象的任何地方都是可访问的。
我还将 game 作为参数添加到 handle_events 函数的定义中。
def handle_events(game):
现在,我将移除那些绑定和重新绑定 close_selected 的代码行,因为我可以从 game 对象访问 close_selected 属性。
当事件类型是退出时,我只需将 game.close_selected 重新绑定为 True。
if event.type == QUIT:
game.close_selected = True
完成版本2的编程
你需要自己完成第二版的编程。你需要完成绑定 game 的属性、创建一个 Dot 类并绑定点的属性。然后,你就可以在其他函数中使用这些属性。
例如,当你在 update_game 中调用 move_dot 时,你将能够传递 game.small_dot 作为参数,而不是传递小点的那一堆组件。
在 move_dot 函数内部,你可以使用点的属性,而不是第一版中使用的那些单独的参数对象。

随着编程经验的积累,你会看到类是多么方便。
总结
本节课中,我们一起学习了如何为“戳点”游戏第二版搭建框架。我们引入了 Game 类 来封装游戏状态,并计划引入 Dot 类。通过使用对象,我们显著简化了函数间的参数传递,使代码结构更模块化、更易于理解和维护。这是面向对象编程思想的一次初步实践。
087:Poke the Dots 版本2代码回顾


在本节中,我们将回顾Poke the Dots游戏版本2的代码。我们将通过调试器跟踪程序的执行,观察Game和Dot类对象是如何被创建和使用的,并学习如何通过属性引用在函数间传递多个对象。最后,我们将检查新代码是否符合用户定义类的软件质量测试标准。
🎼 代码结构与调试入门
以下是版本2的代码。通过引入新的Game和Dot类,我们成功通过了版本1中失败的软件质量测试。
现在,查看play_game和update_game函数。它们的参数数量已从原来的7个和8个分别减少到各1个,这是一个显著的改进。
为了理解这是如何实现的,我们需要深入查看Game和Dot对象内部的属性。我们将通过设置断点并使用调试器来逐步执行代码。
🔍 深入调试:观察对象的创建
首先,在create_game函数的第一个语句旁添加一个断点,然后按下调试按钮。
高亮的语句包含一个对Game类的函数调用,该调用会创建一个新的Game对象。按下“步入”按钮。
由于我们尚未为Game类提供任何代码,调试器不会显示Game函数的Python代码。从之前的课程中我们知道,每个类对象同时也是一个函数对象。在未来的版本中,你将看到如何向同时也是类的函数对象添加代码。
尽管Game函数被求值,但一个新的Game对象已被创建。赋值语句将标识符game绑定到这个新对象。
你可以看到几个以双下划线开头的属性。我们暂时忽略这些属性。
在Game类注释中列出的属性,例如window和small_dot,目前还不在Game对象中。当我们将这些对象绑定到属性时,它们才会被添加到Game对象。
当我按下“步过”按钮时,栈数据显示一个window属性已被添加到Game对象,并绑定到我们传入函数的window对象。
接下来的三个赋值语句也发生了同样的情况。
现在,Game对象拥有了三个新属性:clock、close_selected和frame_rate。
当前高亮的语句创建了一个新的Dot对象,并将其绑定到Game的一个属性引用上。
🎯 创建Dot对象
我按下“步入”按钮来访问create_dot函数。
create_dot函数开始时,其局部命名空间中有五个参数。
第一个赋值语句创建了一个Dot对象。当我步过这个语句时,dot被添加到局部变量中,并绑定到新的Dot对象。
我继续步过接下来的五个语句,为新的Dot对象添加属性。在局部变量中,你可以看到所有五个属性都已添加到dot对象,并绑定到五个参数对象。


例如,属性velocity和参数speed都绑定到同一个列表对象,因为它们的地址相同。


随着small_dot创建完成,我将其返回到create_game函数。在栈数据中,你可以看到small_dot已作为属性添加到Game对象。
我步过big_dot的创建语句,并再次检查栈数据。
可以看到,big_dot也已作为Game的属性添加。最后,我步出函数,将Game对象返回到main函数。
正如你在栈数据中看到的,main函数在其局部命名空间中引用了两个对象:window和game。
🕹️ 游戏运行与属性使用
我步入play_game函数。你可以在play_game的局部命名空间中看到game对象参数。展开game对象,你可以看到在create_game中添加的属性,例如game.close_selected是False。
在程序的这个阶段,我们已经设置了许多Game和Dot属性,但尚未使用它们。while语句是我们第一次使用先前设置的属性。
设置一个属性却从不使用它是没有意义的。
属性也可以用来记录对象发生的变化。在handle_events函数中,当关闭事件发生后,game的close_selected属性会被重新绑定,这样while语句就能识别这个变化并停止程序。
Dot对象的center和velocity属性也被用来记录程序中的重要变化。
相比之下,game的window属性在多个地方被使用,但从未被改变。
我向下滚动到draw_game函数,在第一个draw_dot函数调用旁放置一个断点,然后按下调试按钮跳转到那里。
game对象已被传入draw_game函数,因此你可以在局部命名空间中看到它。高亮的draw_dot函数使用了另一个属性引用来访问game的small_dot属性,并将其传递给draw_dot函数。
步入draw_dot函数,你可以看到dot(而不是game)已被添加到局部命名空间。当前高亮的语句是另一个属性引用,它引用了之前绑定的dot对象的window属性。
它使用了window类关联的get_surface方法,并将一个局部标识符绑定到其结果上。
非常巧妙。属性引用非常适用于在函数间传递多个对象,而无需为你的函数设置多个不同的参数对象。
它们也非常适合记录程序的重要变化。

跟踪到此结束。


✅ 用户定义类的软件质量测试

让我们看看为用户定义类新增的软件质量测试。
在注释部分,有一些针对用户定义类的新测试:你创建的每个类都应该有一个注释,说明该类的用途。
在标识符和名称部分,用户定义类的名称应使用大写单词;这是Python的风格约定。
最后,为用户定义类新增了一个测试类别。
该测试询问你的类是否代表一组用于两个或更多任务的对象,并形成一个单一的概念对象。
让我们检查新的Dot和Game类是否通过了这些新测试。
每个类都有一个以大写字母开头的名称,并且都以描述其用途的类注释开头。


Game类代表了构成整个Poke the Dots游戏的六个对象组。它在多个函数中被使用,例如main、play_game和handle_events。
Dot类代表了窗口中的一个圆形对象,它具有颜色、中心点、半径和速度。有多个Dot对象在多个函数中被使用,例如update_game、draw_dot和move_dot。
你应该编辑你的代码以确保满足这些测试要求。在未来的课程中,将有更多测试被添加到“用户定义类”部分。
📝 本节总结
版本2的回顾到此结束。恭喜你!在下一个版本中,你将添加更多功能,使你的游戏更加有趣。
在本节课中,我们一起学习了:
- 如何使用调试器跟踪
Game和Dot类对象的创建过程。 - 属性引用如何简化函数间的数据传递。
- 如何利用属性记录程序状态的变化。
- 用户定义类需要满足的软件质量测试标准,包括清晰的注释、遵循命名约定以及代表一个有用的概念对象组。
088:Poke the Dots版本2解决方案问题分析 🎯
在本节课中,我们将分析Poke the Dots游戏版本2的解决方案中存在的问题,并了解版本3需要改进的方向。

概述
上一节我们介绍了如何使用类来改进代码结构。本节中,我们来看看版本2游戏在功能和可玩性方面存在的核心问题。
版本2代码质量与功能缺失
你使用了类来提高代码质量,但游戏仍然缺乏使其可玩的大部分功能。
玩家除了关闭窗口外,无法与游戏进行任何交互,并且分数没有显示。
在版本3中,你将改变这两点。
事件处理的概念
回忆一下,事件是游戏中发生的某件事。它可以是玩家操作的结果,也可以是计算机中某个进程触发的。
在之前的版本中,你只处理了当玩家点击关闭图标时发生的关闭事件。
在这个版本中,你将通过处理一个额外的玩家触发事件来使游戏更具交互性:鼠标点击。
在Poke the Dots中,你将只处理关闭和鼠标点击事件。
Pygame支持许多其他事件,包括按键按下。
处理其他事件与处理鼠标点击类似。
你将在Poke the Dots这个版本的反思中遇到其他类型的事件。
总结

本节课中我们一起学习了Poke the Dots版本2解决方案的主要问题:缺乏玩家交互和分数显示。我们明确了版本3的改进目标,即引入鼠标点击事件处理来增强交互性,并开始理解游戏事件的基本概念。下一节我们将着手实现这些新功能。
089:观察“点戳游戏”版本3 👾
在本节课中,我们将要观察和分析“点戳游戏”的第三个版本。我们将了解这个版本相比之前版本新增的功能,特别是计分板和点随机传送的机制。

游戏概述
让我们来看看“点戳游戏”的第三个版本。它与第一和第二个版本有相似之处。
核心行为观察
上一节我们介绍了游戏的基本框架,本节中我们来看看版本3的具体表现。
两个圆点会移动,并且在碰到窗口边缘时会反弹。当两个圆点相互碰撞时,游戏不会停止。
在第三个版本中,窗口角落会出现一个计分板。它显示的是游戏开始后经过的秒数。
当我按下鼠标左键时,两个圆点会消失,然后在窗口内的不同位置重新出现,即它们会进行“传送”。
以下是游戏中的关键交互列表:
- 圆点持续移动并会在窗口边缘反弹。
- 圆点碰撞不会导致游戏结束。
- 屏幕角落显示一个以秒为单位的计时计分板。
- 点击鼠标左键会使两个圆点随机传送到窗口内的新位置。
- 点击窗口的关闭按钮可以关闭游戏窗口。
我再多按几次鼠标按钮。每次按下,两个圆点都会传送到新的随机位置。
当然,按下窗口上的关闭按钮可以关闭窗口。
学习任务
请试玩这个版本,以熟悉这些新的变化。然后,请完成你更新的游戏描述和功能测试计划。


本节课中我们一起学习了“点戳游戏”版本3的新特性。我们观察到游戏增加了实时计分(计时)功能,并引入了通过鼠标点击使圆点随机传送的交互机制,这为游戏增添了新的变化和挑战。
090:为“戳点游戏”版本3创建算法

🎯 概述
在本节中,我们将更新“戳点游戏”版本3的算法。我们将学习如何处理鼠标点击事件,以实现圆点的瞬间移动功能。同时,我们也会探讨在绘制多个游戏对象时,如何安排它们的绘制顺序。
🔄 更新算法以处理鼠标点击事件
上一节我们介绍了游戏的基本框架,本节中我们来看看如何为版本3添加新的交互功能。
首先,我们需要在算法中导航到“处理单个事件”的面板。我们不会更改“关闭事件”的判断步骤,因为游戏仍需处理窗口关闭事件。
我们将为“非关闭事件”的假值分支添加处理逻辑。展开该分支的假值框,并添加一个新的判断图标。在算法中,展开一个判断图标的假值分支并添加另一个判断图标,这相当于代码中的 elif 子句。
对于这个新的判断,我们选择的条件是:事件类别等于鼠标点击。
在该判断的真值框中,我们添加文字“处理鼠标点击”,然后将此步骤展开为它自己的独立处理面板。
以下是算法更新的核心步骤:
- if event.type == pygame.QUIT:
- 处理关闭事件。
- else:
- if event.type == pygame.MOUSEBUTTONDOWN:
- 处理鼠标点击事件。
- if event.type == pygame.MOUSEBUTTONDOWN:
本次演示到此为止。在接下来的实践活动中,你将完成整个算法,包括实现圆点的瞬间移动以及添加新的计分板。
🖼️ 游戏对象的绘制顺序
当你在游戏中绘制多个对象时,每个后绘制的对象都会覆盖在先绘制的对象之上,并可能遮挡其部分内容。
因此,你需要通过实际游玩游戏来确定计分板应该在圆点之前还是之后绘制。这取决于你希望计分板显示在圆点下方作为背景,还是覆盖在圆点上方作为UI界面。

📝 总结
本节课中我们一起学习了如何为“戳点游戏”版本3更新算法,重点引入了对鼠标点击事件的处理逻辑。我们还了解了多个游戏对象绘制顺序的重要性,这决定了它们在屏幕上的最终视觉效果。在接下来的实践中,你将应用这些知识来完成游戏的增强功能。
091:编程实现“戳点游戏”版本3 🎮
在本节课中,我们将学习如何为“戳点游戏”的第三个版本编写代码。我们将重点实现使用经过的时间来更新分数的功能,并学习如何处理鼠标点击事件以使圆点“传送”。大部分编程工作将由你完成,但本节会提供关键的指导和提示。

上一节我们介绍了游戏的基本框架,本节中我们来看看如何实现更动态的分数系统和交互功能。
使用经过时间更新分数 ⏱️
算法步骤“使用经过的时间更新分数”可以通过调用 PyGame 时间模块中的 get_ticks 函数来实现。
这个函数返回自游戏窗口创建以来所经过的毫秒数。
你必须将毫秒转换为秒,然后将这个整数转换为字符串对象以便在屏幕上显示。
核心代码公式如下:
# 获取自程序开始以来的毫秒数
milliseconds = pygame.time.get_ticks()
# 转换为秒
seconds = milliseconds // 1000
# 转换为字符串以便渲染
time_string = str(seconds)
实现鼠标点击传送功能 🖱️
我们希望圆点在鼠标点击后能够“传送”。PyGame 支持三种鼠标事件。
以下是三种主要的鼠标事件:
- 鼠标移动事件:每当鼠标移动时生成。
- 鼠标按键按下事件:当鼠标按键被按下时生成。
- 鼠标按键释放事件:当鼠标按键被释放时生成。
一次完整的鼠标点击直到鼠标按键被释放时才被认为发生。因此,你应该使用 MOUSEBUTTONUP 事件来检测一次点击。
核心代码概念:
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP:
# 在这里处理点击逻辑,例如重置圆点位置
dot.center = event.pos # 将圆点中心传送到点击位置
本节课中我们一起学习了如何利用 pygame.time.get_ticks() 来创建基于时间的分数系统,以及如何通过检测 MOUSEBUTTONUP 事件来实现对鼠标点击的响应。你现在已经掌握了开始编程实现游戏版本3所需的所有关键知识。

祝你编程愉快!🚀
092:Poke the Dots 版本3代码回顾 🎮

在本节课中,我们将回顾 Poke the Dots 游戏版本3的代码。我们将重点学习如何扩展事件处理模板,以处理键盘按键这类新的事件类别。通过一个具体的例子,你将看到如何让游戏在按下特定按键(如‘P’键)时,产生与鼠标点击相同的效果。
扩展事件处理模板
上一节我们建立了稳固的事件处理模板。本节中我们来看看如何轻松地添加新的事件类别。
尽管 Poke the Dots 的最终版本不会处理按键事件,但本示例将展示如何处理按键事件,以便你能在自己的项目中应用。
在这个例子中,每当我按下 P 键,圆点就会像鼠标点击时一样进行传送。
我们需要为每个新的事件类别导入一个 Pygame 标识符。我将从 Pygame 模块导入 KEYDOWN 事件类别。
KEYDOWN 是一个事件类别,每当键盘上的一个键被按下时就会生成。
添加键盘事件处理
在 MOUSEBUTTONUP 事件的 if 语句块下方,我将添加一个新的子句:if event.type == KEYDOWN。这次,我将事件的 type 属性与导入的 KEYDOWN 类别进行比较。
为了响应这个事件,我会再次调用 handle_mouse_up 函数。我也可以创建一个名为 handle_key_down 的新函数,并将 handle_mouse_up 中的两个随机化圆点的函数调用复制进去。然而,由于我希望两个事件产生相同的效果,不如为两个事件处理器使用同一个函数。
运行游戏。当我按下 P 键时,圆点传送了,效果很好。但当我按下其他键(如 Q 键)时会发生什么?圆点仍然会传送,这就不太好了,因为我只希望 P 键影响圆点。
筛选特定按键
KEYDOWN 事件在按下任何键时都会生成,但这个程序应该只响应单个特定的键。
每个事件都有一个 type 属性,代表其类别(如 MOUSEBUTTONUP 或 KEYDOWN)。每个事件类别还有其他属性,代表该事件的特性。
例如,鼠标光标的位置由 MOUSEBUTTONUP 事件的 pos 属性表示。pos 绑定了一个元组,包含事件发生时鼠标光标的 X 和 Y 坐标。
KEYDOWN 事件的 key 属性代表生成事件的按键。我必须将 event.key 与一个键值进行比较。每个属性的值都必须从 Pygame 导入。
以下是 Pygame 中 key 属性值的文档。P 键被标记为 K_p(一个大写的 K 后跟下划线和一个小写的 p)。我将导入这个代表小写或大写 P 的属性值。
在 if 语句块内,我将添加一个 if 语句,其子句为 if event.key == K_p。我将把对 handle_mouse_up 函数的调用移入这个 if 语句块内。
再次运行游戏。当我按下 P 键时,圆点传送。但当我按下其他不同的键时,圆点不会传送。
调试与检查

为了检查 key 属性,我将在 KEYDOWN 事件内的 if 子句旁边设置一个断点。然后,我将按下调试按钮并按下 P 键,这会使调试器停在 if event.key == K_p 旁边。
在堆栈数据中,我将在本地变量中展开事件对象。注意事件的 key 属性是整数 112,这与导入到全局变量中的 K_p 标识符的值相同。
同时注意事件的 unicode 属性是 ‘p’,因此我可以轻松看到按下了哪个键,这很酷。
我再次按下调试按钮,这次按下 Q 键。现在,堆栈数据显示一个 key 属性为 113、unicode 属性为 ‘q’ 的事件,它不等于 K_p,因此圆点不应传送。我再次按下调试按钮,圆点继续移动而没有传送。
我停止跟踪。
代码结构建议
我本可以将 if 子句与 if 子句合并为 if event.type == KEYDOWN and event.key == K_p。然而,为整个 KEYDOWN 事件类别设置一个 if 子句,并在其中包含多个 if 子句来检查不同的属性值,可以让我为不同的按键执行不同的操作。
通常,你应该为每种事件类型设置一个 if 子句,其中包含多个 if 子句来检查不同的属性值。
总结与测试
如你所见,通过为每个事件类别添加更多的 if 子句,可以很直接地向事件循环中添加更多的事件类别。
这个版本的 Poke the Dots 没有新的软件质量测试,但请确保你的代码通过了之前的所有测试。

恭喜,你已经完成了 Poke the Dots 版本3!本节课中,我们一起学习了如何扩展事件处理循环以响应键盘事件,并掌握了如何筛选特定的按键来触发游戏逻辑。
093:Poke the Dots版本3的解决方案问题

概述
在本节课中,我们将分析Poke the Dots游戏版本3存在的代码质量问题,并学习如何通过数据封装和信息隐藏来改进代码结构。我们将重点理解类、方法以及如何通过封装来提升代码的可维护性和可复用性。
版本3最明显的问题
上一节我们介绍了版本3的基本功能。本节中我们来看看版本3存在的一个明显问题:当两个圆点碰撞时,游戏不会结束。不过,这个问题将留到后续版本解决。版本4将专注于提升代码质量。
代码质量与数据封装
在版本2中,我们通过添加类,使代码通过了“函数参数不应超过五个”的软件质量测试。在版本3中,我们讨论了类作为一种数据封装形式,因为它们将组件对象组合成一个实体使用。
然而,封装还规定,被封装对象的内部细节应该被隐藏。因此,我们不应该在类定义之外访问任何对象的属性。
目前,Game和Dot类的数据封装实现并不完整,因为这些类的属性在类定义之外被绑定和访问。
如何修复:在类内部绑定属性
要修复这个问题,必须在当前为空的Game和Dot类定义中添加代码,以便在这些定义内部绑定属性。
有些属性,例如窗口的标题和大小,或圆点的颜色和中心点,在不同的窗口和圆点之间会有所不同。以window函数为例,它有参数来传递标题和大小对象,以便用于创建合适的窗口对象。
如果你为Game和Dot类的初始化方法(如__init__)添加参数,这些参数就可以在对象创建时用于绑定属性。
其他属性也可以在对象创建时绑定,但不需要作为参数传递。一个例子是游戏的帧率,因为程序中的每个游戏对象都使用相同的固定帧率,所以在创建游戏对象时不需要将其作为参数传递。
使用方法来修改属性
如果一个属性需要在程序运行期间被修改,例如窗口的字体大小,封装原则禁止在类定义之外重新绑定该属性。因此,应该使用一个方法来改变这个属性。具体来说,set_font_size方法用于改变窗口的字体大小。
方法是在类内部定义的函数。方法允许你修改和返回关于对象的信息,而无需在类定义之外直接访问属性。
封装行为与数据
方法和类不仅封装数据,也封装行为。行为封装指的是将所有可以应用于对象的行为或动作分组,就像数据封装将所有关于对象的信息分组一样。
例如,考虑Python内置的str类。它有各种方法,如检查字符串是大写还是小写、是否包含数字或字母等等。这些方法构成了字符串对象的行为,因为这些方法决定了你在程序中可以对字符串做什么。
信息隐藏的概念

在类定义中封装数据和行为,使用了一个叫做“信息隐藏”的设计技术。信息隐藏是一种限制对数据和代码细节访问的设计技术。

使用类就是告诉程序员(包括你自己),不要在类定义之外修改类的实现细节。
一个协作编程的例子
想象一下,你被分配编写Poke the Dots游戏的Game和Dot类,而另一位程序员负责代码的其余部分。
你会为Game类编写一个play方法,并为两个类都编写draw方法。然后,另一位程序员将能够在程序的主函数中调用Game类的play方法。Game类中的play方法会调用两个类中的适当方法。
如果你被要求将游戏更新为“Poke the Stars”而不是“Poke the Dots”,你需要修改你的类:你可以通过修改Dot类的内部细节(例如其属性和draw方法)将其改为Star类。你也可以修改Game类,使其调用新Star类的方法,而不是Dot类的方法。
另一位程序员不需要做任何更改,因为对Game类中play方法的调用仍然有效。这是因为那位程序员的代码没有使用任何一个类的任何内部细节。使用封装和信息隐藏,可以在类内部信息发生变化时,无需更改外部代码。
Python的约定与执行
Python并不严格强制执行“不要触碰类内部代码”的约定。其他语言有更强的机制来强制执行信息隐藏,例如使用一系列关键字来指示哪些信息应该是私有的。Python使用约定而非语言特性,并信任程序员会遵循这些约定。
版本4的重点
Poke the Dots的版本4通过向用户定义的类添加方法来专注于代码质量,因此此版本的功能不会改变。你不需要制定新的描述或功能测试计划。你的下一个活动是修改你的算法以添加方法。
总结

本节课中,我们一起学习了Poke the Dots版本3在代码结构上的问题。我们深入探讨了数据封装和信息隐藏的核心概念,理解了为何应将属性绑定在类内部,以及如何使用方法来安全地修改对象状态。我们还看到了良好的封装如何促进团队协作和代码的可维护性。版本4的目标就是应用这些原则,通过添加方法来完善Game和Dot类的封装性。
094:为第四版“戳点”游戏创建算法

概述
在本节课中,我们将学习如何为“戳点”游戏(版本四)的算法添加方法。我们将重点理解类、方法、属性以及封装的概念,并学习如何将之前用函数实现的功能重构为类的方法。
回顾:类与属性
上一节我们介绍了类的基本概念,并使用属性定义了类的状态。例如,我们曾创建了一个Person类,它拥有name、birth_date和height三个属性。
为类添加方法
本节中,我们来看看如何为类添加方法。方法是定义在类内部的函数,用于描述对象可以执行的操作。
在算法构建器的形状面板中,添加类时有两个选项:属性和方法。我们之前只使用了属性,现在我们将添加方法。
添加initialize方法
我添加的第一个方法将命名为initialize。方法与普通步骤不同,它只包含一个动作短语,而不包含动作和对象。
当你从Person类应用一个方法时,它总是应用于一个Person对象。因此,无需从词汇表中选择对象。在这种情况下,动作短语是“initialize”,我将在对象菜单中选择空白选项。
initialize方法将包含创建和绑定对象属性所需的所有步骤。回想一下,Person类有三个属性:name、birth_date和height。要创建一个Person对象,必须绑定这些属性。
设置name和height的值过于简单,不值得用单独的步骤表示,因为它们只是字面字符串和整数。然而,绑定birth_date更复杂,因为它包含年、月、日三个部分。因此,我将在initialize方法中添加一个“create birth date”的步骤。
如果创建Person对象还需要其他复杂步骤,我也会将它们添加到这里。“create birth date”只是一个将成为initialize方法中代码块的步骤,它本身不是一个方法。
添加get_age方法
一个人的年龄会随时间变化,因此我将创建另一个方法来计算年龄。我将添加一个方法,并选择“get age”作为动作短语。
在普通步骤中,格式是“动作 对象”。如果这是一个普通步骤,“get”是动作,“age”是对象。然而,对于方法,对象是该方法所应用的特殊参数。因此,Person是“get age”动作短语所应用的对象。
与处理initialize方法一样,我将展开get_age方法并向其中添加两个步骤:添加普通步骤“get current date”和“subtract birth date from current date”。
在主程序中使用方法
现在,我返回到主程序面板。我将添加一个步骤。请注意,我现在可以选择添加普通步骤或从现有类添加方法。
首先,我添加一个名为“create person”的普通步骤。在能够对其应用任何方法之前,我们需要一个步骤来创建新对象。因此,我们将始终使用名为“create [类名]”的步骤来创建对象,并且initialize方法将应用于这个新对象。
由于initialize必须始终被应用,为了避免为创建的每个类重复展开“create [类名]”并显式调用initialize方法,我们将采用一个约定:在算法中,“create [类名]”步骤将隐式地自动在新创建的对象上调用名为initialize的方法。因此,我不需要为创建的每个类都展开“create person”并添加对initialize方法的调用。
相反,“create person”将自动创建一个新的Person对象并对其调用initialize方法。
对于主面板中的第二步,我将从列表中添加一个方法。我选择Person类的get_age方法。现在,我的算法创建了一个新的人并计算了他们的年龄。
重构“戳点”游戏算法
让我们看看“戳点”游戏的算法。请注意,“create window”步骤位于主面板中。然而,当我们实现“create window”时,我们包含了游戏名称“Poke the dots”和窗口大小。因此,“create window”并非独立于我们正在创建的游戏。游戏应该封装所有关于游戏的信息,包括窗口要求。
现在你了解了更多关于封装的知识,这个问题就显得很重要了。为了改进封装,我将在Game类内部重新创建“create window”的功能。
首先,我将向Game类添加一个initialize方法。接着,我将向initialize方法添加一个“create window”步骤,并从主面板中删除“create window”步骤。
我还需要对主面板进行另一项更改,因为我现在在Game类中创建窗口,我也希望在Game类中关闭它。因此,我将从主面板中删除“close window”。在接下来的活动中,当你创建play方法时,你将在Game类的play方法末尾添加“close window”。
任务总结
目前由用户定义函数实现的大部分功能,必须用你现有Game和Dot类中的用户定义方法重新创建。
以下是你的任务:
- 完成
Game类中的initialize方法,并为Dot类创建一个initialize方法。 - 创建适当的
Game和Dot类方法。 - 更新你的算法以使用这些方法。

总结
本节课中,我们一起学习了如何为类添加方法,特别是initialize方法用于初始化对象状态。我们理解了方法调用与普通步骤的区别,并学习了如何通过将功能(如创建窗口)封装到类内部的方法中来改进代码结构。这为接下来实现更复杂的游戏逻辑奠定了基础。
095:用户自定义方法与self参数

在本节课中,我们将要学习Python中的用户自定义方法。我们将了解什么是方法,它与普通函数有何不同,以及如何使用self参数。通过一个矩形相交检测的程序示例,我们将详细拆解方法的定义、调用过程以及Python解释器在幕后是如何工作的。
概述:什么是方法?
一个在类内部定义的函数被称为方法。每个方法都必须应用于一个对象,该对象的类型就是定义该方法的类。
回想一下之前关于方法调用的课程,lower方法定义在字符串类中,因此它可以应用于一个字符串对象,返回一个将所有大写字母转换为小写字母的相似字符串对象。另一个例子是字符串方法find,它返回子字符串在字符串中首次出现的索引。
方法所应用的对象是一个特殊的参数,除此之外,它还可以有普通的参数。例如,对于方法调用R2D2.lower(),特殊参数是R2D2。对于Edmonton.find('O'),特殊参数是Edmonton,而'O'是它的普通参数。
定义用户自定义方法
用户自定义方法是通过在定义它的类的代码块(suite)中放置一个函数定义来创建的。在函数定义的参数列表开头,需要添加一个额外的参数,这个参数将绑定到那个特殊的对象(即方法被调用的对象)。
上一节我们介绍了方法的基本概念,本节中我们来看看如何在实际程序中定义和使用方法。
考虑之前课程中用于检查两个矩形是否相交或重叠的程序。每个矩形对象有四个属性:x、y、width和height,其中x和y是矩形左上角的整数坐标。
以下是该程序的新版本,其中创建矩形和检查相交的函数已被替换为方法__init__和intersects。
class Rectangle:
def __init__(self, corner_x, corner_y, width, height):
self.x = corner_x
self.y = corner_y
self.width = width
self.height = height
def intersects(self, other_rect):
return not (self.x + self.width <= other_rect.x or
other_rect.x + other_rect.width <= self.x or
self.y + self.height <= other_rect.y or
other_rect.y + other_rect.height <= self.y)
def main():
rec1 = Rectangle(5, 10, 15, 20)
rec2 = Rectangle(10, 15, 15, 20)
if rec1.intersects(rec2):
print("Overlap")
if __name__ == "__main__":
main()
我将使用这个新的矩形程序来展示用户自定义方法是如何定义的。稍后我会解释为什么使用了下划线。
程序执行与命名空间分析
当程序启动时,局部代码块是主模块,其命名空间既是局部命名空间也是全局命名空间。为了简化图示,我们省略了预绑定标识符print。
以下是程序执行时命名空间和对象创建的关键步骤:
-
定义
main函数:解释器应用函数定义语义,创建一个函数对象及其自己的命名空间,并将标识符main添加到局部命名空间并绑定到这个函数对象。 -
定义
Rectangle类:解释器应用类定义语义。- 步骤1:创建一个新的空命名空间作为局部命名空间,并使用当前的全局命名空间来评估类定义块。
- 类定义块由两个函数定义语句组成,因此使用函数定义语义来评估每个定义。注意,函数名被添加到了这个新的局部命名空间(即类的命名空间)中。
- 步骤2:创建一个新的类对象,其命名空间就是这个新的局部命名空间。当一个函数定义出现在类内部时,函数名被添加到类对象的命名空间中,这正是一个函数成为方法的原因。 任何名称绑定在类对象命名空间内的函数,都会被创建为方法对象,而不是普通的函数对象。因此,
__init__和intersects都是方法对象。 - 步骤3和4:类名
Rectangle被添加到原始的局部命名空间(主模块的命名空间),并绑定到新的类对象。
-
调用
main函数:解释器应用函数调用语义,开始执行main函数内的代码。 -
创建
Rectangle对象 (rec1):- 语句
rec1 = Rectangle(5, 10, 15, 20)是一个对Rectangle类对象的函数调用。 - 由于
Rectangle是一个类对象,函数调用语义需要扩展:如果函数对象是一个类,则创建一个该类型的新对象,然后对其应用__init__方法,并将这个新对象作为结果返回。 - 因此,创建一个新的
Rectangle对象,并将其作为特殊参数添加到__init__方法的参数列表开头。 __init__方法的参数self、corner_x、corner_y、width、height被绑定到对应的参数对象(新矩形对象和四个数字)。__init__方法内的四条语句为这个新矩形对象添加了四个属性(x,y,width,height)并将它们绑定到相应的值。__init__方法没有return语句,它隐式返回None。但根据修订后的语义,调用Rectangle类函数返回的结果对象是新创建的那个矩形对象。标识符rec1被绑定到这个矩形对象。
- 语句
-
创建第二个
Rectangle对象 (rec2):过程与创建rec1类似。 -
调用
intersects方法:- 条件语句
if rec1.intersects(rec2):中的rec1.intersects是一个属性引用。 - 解释器解引用
rec1得到第一个矩形对象,并在该对象的命名空间中查找属性intersects。方法名并不在单个对象的命名空间中。 - 当类定义被评估时,其方法名被添加到了类的命名空间中,并与该类的所有对象共享。对象使用从其类中共享的方法名。
- 因此,当在对象中找不到某个属性时,解释器会到该对象的类中去搜索。这是属性引用语义的扩展。
- 解释器在
Rectangle类的命名空间中找到了intersects,并解引用得到intersects方法对象。 - 正常的函数调用语义继续:参数列表通过评估
rec2创建,得到一个单元素参数对象列表,然后将特殊参数(rec1)添加到这个参数列表的开头。 intersects的参数self和other_rect被添加到其命名空间并绑定到两个参数对象(rec1和rec2)。intersects方法被评估并返回True,因此if语句的代码块被执行,在Shell中显示“Overlap”。
- 条件语句
关于self和__init__的约定
在上述程序中,我们使用self作为绑定到特殊对象的参数的名称。实际上,方法的第一个参数可以使用任何名称。然而,Python有一个约定,使用self作为第一个参数的名称。我们应该遵循这个约定以提高代码的可读性。
另外,我们使用了__init__(两边各有两个下划线)作为方法名。如果类定义中不包含__init__方法,Python会应用一个默认的、什么都不做的__init__方法。必须使用这种双下划线的形式,Python才能识别出你的__init__方法,并在创建类实例时自动调用它。
总结
本节课中我们一起学习了Python的用户自定义方法。我们了解到:
- 方法是定义在类内部的函数。
- 方法必须通过类的实例(对象)来调用,该实例会自动作为第一个参数(通常命名为
self)传递给方法。 - 方法的定义和调用过程涉及Python解释器对命名空间、类对象和实例对象的复杂管理。
__init__是一个特殊的方法,用于在创建对象时初始化其属性。- 遵循使用
self作为实例引用和__init__作为构造方法的命名约定是良好的编程实践。

通过理解这些概念,你就能在自己的类中定义和使用方法来组织和封装代码行为了。
096:私有属性 🔒

在本节课中,我们将要学习面向对象编程中的一个重要概念:私有属性。我们将了解什么是封装,为什么需要私有属性,以及Python中如何通过约定来实现私有属性。
概述:封装与私有属性的重要性
上一节我们介绍了用户自定义方法。本节中我们来看看如何保护类的内部实现细节,这就是封装的核心思想。
封装是一种限制对组件实现细节访问的技术,目的是使组件的实现独立于其他组件如何使用它。私有属性是实现封装的关键手段之一。
一个示例:矩形类的问题
考虑在用户自定义方法课程中介绍的矩形类。
class Rectangle:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
如果在定义该属性的类定义之外访问属性,那么当类的实现发生更改时,就可能出现问题。
例如,我更改程序以在矩形类外部访问width属性。
rect = Rectangle(0, 0, 10, 20)
print(rect.width) # 在类外部访问属性
运行程序,它工作正常。
改变实现导致的问题
然而,如果我更改矩形类的实现,使得width不再是一个属性。
class Rectangle:
def __init__(self, x1, y1, x2, y2):
self._x1 = x1 # 左上角x坐标
self._y1 = y1 # 左上角y坐标
self._x2 = x2 # 右下角x坐标
self._y2 = y2 # 右下角y坐标
由于程序在类外部使用了width属性,当width属性被不同的属性替换时,程序就会失败。
这违反了封装原则,因为矩形类的实现不再独立于其他组件如何使用它。
解决方案:使用方法提供访问
如果你想提供对矩形宽度的访问,可以在矩形类中创建一个width方法。
class Rectangle:
def __init__(self, x1, y1, x2, y2):
self._x1 = x1
self._y1 = y1
self._x2 = x2
self._y2 = y2
def width(self):
return self._x2 - self._x1
# 使用
rect = Rectangle(0, 0, 10, 20)
print(rect.width()) # 通过方法访问
现在,即使没有width属性,程序也能运行。
私有属性与封装
如果将属性的访问限制在定义它们的类内部,那么对象的使用就可以独立于对象类的实现。因此,就实现了封装。
许多编程语言都有可用于防止在定义属性的类之外访问属性的功能。
以下是其他语言的做法:
- 例如,Java和C++都使用关键字
private,以便在类定义之外访问属性时生成错误。 - 实际上,在C++中,除非使用不同的关键字(如
public)来指定属性不是私有的,否则所有属性默认都是私有的。
Java和C++中使用的确切语法并不重要;重要的是Java和C++中有语言特性允许你限制属性的作用域,以强制执行属性封装。
Python的约定:单下划线前缀
Python没有可以防止在类定义之外访问属性的关键字,而是有一个约定。
任何以单个下划线开头的属性都应被视为私有属性。它不应该在定义它的类定义之外使用。
因此,我将更改矩形类中的属性名称,以表明它们应该是私有的。
class Rectangle:
def __init__(self, x1, y1, x2, y2):
self._x1 = x1 # 私有属性
self._y1 = y1 # 私有属性
self._x2 = x2 # 私有属性
self._y2 = y2 # 私有属性
然而,这只是一个约定,带有单个前导下划线的属性仍然可以在类定义之外访问。它主要依赖于程序员的自律。
Python还有另一种称为属性(properties) 的机制,可用于使在类定义之外访问属性变得更加困难,但我们不会在本课程中讨论属性。
总结
本节课中我们一起学习了私有属性的概念及其在实现封装中的重要性。
- 封装使类的实现独立于外部使用。
- 直接暴露属性在实现更改时会导致程序错误。
- 通过方法提供对数据的访问是更好的做法。
- Python使用单下划线前缀(
_attribute) 的命名约定来标记私有属性,这主要是一种编程规范,而非强制性的语言限制。

理解并遵守私有属性的约定,是编写健壮、可维护Python代码的重要一步。
097:编程“戳点”游戏版本4 🎮

在本节课中,我们将学习如何将“戳点”游戏从版本3重构为版本4。核心任务是将现有的函数式代码转换为面向对象的类和方法结构。我们将创建Dot类和Game类,并将初始化、绘制等逻辑封装为类的方法。
概述
上一节我们完成了“戳点”游戏版本3,其中使用了多个函数来管理游戏状态。本节中,我们将重构代码,引入类和方法。这类似于之前为“黑客”游戏版本6添加函数定义的过程。我们将把算法构建器中的每个初始化面板转换为代码中的一个方法。
创建Dot类的初始化方法
首先,我将为Dot类创建__init__方法。以下是方法头:
def __init__(self, color, center, radius, velocity, window):
由于__init__是一个方法,每个方法定义都需要一个参数来绑定调用方法时的特殊实参,因此我在其参数列表中添加了self。self必须始终是方法定义中的第一个参数,无论是否存在其他参数。Dot类创建圆点需要五个额外的参数对象,因此我也为这些参数添加了对应的形参:color、center、radius、velocity和window。
接下来,我将在__init__方法中初始化这些属性。
在版本3的create_dot函数中,我们将每个属性引用绑定到一个参数对象。__init__方法将取代create_dot函数,以便在创建圆点对象时立即执行初始化所需的所有步骤。
在create_dot函数中,局部标识符dot被绑定到一个Dot对象,然后其属性被绑定。在__init__方法中,self被绑定到新的Dot对象,其属性必须被绑定。因此,我将使用参数self而不是局部标识符dot。
我将在__init__方法中为每个圆点属性添加一个赋值语句。
更新游戏创建逻辑
在create_game函数中,我将把对create_dot函数的调用替换为对Dot类构造函数的调用,并使用相同的参数。
现在,我将删除create_dot函数定义并运行程序。程序仍然可以工作。

为Dot类添加绘制方法

接下来,我将为Dot类添加另一个方法。请注意,draw_dot函数只有一个参数dot。
我将复制draw_dot函数套件中的三条语句,并删除该函数定义。
现在,我将向Dot类添加方法头def draw(self):。
draw_dot函数需要一个dot作为参数。然而,draw是一个方法,因此它的特殊实参将是我想要绘制的圆点。所以,self是我唯一需要的参数。
接下来,我将把从draw_dot复制的三条语句粘贴到draw方法中,并将所有dot引用更改为self。
现在,我将把对draw_dot和draw_game的函数调用替换为对draw方法的方法调用。
我再次运行游戏,它运行良好。
创建Game类的初始化方法
现在,我将开始编写Game类中的__init__方法。首先,创建方法头。由于在此版本中窗口将在Game类内部创建,我不需要window参数。
我将通过修改旧的create_window函数中的代码来创建一个窗口。
create_window中的另外四条语句调整了窗口字体的类型、大小和颜色。我将创建一个方法来执行此操作。
首先,我创建方法头def _adjust_window(self):。现在,我将把create_window中的四条语句粘贴到_adjust_window方法中。
由于窗口现在是游戏对象的一个属性,我必须使用self.window而不是局部标识符window。
调整窗口是创建游戏对象时应采取的步骤之一,因此我将在__init__方法中添加对_adjust_window的调用。
由于_adjust_window是在Game类定义中调用的,它的特殊实参将是self。
现在,我将删除create_window函数及其在主程序中的调用。我将把对create_game的调用替换为对不使用window参数的Game类构造函数的调用。
最后,我运行程序,但它在play_game的第一行崩溃,因为没有closed_selected属性。我将把create_game中的所有属性绑定语句复制到__init__中,将对象引用从game更改为self,然后再次运行程序。
修复属性访问错误
它仍然无法工作。Python报告名称window未定义。为了解决这个问题,我必须在__init__方法中将window更改为self.window,然后再次运行。
这次,它在我尝试关闭窗口之前都能正常工作。要解决此问题,您必须在编写play方法后,将window.close()从主程序移动到play方法中。
遵循Python私有属性约定
现在,我将向您展示如何调整代码以遵循Python的私有属性约定。
在Dot类中,我将在__init__方法中为每个属性添加单下划线。同样,我将在Game类的__init__方法中为_adjust_window方法调用添加下划线,并在_adjust_window定义的开头添加下划线,因为它只应在Game类内部调用。
我希望这个方法像属性一样私有。以单下划线开头的方法应被视为私有的,就像属性一样。
当我这次运行游戏时,解释器在尝试访问圆点的window属性时报告属性错误,因为该属性已重命名为_window。
在代码的其余部分,Dot的属性是从类外部访问的;这是一个很好的例子,说明了为什么我们希望我们的类被封装。
通过更改__init__方法中的属性名称,我破坏了代码的功能。
你的任务

现在轮到你了。修改你的程序,使所有属性都使用前导下划线,并且不再从类定义外部访问属性。
你还必须用方法替换除main之外的所有函数,并更新main中的代码。
总结
本节课中,我们一起学习了如何将“戳点”游戏从过程式代码重构为面向对象的类结构。我们创建了Dot和Game类,定义了__init__初始化方法和draw等方法,并将游戏逻辑封装在类内部。我们还介绍了Python中用于表示“私有”属性的单下划线约定。通过这次重构,代码结构更清晰,更易于维护和扩展。
098:Poke the Dots 版本4代码回顾 🎮

在本节课中,我们将回顾 Poke the Dots 游戏版本4的代码,重点探讨用户自定义方法、函数与方法的区别,以及如何通过调试器和文档来理解代码。我们还将介绍本版本新增的软件质量测试要求。
调试器中的方法与函数
上一节我们介绍了用户自定义方法的概念。本节中,我们来看看如何利用调试器探索方法与函数之间的差异。
首先,我们在 main 函数中创建新游戏对象的赋值语句旁设置一个断点。
按下调试按钮后,调试器会在断点处停止,并高亮显示对 Game 类函数的调用。此时,局部变量中没有任何对象,且 Game 类函数没有参数。
由于 Game 类函数不是方法,因此在调用 Game 前没有表达式,不会有特殊参数被添加到参数列表中。
按下“步入”按钮,我们进入了 Game 类定义的 __init__ 方法。
为什么活动函数是 __init__ 而不是 Game 类函数?因为当调用一个类函数时,会立即创建该类的一个实例,并将该实例作为特殊参数传递给 __init__ 方法。
因此,Game 的 __init__ 方法有一个参数 self。self 被绑定到 Game 类刚创建的新对象上,它出现在栈数据中,并指向新创建的 Game 对象。
请记下 self 的内存地址。接着,我们步出 __init__ 方法,返回到 main 函数。
标识符 game 已被添加到局部命名空间,并被绑定到传递给 __init__ 的那个对象。记住这个 game 对象的地址。
play 方法调用没有常规参数,但有一个特殊参数,即 game 对象。当我按下“步入”按钮时,方法的命名空间中已经绑定了一个对象。
在方法定义中,play 有一个参数 self。与 __init__ 一样,self 被绑定到调用方法时添加到参数列表中的那个特殊参数。你可以比较地址,会发现 self 与 main 函数中调用 Game 类函数时创建的 game 对象是同一个。
我将停止这次跟踪,以便探索普通函数调用、类函数调用和方法调用之间的区别。
语法与对象类型
仅通过观察语法,无法区分普通函数、类函数和方法。例如,我们仅凭经验知道 time.sleep 是一个函数,而 window.set_font_name 是一个方法,但它们的语法完全相同:都是一个标识符,后跟一个点,再跟一个标识符,然后是括号和参数。
一个函数是普通的、类的还是方法的,取决于括号前的标识符绑定的是普通函数对象、类函数对象还是方法对象。
以下是确定标识符所绑定函数对象确切类型的三种常见方法:
- 可以对该对象调用
print函数。 - 如果有权限,可以查看定义该对象的代码。
- 可以查阅文档,这是通常的做法。
为了探讨这个问题,我将在 play 方法中添加几条语句。接下来,我会添加一个 import 语句,以便使用包含点的长名称来访问 pygame.draw.circle。


我将移除 main 中的断点,然后在 play 的第一个新语句旁设置一个新断点:print(pygame.quit)。然后按下调试按钮。
这些 print 调用让我可以检查表达式求值后的对象。我将在 Shell 中步过第一条打印 pygame.quit 的语句,显示 quit 的整数值。
第二条语句打印 <built-in function circle>。
第三条语句打印 <class ‘pygame.color’>。
第四条语句打印 <bound method Window.get_surface of <UAGame.Window object at 某个内存地址>>。
这四条语句都使用了属性引用语法,但各自求值得到不同类型的对象:pygame.quit 求值为一个整数,而其他三个分别求值为一个普通函数、一个类函数和一个方法函数。
当你阅读包含函数调用的代码时,不要依赖视觉检查来确定其含义,而应查阅文档来确定该函数调用是普通函数、类函数还是方法函数。
查阅文档
让我们查看 pygame.color 的文档。由于 pygame 绑定到一个模块,pygame.color 是该模块的一个属性。我点击 pygame.color 链接查看颜色文档。
Pygame 使用 Python 的标识符命名约定。由于 Color 以大写字母开头,它是一个类而不是模块。Color(name) 代表颜色类函数。我知道它是类函数,因为它首字母大写,并且文档说明“Pygame object for color representations”,下面写着“The Color class represents...”。
让我们查看 pygame.draw.circle 的文档。由于 pygame 绑定到一个模块,pygame.draw 是该模块的一个属性。我点击 pygame.draw 链接查看绘制模块的文档。
由于 draw 以小写字母开头,它是一个模块,而不是类。pygame.draw.circle 是一个普通函数,因为它直接出现在 draw 模块中,而不是在某个类内部。
让我们查看 self.window.get_surface 的文档。self 绑定到一个类型为 Game 的对象。Game 的 window 属性绑定到一个 Window 对象。因此,get_surface 是 Window 的一个属性,它定义在 UAGame 模块中。
所以我将在 UAGame 文档的 Window 类中查找 get_surface。由于 get_surface 定义在一个类内部,它必须是一个方法。
软件质量要求
现在到了本版本 Poke the Dots 的软件质量环节。我将在“类”部分添加三条新的软件质量测试。
要通过本版本的软件质量测试,在用户自定义类中定义的属性,不得在定义它们的类之外被访问。


此外,你新定义的用户方法必须满足针对用户自定义函数的软件质量测试。
请确保你的代码满足这三条新测试,以及之前所有版本的所有测试。
恭喜,你完成了版本4!只剩下一个版本,你就完成 Poke the Dots 了。

本节课中,我们一起学习了如何通过调试器观察方法与函数的执行差异,理解了仅凭语法无法区分函数类型,并掌握了通过打印对象、查看代码或查阅文档来确定其类型的方法。最后,我们明确了版本4新增的软件质量测试要求,即封装类属性并确保方法符合函数质量标准。
099:Poke the Dots 版本5解决方案


在本节课中,我们将学习如何解决Poke the Dots游戏版本4中遗留的唯一问题:当两个圆点碰撞时,游戏没有做出任何反应。我们将探讨如何检测两个移动物体之间的碰撞,并据此改变游戏行为,从而为游戏增加挑战性。
概述与问题回顾 🎯
上一节我们实现了圆点与窗口边缘的碰撞检测。然而,版本4的游戏存在一个明显的缺陷:当两个圆点相互碰撞时,没有任何事情发生。这导致游戏缺乏挑战性,因为玩家无需任何操作也能获得高分。
本节中,我们将重点解决两个移动物体(即两个圆点)之间的碰撞检测问题。与之前处理静态窗口边缘和动态圆点的碰撞不同,这次我们需要处理两个都在运动的物体。
核心任务:实现圆点间碰撞检测 💥
为了实现圆点间的碰撞检测,你需要完成一个新的设计描述、测试计划和算法,然后才能编写最终代码。
以下是完成此任务需要遵循的步骤:
- 设计描述:明确描述当两个圆点碰撞时,游戏应做出何种反应(例如,游戏结束、得分重置、圆点改变颜色或速度等)。
- 测试计划:设计具体的测试用例,验证碰撞检测逻辑是否正确工作。这包括圆点刚好接触、部分重叠以及完全分离等多种情况。
- 算法设计:规划出检测两个圆形物体碰撞的计算逻辑。核心是判断两个圆的圆心距离是否小于它们的半径之和。其公式可以表示为:
distance(center1, center2) < (radius1 + radius2)
在代码中,这通常通过计算两点间的欧几里得距离来实现。
从理论到实践 🔄
在明确了设计、测试和算法后,你将进入代码实现阶段。你需要将上述碰撞检测公式整合到游戏的主循环中,并在检测到碰撞时触发你在设计描述中定义的新游戏行为。
通过解决这个最终问题,Poke the Dots游戏将从一个简单的演示程序转变为一个具有基本挑战性的完整小游戏。
总结 📝

本节课中,我们一起学习了如何为Poke the Dots游戏增加核心的挑战机制——圆点间碰撞检测。我们回顾了从静态碰撞检测到动态碰撞检测的升级,并明确了实现这一功能所需的三个步骤:设计描述、测试计划和算法设计。核心在于运用公式 distance < (r1 + r2) 来判断两个移动的圆是否发生碰撞。完成这些工作后,你的游戏将能够对玩家的操作(或未操作)做出有意义的反馈,从而变得更具可玩性。
100:观察“戳点”游戏最终版 🎮
在本节课中,我们将观察“戳点”游戏的最终版本,并了解其与前一个版本相比新增的核心功能——圆点碰撞检测。

概述
“戳点”游戏版本5在视觉和基础玩法上与版本4保持一致。然而,它引入了一个决定性的新机制:当两个移动的圆点发生碰撞时,游戏会立即结束。本节课我们将详细观察这一变化,并理解其对游戏玩法的影响。
游戏启动与基础观察
启动游戏,初始界面与版本4没有明显区别。
- 屏幕上有一个红色圆点和一个蓝色圆点。
- 两个圆点以相同的速度移动。
- 当圆点碰到窗口边缘时,它们会反弹。
- 白色的计分板依然在记录游戏开始后的秒数。
交互与新增机制
接下来,我们通过点击鼠标进行交互,并观察新增的碰撞机制。
点击鼠标数次,圆点仍然会瞬间传送到窗口内的随机位置。
然而,当两个移动的圆点发生碰撞时,新的情况出现了:圆点立即停止运动,同时在屏幕一角显示“游戏结束”的消息。
请注意消息文本的颜色与小圆点的颜色相同,而消息背景的颜色与大圆点的颜色相同。
游戏玩法与挑战
这一碰撞机制最终为“戳点”游戏引入了真正的挑战。玩家的目标是尽可能避免两个圆点相撞,从而获得更高的分数。
点击窗口的关闭图标,可以正常关闭游戏窗口。
当你尝试游玩时,你能获得的最高分数是多少?我们的测试记录是71、44、21和10秒。
总结与后续任务
本节课我们一起观察了“戳点”游戏的最终版本。核心新增功能是圆点碰撞检测,它使得游戏在碰撞发生时立即结束,从而创造了游戏目标和挑战性。

为了巩固理解,你的下一个任务是:修改你的游戏描述和功能测试计划,将圆点碰撞这一新机制纳入考虑。
101:12_03_01 为第五版“戳点”游戏创建算法
在本节课中,我们将学习如何为“戳点”游戏的第五个版本创建算法。核心任务是添加最后一个游戏功能:处理圆点碰撞。我们将学习如何检测碰撞,并引入一个新的游戏状态属性来管理游戏结束后的行为。
上一节我们完成了游戏的基本循环和事件处理。本节中,我们来看看如何实现圆点碰撞检测,并据此控制游戏流程。

检测圆点碰撞
圆点碰撞不是由玩家事件触发的,而是游戏对象移动的结果。因此,必须在每一帧都检查两个圆点是否发生了碰撞。
以下是检测两个圆点是否碰撞的核心逻辑。我们假设每个圆点都有位置 (x, y) 和半径 r。当两个圆心的距离小于或等于它们半径之和时,即发生碰撞。
碰撞检测公式:
distance = sqrt((x1 - x2)^2 + (y1 - y2)^2)
如果 distance <= (r1 + r2),则发生碰撞。
在代码中,我们可以这样实现:
def dots_collide(dot1, dot2):
distance = ((dot1.x - dot2.x)**2 + (dot1.y - dot2.y)**2)**0.5
return distance <= (dot1.radius + dot2.radius)
管理游戏状态
当圆点发生碰撞时,游戏应该结束。我们需要在游戏类中添加一个新的属性来跟踪游戏是否应该继续。
添加游戏状态属性:
class Game:
def __init__(self):
# ... 其他初始化代码 ...
self.continue_play = True # 新属性,True表示游戏继续,False表示游戏结束
这个 continue_play 属性将决定游戏在绘制和更新步骤中的行为。
更新游戏逻辑
有了游戏状态属性后,我们需要在游戏的 update 和 draw 方法中根据其值选择不同的行为。
以下是更新游戏逻辑的步骤:
-
在
update方法中:每一帧更新圆点位置后,立即检查它们是否碰撞。如果碰撞发生,则将continue_play属性设置为False。def update(self): if self.continue_play: # 更新圆点A和圆点B的位置 self.dot_a.move() self.dot_b.move() # 检查碰撞 if dots_collide(self.dot_a, self.dot_b): self.continue_play = False # 如果 continue_play 为 False,则不再更新圆点位置 -
在
draw方法中:根据游戏状态绘制不同的内容。游戏进行时正常绘制所有元素;游戏结束时,可以绘制“游戏结束”的提示信息。def draw(self): # 绘制背景等不变元素 if self.continue_play: # 游戏进行中,绘制移动的圆点 self.dot_a.draw() self.dot_b.draw() else: # 游戏结束,绘制结束画面或文本 draw_game_over_text()
通过这种方式,游戏逻辑变得清晰:只要圆点未碰撞,游戏就继续运行和绘制;一旦碰撞发生,游戏状态改变,画面和行为也随之改变。

本节课中我们一起学习了如何为“戳点”游戏第五版完成算法。我们引入了圆点碰撞检测机制,并通过添加一个 continue_play 状态属性来优雅地管理游戏的结束。这使得游戏在圆点碰撞后能停止更新逻辑并显示相应的结束状态,从而实现了完整的游戏流程。
102:12_04_01 编程“戳点”游戏 - 第五版
在本节课中,我们将学习如何完成“戳点”游戏的最终版本。我们将重点实现两个核心功能:判断游戏是否结束(即两个点是否相交)以及在游戏结束时绘制包含点颜色的结束信息。

游戏结束判定:点相交检测
上一节我们介绍了游戏的基本框架,本节中我们来看看如何判断游戏是否结束。游戏结束的条件是两个移动的点发生碰撞或相交。
在之前用户自定义的“小于”方法中,我们看到了一个用于判断两个矩形对象是否相交的例子。然而,对于圆形的点,我们需要不同的计算方法。
一个实心圆的所有点到其圆心的距离都小于或等于一个固定值,这个固定值被称为半径。如果两个圆相交,那么它们圆心之间的距离必须小于它们半径之和。
我们可以使用距离公式来计算两点之间的距离。公式如下:
距离 = √[(x₂ - x₁)² + (y₂ - y₁)²]
在代码中,我们可以这样实现:
import math
def distance(point1, point2):
return math.sqrt((point2.x - point1.x)**2 + (point2.y - point1.y)**2)
以下是判断两个圆(点)是否相交的步骤:
- 计算两个点圆心之间的距离。
- 计算两个点半径之和。
- 如果距离小于半径之和,则两点相交,游戏结束。
绘制游戏结束信息
当我们确定游戏结束后,需要绘制游戏结束的提示信息。这条信息需要使用两个点的颜色。
但你已经知道,不能直接访问点对象的颜色属性。为了获取颜色信息,你必须在Dot类中编写一个方法,用于返回其颜色属性。
例如,在Dot类中添加一个get_color方法:
class Dot:
def __init__(self, color, radius, center, velocity):
self.color = color
self.radius = radius
self.center = center
self.velocity = velocity
def get_color(self):
return self.color
这样,在游戏主循环中,当检测到游戏结束时,就可以调用dot_a.get_color()和dot_b.get_color()来获取颜色,并用这些颜色来渲染游戏结束的文字。
开始编程
现在,请根据以上思路,编程实现“戳点”游戏的第五个也是最终版本。确保整合游戏结束检测逻辑和游戏结束画面的绘制。

本节课中我们一起学习了如何为“戳点”游戏收尾。我们掌握了使用距离公式计算两点距离,并通过比较圆心距与半径和来判断圆形对象是否相交。同时,我们通过为Dot类添加获取方法来安全地访问其颜色属性,从而能够绘制个性化的游戏结束信息。这些是完成一个基础碰撞检测游戏的关键步骤。
103:Poke the Dots 版本5的潜在改进方案

在本节课中,我们将对《Poke the Dots》游戏的最终版本(版本5)进行反思,并探讨一系列可以进一步提升游戏体验的潜在改进方案。虽然课程要求已经完成,但软件开发的魅力在于持续的迭代与优化。
上一节我们完成了《Poke the Dots》游戏的核心功能开发。本节中,我们将不再进行代码追踪或质量测试,而是专注于游戏创作流程图的最后一个环节:探索解决方案的潜在问题与改进空间。
🎨 提升视觉吸引力
当前版本的《Poke the Dots》在视觉上较为单调。一个简单的改进方法是增强游戏的动态视觉效果。
以下是提升视觉吸引力的一个具体方案:
- 随机化圆点颜色:修改鼠标按键释放(
mouse up)事件处理器。每次玩家点击鼠标时,随机改变两个圆点的颜色。这可以通过在事件处理器中添加类似dot.color = random_color()的代码来实现。
🎯 增加游戏挑战性
《Poke the Dots》的游戏玩法可能略显简单,导致趣味性不足。我们可以通过增加难度来提升游戏的挑战性和可玩性。
以下是三种增加游戏难度的方法:
- 点击加速:每次按下鼠标按键时,增加圆点的移动速度。这迫使玩家必须谨慎选择点击时机,因为过于频繁的点击会使圆点速度变得极快。实现方式可能是在事件处理器中修改速度变量,例如
dot.velocity_x *= 1.1。 - 精确点击传送:当前游戏允许点击窗口内任意位置来传送圆点,这与游戏名称“戳圆点”并不完全相符。一个更准确且更具挑战性的改进是:修改鼠标事件处理器,仅当鼠标光标位于圆点内部时点击,才触发传送功能。这需要检测点击坐标是否在圆点范围内,可通过计算距离来判断:
if distance(mouse_pos, dot.center) < dot.radius:。 - 错误点击惩罚:结合上一条“精确点击传送”的改进,如果玩家点击时未命中圆点,可以施加惩罚,而不是什么都不发生。例如,每次错误点击都会在屏幕上添加一个具有随机颜色和大小的新圆点。如果尝试此改进,可能需要将游戏窗口设置得更大一些。

🔄 优化游戏重玩体验
目前,玩家必须关闭游戏窗口并重新启动程序才能开始新一局游戏。我们可以让重玩变得更加便捷。

以下是一种优化重玩体验的方法:
- 添加重玩按键:增加一个事件处理器,用于检测游戏是否结束以及是否按下了特定的“重玩”按键(如‘R’键)。如果条件满足,可以通过将控制游戏循环的变量(如
continue_game)重新设置为True来重启游戏。
😊 然而,你必须找到一种方法,在每次重启时将玩家得分重置为零。
以上只是众多可能改进方案中的一部分。我们鼓励你尝试自己实现其中一些改进,或者创造出属于你自己的独特优化方案。

🎉 课程总结

恭喜你完成了《Poke the Dots》游戏的开发!
本节课中我们一起学习了如何对已完成的软件项目进行反思并提出改进思路。我们探讨了从视觉表现、游戏难度到用户体验等多个维度的优化方案。
本课程至此已全部结束。我们希望你喜欢这门课程,并学到了如何运用计算机科学将计算应用于多种不同的信息形式。
计算机科学不仅仅是Python编程和电脑游戏。它依赖于精确的概念和表示法,例如本课程中使用的词法语法、语义规则和各种图表。计算机科学也运用许多流程,例如支持迭代式软件设计与开发的游戏创作流程。
编程远不止是写代码。你可以运用在本课程中获得的问题解决技能,在未来解决许多计算问题,无论是在电子游戏还是其他问题领域。我们相信,你在此课程中使用的游戏创作流程将直接适用于任何计算领域。
我们希望你享受编写Python程序的乐趣,同时我们也确信,通过仅仅应用新语言的规则,你将能够快速掌握其他编程语言。
我们祝愿你在未来的学习中一切顺利,并希望这些学习能包含更多的计算机科学知识。

浙公网安备 33010602011771号