创造你自己的-Python-文本冒险-一-

69 阅读1小时+

创造你自己的 Python 文本冒险(一)

原文:Make Your Own Python Text Adventure

协议:CC BY-NC-SA 4.0

一、入门指南

介绍

你可能听过互联网上的口号,“学会编码!学会编码!”,并且您已经了解到 Python 是一个很好的起点…但是现在该怎么办呢?很多想编程的人不知道从何下手。你可以用代码创造“任何东西”的想法是令人麻痹的。这本书提供了一个明确的目标:通过创建文本冒险来学习 Python。

这本书将教你编程的基础,包括如何组织代码和一些编码的最佳实践。在书的结尾,你将会有一个工作的游戏,你可以玩或者向朋友炫耀。你还可以通过编写不同的故事线来改变游戏,使之成为你自己的游戏,包括添加新物品,创造新角色等。

学习编程是一项令人兴奋的努力,但一开始会感到畏惧。然而,如果你坚持下去,你可以成为一名职业程序员或周末爱好者,或者两者兼而有之!我的故事与许多程序员的故事相似:我编程的第一件事是 QBASIC 中的一个数字猜谜游戏,现在编程是我的工作。我希望你也能加入我们,我感谢你选择这本书作为起点。

这本书是给谁的

这本书是为以前从未编程的人或初学 Python 的程序员而写的。如果你属于第二组,你可能会浏览一些早期的材料。

虽然这是面向初学者的,但我确实假设您了解计算机基础知识,如打开命令提示符、安装软件等。如果你遇到什么困难,在网上搜索“如何在[操作系统]上做[事情]”通常会帮你解决。对程序员特别有用的网站是 stack overflow(http://stackoverflow.com)1和超级用户( http://superuser.com ), 2 所以如果你在搜索结果中看到它们,先给它们一个机会。

如何使用这本书

在这本书的每一章中,你将在创造你的文本冒险的总体目标上取得进展。前几章可能看起来很慢,因为它们侧重于学习 Python 的基础知识。事情会在本书的后半部分有所好转,那时焦点会转向游戏世界的构建。

我建议在你的电脑上或旁边阅读这本书,这样你就可以轻松地在阅读和编写代码之间来回切换。这本书前半部分的每一章都将以家庭作业部分结束。这些问题主游戏不会要求,但你至少应该试一试。在适用的情况下,本书末尾提供了解决方案。

本书中的大部分 Python 代码如下所示:

1   greeting = "Hello, World!"
2   print(greeting)

打算输入到交互式 Python 会话中的代码(参见第三章)将如下所示:

>>> greeting = "Hello, World!"
>>> print(greeting)

内联出现的代码或命令的引用将appear like this。你应该学习的专业术语是这样出现的。

如果你遇到困难,你可以在这里下载本书每一章的代码。 3 忍住复制粘贴一切的冲动!如果你把密码打出来,你会记住更多的信息。然而,我们都会犯错误,所以如果您不能找出问题所在,可以将您的代码与我的代码进行比较。如果你真的确定一切都是一样的,用 DiffChecker 4Mergely 这样的在线比较工具再检查一遍。 5 你还可以查看附录 B 中一些你可能会遇到的常见错误。

最后,这个游戏是你的游戏。它是完全可定制的,如果你觉得增加更多的房间和敌人,改变故事,增加难度等都很舒服。,请这样做。我会指出这样的定制机会:

  Customization Point

关于定制的一些注意事项。A458925_1_En_1_Figa_HTML.jpg

请记住,每一章都是建立在最后一章的基础上的,所以如果您偏离材料太远,您可能希望将您的定制代码保存在另一个目录中,以便您可以继续从源材料中学习。

设置您的工作空间

不要跳过这一节!在开始编写本书中的代码之前,您需要确保一切都设置妥当。如果配置不当,很多问题会等着你。

Python 版本

Python 的创造者决定 Python 3 不会向后兼容 Python 2。虽然 Python 3 在 2008 年发布,但一些人仍然坚持使用 Python 2。初学者没有理由从 Python 2 开始,所以这本书是用 Python 3 写的。不幸的是,一些操作系统与 Python 2 捆绑在一起,这使得安装和使用 Python 3 有点棘手。如果你遇到麻烦,网上有大量针对你的具体操作系统的详细说明。

安装 Python

根据您的操作系统和您使用的包管理器(如果有的话),有许多安装 Python 的方法。

Windows 操作系统

在 Windows 上安装 Python 的一个优点是,您不需要担心已经存在的旧版本。Windows 没有标准的包管理器,所以你需要从 Python 下载安装程序。

  1. 在浏览器中打开 http://python.org/download/ ,下载最新的 Windows 3 . x . y 安装程序。
  2. 运行安装程序。
  3. 在安装程序的第一个屏幕上,您会看到一个包含 Python 3 的选项。路径上的 x。一定要选中那个框。
  4. 继续安装;默认设置就可以了。如果您在安装程序中看到另一个将 Python 添加到环境变量的选项,请确保也选中了该框。
Mac OS X

根据我的经验,在 Mac OS X 上安装开发者工具最简单的方法就是使用家酿 6 包管理器( http://brew.sh )。但是,我很欣赏你可能不想装东西去装别的东西!我将首先提供自制步骤,然后是更传统的途径。

使用自制软件:

  1. 打开一个终端。
  2. 在终端运行 http://brew.sh 命令安装家酿。
  3. 用下面的命令安装 Python 3:brew install python3

现在,您可以在任何想要使用 Python 的时候使用命令python3。命令python指向 Python 的默认 Mac OS X 安装,即版本 2.7.5。

使用安装程序:

  1. 在浏览器中打开 http://python.org/download/ ,下载 Mac OS X 最新的 3.x.y 安装程序
  2. 打开下载包,然后运行Python.mpkg
  3. 按照安装向导进行操作。默认设置就可以了。

现在,您可以在任何想要使用 Python 的时候使用命令python3。命令python指向 Python 的默认 Mac OS X 安装,即版本 2.7.5。

Linux 操作系统

如果您使用的是 Linux,那么您可能已经习惯使用发行版的包管理器了,所以我就不赘述了。通常,像sudo apt-get install python3sudo yum install python3这样的东西会得到你想要的。也有可能您的发行版已经包含了 Python 3。如果其他方法都失败了,你可以从官方网站( https://www.python.org/downloads/source/ )下载源代码并编译 Python。 7

验证您的安装

要验证您的安装,请打开命令提示符/终端(我将交替使用控制台、命令提示符和终端),并尝试这两个命令:

python version

python3 version

有四种可能性:

  • 两者都显示版本号:太好了,您的计算机上同时安装了 Python 2 和 3。只要确保你总是用python3运行本书中的代码。
  • 只有 Python 显示版本号:如果版本中的第一个数字是 3,就像在“Python 3.5.1”中一样,那就没问题。如果它是 Python 2 版本,如“Python 2.7.10”,那么 Python 3 没有正确安装。尝试重复安装,如果还是不行,你可能需要调整你的PATH来指向 Python 3 而不是 Python 2。
  • 只有 Python3 显示一个版本号:太好了,你安装了 Python 3。只要确保你总是用python3运行本书中的代码。
  • 都不显示版本号:Python 没有正确安装。尝试重复安装。如果这仍然不起作用,您可能需要调整您的PATH来包含 Python 安装的位置。

Footnotes 1

http://stackoverflow.com

  2

http://superuser.com

  3

https://www.dropbox.com/sh/udvdkxtjhtlqdh1/AAD9HOD6VTb5RGFZ7kBv-ghua?dl=0

  4

https://www.diffchecker.com

  5

http://www.mergely.com/editor

  6

http://brew.sh

  7

https://www.python.org/downloads/source/

二、你的第一个程序

当你在电脑上打开一个应用程序时,比如一个互联网浏览器,在最低层,CPU 正在执行指令来移动字节的信息。早期的程序都是辛辛苦苦写在打孔卡上的,如图 2-1 所示。

A458925_1_En_2_Fig1_HTML.jpg

图 2-1。

An early punch card Credit: Wikipedia user Harke

谢天谢地,我们已经对计算机编程进行了几十年的改进,使得编写这些指令变得容易多了!现在,编程语言处于“低级”到“高级”的范围内,像 C 和 C++这样的语言是低级的,像 Python 和 Ruby 这样的语言是高级的。按照设计,高级语言允许程序员忽略计算机程序的许多幕后细节。这也是 Python 经常被推荐为首选编程语言的原因之一。

首先,在你的电脑上创建一个文件夹,在那里你可以完成游戏的所有工作。从现在开始,这个目录将被称为项目的根目录。

创建模块

Python 代码被组织成称为模块的文件。每个模块通常包含大量逻辑上相关的代码。例如,我们的项目将包含一个运行游戏的模块,另一个包含管理敌人的代码的模块,另一个用于世界,等等。要创建您的第一个模块,导航到您的根目录并创建一个名为game.py的空文件。

编写代码

编写代码时,严格按照本书中出现的内容编写代码是非常重要的。但是,我不建议简单的复制粘贴。尤其是刚开始的时候,肌肉记忆会帮助你学得更快。如果你遇到错误,逐行检查你的代码,检查错别字、大小写错误、符号错位等。如果你真的不能解决问题,那么也只有这样,复制代码才是正确的。但是一定要仔细阅读粘贴的代码,找出错误。

这里我需要提到 Python 语法中一个更有争议的部分:有意义的空白。许多语言会忽略空格和制表符,但 Python 不会。这意味着您可能会遇到由您(很容易)看不到的字符引起的问题!因此,您需要决定是否使用制表符或空格来缩进代码。大多数 Python 程序员都选择使用空格,所以我将坚持使用四个空格来缩进本书的代码。如果您选择使用制表符并复制代码,您必须将缩进切换到制表符!一些文本编辑器可以通过工具栏命令来完成这项工作。如果您的没有,您应该能够用“\t”(表示“tab”)替换四个空格。

记住这一点,让我们编写第一行代码。打开game.py并添加以下行:

print("Escape from Cave Terror!")

A458925_1_En_2_Figa_HTML.jpgCustomization Point

您可以通过替换引号内的文本来更改游戏的名称。想想你的游戏发生的场景。是中世纪的森林,外星飞船,还是犯罪猖獗的城市?

运行 Python 程序

现在,我们将执行刚刚编写的代码。首先打开命令提示符或终端,然后使用cd命令导航到您的项目根目录。比如cd ~/Documents/code/learn-python-game或者cd C:\Code\my_python_adventure。最后,运行以下命令:

python game.py

(注意:根据您安装 Python 的方式,您可能需要运行python3 game.py。)

如果一切顺利,您应该看到"Escape from Cave Terror!"被打印到控制台。恭喜你!你刚刚写了你的第一个 Python 程序。

家庭作业

尝试以下作业练习:

  1. 制作一个名为calculator.py的新模块,并编写将"Which numbers do you want to add?"输出到控制台的代码。
  2. 运行计算器程序,确保它工作正常。
  3. 尝试从代码中删除引号。会发生什么?

三、倾听你的用户

所有的计算机程序都有一定程度的用户输入。有些可能只是要求用户启动应用程序,而另一些只是耐心等待,直到用户告诉它做一些事情。由于这个应用程序是一个文本冒险,它更接近于“耐心等待”的范围。在本章中,您将学习如何阅读和处理用户在命令提示符下键入的指令。

你的朋友:标准输出和标准输入

根据定义,文本冒险需要用户输入程序的文本指令。作为响应,程序将向用户显示文本。这是命令行应用程序中的常见模式。

为了对此有所了解,让我们演示一个已经安装的命令行应用程序——Python。没错,python命令不仅仅可以运行程序。打开命令提示符并运行python。您应该会看到类似这样的内容:

$ python
Python 3.4.1 (default, May 8 2015, 22:07:39)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.49)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

现在,在光标处,键入以下内容并按 Enter 键:

help(print)

您应该看到这个:

>>> help(print)

Help on built-in function print in module builtins:

[...]

要退出该视图,只需按下q

我们只是通过输入两个命令与这个命令行应用程序进行交互:help(print)q。这两个命令都由 Python 读入、解释和响应。

当应用程序向控制台写出文本时,这被称为写入标准输出或简称为 stdout。类似地,当用户(甚至另一个应用程序)将文本写入控制台时,这被称为写入标准输入或标准输入。

事实上,如果您回到控制台并再次键入help(print),您将会看到文档引用了sys.stdout。默认情况下,print函数将文本写入标准输出。您已经在运行游戏时看到了这一点——应用程序向控制台显示了介绍文本。

现在尝试输入help(input)。您可以看到input函数将“从标准输入中读取一个字符串”这正是我们所寻求的,让我们的应用程序能够倾听用户。

要退出,按下q,然后键入quit()。这应该会把你带回一个常规的命令提示符。

从标准输入读取

打开一个新文件,另存为echo.py。输入以下行:

input("Type some text: ")

保存文件并使用python echo.py运行。请记住,要运行应用程序,您必须位于包含该文件的同一目录下。

希望您看到提示"Type some text"。继续按它说的做,然后按回车键。看起来好像程序忽略了你,把你扔回到命令提示符下。刚刚发生了什么?input命令将提示文本("Type some text")打印到标准输出,并读入您对标准输入的响应。因为没有更多的指令,应用程序简单地退出。

正如这个文件的名字所暗示的那样,我们将创建一个向用户反馈信息的应用程序,但是首先我们需要学习如何存储临时信息。

保存信息

在上一个练习中,我们能够从用户那里读入信息,但是我们不能对它做任何事情。我们需要暂时保存这些输入,以便可以打印出来。可以使用变量将临时信息存储在计算机的内存中,也可以从内存中访问临时信息。

Python 中变量名的一些例子有nmy_numberaddress。要在变量中存储信息,我们只需使用=操作符。这被称为赋值操作符。例如:

1   n = 5
2   my_number = 3
3   address = '123 Maple St.'

然后,每当我们需要调用这些信息时,我们可以参考变量名称,如print(n)print(address)。让我们练习一下。

返回您的终端并运行python。当我们之前这样做时,我们使用了help命令来获取关于函数的信息。也许更有用的特性是能够输入 Python 代码并立即执行。这被称为“Python 解释器”、“Python shell”或“Python REPL”(Read Evaluate Print Loop 的缩写)。

继续输入以下命令:

>>> address = '123 Maple St.'
>>> print(address)

你应该看到"123 Maple St."被打印出来。这是因为我们使用赋值操作符将值“123 Maple St .”赋给了变量address。然后当print函数运行时,它在内存中查找address的真实值,这样它就知道要打印出什么。

有了这些信息,我们现在可以用我们的 echo 程序做一些更有趣的事情。返回echo.py,按如下方式更改代码:

1   user_input = input("Type some text: ")
2   print(user_input)

再次运行这个程序,验证它是否能回显您输入的任何文本。让我们做一些类似于我们游戏的事情。打开game.py并添加以下几行:

2   action_input = input('Action: ')
3   print(action_input)

数据类型

在我们结束这一章之前,我们需要简要回顾一些数据类型。到目前为止,我们主要看到了像"Type some text""123 Maple St."这样的文本数据。这些被称为字符串 1 ,Python 知道它们是字符串,因为它们在代码中被单引号或双引号包围。input返回的数据也是一个字符串。以下是一些字符串示例:

1   name = 'Phillip'
2   forecast = "It's going to rain!"
3   url = 'http://letstalkdata.com'

下一个最常见的数据类型是整数。如果你还记得数学课的话,整数就是像 1,15,-99 和 0 这样的数字。在 Python 中,整数作为数字输入,没有任何额外的符号。

1   a = 3
2   b = 4
3   hypotenuse = 5

有小数点的数称为浮点数,或简称为 float。浮点数的输入类似于整数,除了它们包含小数点。

1   a = 3.0
2   b = 4.0
3   hypotenuse = 5.0

您可以像预期的那样对数字执行基本的数学运算。在解释器中尝试其中的一些:

>>> 5 + 6
>>> 0 - 99
>>> 5.0 / 2.0
>>> 5 / 2
>>> 4 * (7 - 2)

Python 内置了很多很多的数据类型,但是现在需要注意的重要一点是,Python 是根据您的输入方式来推断类型的。my_variable = 5my_variable = '5'差别很大!

家庭作业

尝试以下家庭作业练习:

  1. my_variable = 5my_variable = '5'有什么区别?

  2. print(n)print('n')有什么区别?如果您不确定,请尝试在 Python 解释器中输入以下命令:

    n = 5
    print(n)
    print('n')
    
    
  3. 尝试不使用变量重写echo.py

Footnotes 1

在内部,Python 以各种不同的格式存储文本数据。你会遇到的最常见的是strbytes。因此,单词“字符串”并不总是与 Python str类型完全相关。

四、决策

你明天要做一个重大决定——坐公共汽车还是走路。好吧,好吧,也许这不是一个重大的决定,但还是一个决定。你的决定可能基于许多因素,但让我们保持简单。如果下雨,那你就坐公交;否则,你会走路。

请注意决策的结构:

  1. 第一,有些事不是真的就是假的。在这种情况下,是真是假的事情就是有没有下雨。这叫做条件。
  2. 接下来,如果条件为真,则采取行动。如果下雨,你就乘公共汽车。
  3. 最后,如果条件为假,则采取行动。如果不下雨,那你就步行。

计算机需要以同样的方式做决定的能力。在计算机代码中,我们可以给计算机一个条件来评估,如果这个条件为真或为假,我们就采取行动。这个概念被称为分支,因为当我们需要时,代码可以“分支”到两个或更多的方向。

布尔运算

形式上,一个或真或假的陈述被称为布尔表达式。以下是布尔表达式的一些示例:

  • 我的年龄是 30 岁
  • 我有两个兄弟姐妹
  • 1 > 100
  • 1 < 100

如果你通读这些陈述,你应该能够说出每一个对你来说是对还是错。对于前两个条件,我们的答案各不相同,但希望我们都同意后两个条件!

在 Python 中,我们可以按如下方式编写这些表达式:

1   age == 30
2   siblings == 2
3   1 > 100
4   1 < 100

注意,我们可以像在数学中一样使用<>操作符。但是双等号是怎么回事?那是打印错误吗?不,这个符号是相等运算符。记住一个等号(=)已经有了一个目的——给变量赋值。在示例中,我们不是赋值,而是检查值,因此我们必须使用不同的运算符。

如上所述,这些表达式中的每一个都可以计算为真或假。“真”和“假”是如此重要的概念,以至于它们实际上是 Python 中的关键字。不出所料,这种新的数据类型被称为布尔数据类型。“布尔表达式”和“布尔数据类型”通常都简称为“布尔”,上下文暗示了所指的内容。

当 Python 代码被求值时,布尔表达式被转换为它们的布尔类型。这意味着以下表达式都是等价的:

1   1 == 1
2   'abc' == 'abc'
3   True

类似地,这些表达式也是等价的:

1   1 == 0
2   'abc' == 'xyz'
3   False

为了向自己证明这一点,打开 Python shell 并输入一些内容。Python 会对每个表达式进行求值,并用TrueFalse进行响应。

还有一个比较运算符需要学习,那就是“不等于”运算符。在 Python 中,这被写成!=。在 Python 解释器中尝试这些表达式

>>> 1 != 0
>>> True != True
>>> 'abc' != 'xyz'

总结一下,这里是我们目前所知的算子,加上>=<=:

| 操作员 | 类型 | 目的 | | `=` | 分配 | 给变量赋值 | | `==` | 比较 | 检查两个值是否相等 | | `!=` | 比较 | 检查两个值是否不相等 | | `>` | 比较 | 检查左边的值是否大于右边的值 | | `>=` | 比较 | 检查左边的值是否大于或等于右边的值 | | `<` | 比较 | 检查左边的值是否小于右边的值 | | `<=` | 比较 | 检查左边的值是否小于或等于右边的值 |

If 语句

现在我们知道了布尔表达式和数据类型,我们可以开始用 if 语句向代码添加条件。一个if-语句必须有一个条件,一个条件为真时要采取的动作,以及一个条件不为真时要采取的可选动作。例如:

1   n = 50
2   if n < 100:
3       print("The condition is true!")
4   else: # <-- This part is optional

5       print ("The condition is false!")

有时,将自己和他人的注释直接放入代码中会很有帮助。这些被称为代码注释,Python 会在程序运行时忽略它们。在 Python 中,代码注释以#开头。

我们还可以使用关键字elif堆叠if语句:

1   n = 150
2   if n < 100:
3       print("n is less than 100.")
4   elif n < 1000:
5       print("n is less than 1000.")
6   else:
7       print("n is a big number!")

在 Python 中,elif是“else if”的写法。它被简称为elif,因为它被广泛使用。

现在继续打开game.py,按如下方式更改代码:

 1   print("Escape from Cave Terror!")
 2   action_input = input('Action: ')
 3   if action_input == 'n':
 4       print("Go North!")
 5   elif action_input == 's':
 6       print("Go South!")
 7   elif action_input == 'e':
 8       print("Go East!")
 9   elif action_input == 'w':
10       print("Go West!")
11   else:
12       print("Invalid action!")

该代码将读入用户输入,并将输入的值与预定义的字符(“n”、“s”、“e”或“w”)进行比较。如果其中一个条件为真,程序将分支到代码的那个部分,并将操作打印到控制台。否则,它会通知用户该操作无效。

布尔运算

有时,将多个条件组合成一个条件会很有帮助,我们使用关键字andor来实现这一点。这些工作就像你期望的那样。

1   if a == 3 and b == 4:
2       print("The hypotenuse is 5.")
3   if a == 3 or b == 4:
4       print("The hypotenuse might be 5.")

您可以根据需要使用任意数量的运算符,但是当您开始组合运算符时,有时需要使用括号来指定条件的求值顺序。尝试在解释器中键入以下内容:

>>> 1 == 100 and 1 == 2 or 1 == 1
>>> (1 == 100 and 1 == 2) or 1 == 1
>>> 1 == 100 and (1 == 2 or 1 == 1)

第一个例子在语法上是正确的,但是读起来令人困惑。为了澄清这一点,在第二个例子中,我们用圆括号将前两个条件括起来。在第三个例子中,我们实际上改变了表达式的求值顺序,从而改变了响应。

这两种分组的区别如下:

1   (1 == 100 and 1 == 2) or 1 == 1
2   (False) or 1 == 1
3   False or True
4   True

和...相对

1   1 == 100 and (1 == 2 or 1 == 1)
2   1 == 100 and (True)
3   False and True
4   False

考虑到这一点,我们可以这样做:

1   if (a == 3 and b == 4) or (a == 4 and b == 3):
2      print("The hypotenuse is 5.")

请注意,我们不能这样做:

1   # Warning: Bad Code!

2   favorite_color = 'blue'
3   if (favorite_color = 'red' or 'orange'):
4       print("You like warm colors.")

虽然在头脑中阅读代码可能有意义,但这是无效的语法。orand两边的语句必须是完整的布尔表达式。

为了使我们的游戏更加用户友好,让我们让每个条件忽略行动的情况:

 1   print("Escape from Cave Terror!")
 2   action_input = input('Action: ')
 3   if action_input == 'n' or action_input == 'N':
 4       print("Go North!")
 5   elif action_input == 's' or action_input == 'S':
 6       print("Go South!")
 7   elif action_input == 'e' or action_input == 'E':
 8       print("Go East!")
 9   elif action_input == 'w' or action_input == 'W':
10       print("Go West!")
11   else:
12       print("Invalid action!")

如果你现在测试这个游戏,你可以验证动作是否被接受。

家庭作业

尝试以下家庭作业练习:

  1. ===有什么区别?
  2. 创建ages.py来询问用户的年龄,然后打印出一些与他们年龄相关的信息。例如,如果那个人是成年人,如果他们可以买酒,他们可以投票,等等。注意:int()函数可以将字符串转换成整数。

五、函数

在计算机编程中,函数是一个命名的代码块。有时,值被传递到函数中。我们已经看到了一个函数的例子:

print("Hello, World!")

单词print指的是 Python 内核中的一段代码,我们向它传递一个值,以显示在控制台上。如果您做了上一章的功课,您可能还使用了int(),这是 Python 核心中的另一个函数,它接受一个值并将该值转换为一个整数。从视觉上看,因为有了括号,你就知道某个东西是函数。你能想出我们用过的另一个函数吗?

与函数非常相似的是方法。事实上,函数和方法是如此的相似,以至于你会经常看到这两个术语可以互换使用。区别在于,方法是与对象相关联的函数。稍后我们将更多地讨论对象,但是现在把对象想象成应用程序中的一个具体的“东西”——一个人的名字、一个日历日期或者一种喜欢的颜色。一个if语句不是一个对象,一个>=操作符也不是一个对象,等等。一个方法的例子是处理字符串的title()函数。在 Python shell 中尝试一下:

>>> place = "white house"
>>> important_place = place.title()
>>> print(important_place)

你应该看到当你把它打印出来的时候,“white house”变成了大写的“White House”。我们可以看到,title()是一个方法,因为我们需要一个对象(在本例中是字符串“white house”)存在才能使用它。使用.字符引用一个方法。在某些方面,你可以像英语中的所有格“‘s’:place.title()变成“处所的称谓函数”或“属于处所宾语的称谓函数”。

数据输入,数据输出

大多数函数都返回值。例如,int()函数返回我们传入的整数结果,而title()方法则给出一个大写的字符串。其他函数只是“做一些事情”,比如print()函数。它接受一个值并显示文本,但实际上并不返回任何数据。在实践中,我们通常将函数返回的结果存储在一个变量中,而对于像print()这样的函数,我们不会做同样的事情。

1   my_number = '15'
2   # The int() function gives something back, so we save it.

3   my_integer = int(my_number)
4
5   # But this doesn't make sense because print() doesn't give anything back.

6   useless_variable = print(my_integer)

当然,我们并不总是使用 Python 的内置函数;我们经常自己写。请记住,函数是一个已命名的代码块,我们用关键字def来命名函数。下面是一个打印问候语的函数:

1   def say_hello():
2   print("Hello, World!")

为了使用这个函数,我们需要在我们希望函数运行的任何地方写下它的名字来调用它。如下创建hello.py:

hello.py

1   def say_hello():
2   print("Hello, World!")
3
4   say_hello()
5
6   answer = input("Would you like another greeting?")
7   if answer == 'y':
8   say_hello()

每当程序看到say_hello(),它就跳到那个代码块,并在里面做所有的事情。试用这个程序,并验证"Hello, World!"总是至少打印一次,并且根据您的回答可以选择打印第二次。

say_hello()函数不接受数据。我们说这个函数没有任何参数。接受数据的函数必须有一个或多个参数。 1 我们来试试say_hello的改装版:

1 def say_hello(name):
2 print("Hello, " + name)

该函数有一个名为name的参数。当函数运行时,name实际上变成了一个变量,它的值就是传入的值。然后,函数(只有函数)可以在任何需要的地方使用该变量。在此示例中,变量用于在控制台中显示变量的值。

这个函数还使用了+操作符将字符串组合或连接成一个字符串。我们现在已经看到+操作符可以用于数学等式中的数字或字符串。

创建hello_name.py来练习编写参数化函数。

hello_name.py

1   def say_hello(name):
2       print("Hello, " + name)
3
4   user_name = input("What is your name? ")
5
6   say_hello(user_name)

现在我们知道了函数,我们可以组织我们的游戏代码了。切换回game.py,创建一个返回玩家动作的函数。

1   def get_player_command():
2   return input('Action: ')

然后在控制玩家移动的代码中调用这个新函数。

1   print("Escape from Cave Terror!")
2   action_input = get_player_command()

接下来,缩进控制玩家移动的代码,并将其包装在一个函数中。为了节省空间,我不包括整个函数。

1   def play():
2       print("Escape from Cave Terror!")
3       action_input = get_player_command()
4       # Remaining code omitted for brevity

为了让游戏仍然可以玩,在文件底部,简单调用play()函数。您的game.py文件现在应该是这样的:

game.py

 1   def play():
 2       print("Escape from Cave Terror!")
 3       action_input = get_player_command()
 4       if action_input == 'n' or action_input == 'N':
 5           print("Go North!")
 6       elif action_input == 's' or action_input == 'S':
 7           print("Go South!")
 8       elif action_input == 'e' or action_input == 'E':
 9           print("Go East!")
10       elif action_input == 'w' or action_input == 'W':
11           print("Go West!")
12       else:
13           print("Invalid action!")
14
15
16   def get_player_command():
17       return input('Action: ')
18
19
20   play()

从用户的角度来看,游戏和上一章没什么变化。但是从编码的角度来看,我们添加了一些结构来使代码更易于维护。代码组织成函数的方式是决定代码好坏的众多因素之一。随着您阅读和编写更多的代码,您将对自己的代码应该如何组织有更好的感觉。

关于函数还有很多可以说的,事实上函数式编程的整个范例都深入到了函数中。一定要理解本章介绍的概念,因为这本书的其余部分非常依赖它们。

家庭作业

尝试以下作业:

  1. 用什么关键字创建函数?
  2. 无参数函数和参数化函数有什么区别?
  3. 当阅读一个函数的代码时,你如何知道它只是“做一些事情”还是“给出一些回报”?
  4. 创建doubler.py来包含一个名为double的函数,该函数接受单个参数。该函数应该返回乘以 2 的输入值。打印出 12345 和 1.57 的双精度值。
  5. 创建calculator.py来包含一个名为add的函数,它接受两个参数。该函数应该返回两个数的和。打印出 45 和 55 的总和。
  6. 创建user_calculator.py并重用之前练习中的add函数。这一次,要求用户输入两个数字,并打印这两个数字的总和。提示:如果这只适用于整数,那也没关系。

Footnotes 1

函数最多可以有 255 个参数。请不要写 255 个参数的函数!

六、列表

到目前为止,我们一直使用只包含一个值的变量,比如age = 30name = 'Joe'。但是在现实世界中(延伸到计算机程序),将值分组在一起通常是有用的。考虑这样一个程序,它需要显示一个班级中所有学生的名字。这对编码来说真的很烦人:

1   student1 = 'John'
2   student2 = 'Jack'
3   student3 = 'Ashton'
4   student4 = 'Loretta'
5   print(student1)
6   print(student2)
7   print(student3)
8   print(student4)

想象一下,一个班级有 30 或 300 名学生!在本章中,我们将学习如何将这些值组合在一起,并允许它们在代码中作为一个组存在。

什么是列表?

当值被组合成一个变量时,它被称为集合,列表是最常用的集合类型。在 Python 中,列表是用括号和逗号创建的,如下例所示:

students = ['John', 'Jack', 'Ashton', 'Loretta']

这非常方便。我们现在可以编写一次对所有学生通用的代码。这个短程序的一个简化版本(虽然不是完全相同的)就是:

1   students = ['John', 'Jack', 'Ashton', 'Loretta']
2   print(students)

列表有两个定义特征:

  • 这是命令。事物被添加到列表中的顺序被保留。
  • 它可能包含重复项。

这意味着这两个列表不完全相同:

1   list1 = ['John', 'Jack', 'Ashton', 'Loretta']
2   list2 = ['Ashton', 'Jack', 'John', 'Loretta']

这个列表完全没问题:

list1 = ['Buffalo', 'Buffalo', 'Buffalo', 'Buffalo', 'Buffalo']

这些特征可能看起来很明显,但是我们将在后面了解其他无序的和/或不包含重复的集合类型。

除了能够编写整体作用于列表的代码之外,Python 还提供了许多处理列表的便捷方法。

常见列表操作

增加

要向列表添加项目,使用append功能。

>>> my_list = ['A','B','C']
>>> my_list.append('D')
>>> my_list
['A', 'B', 'C', 'D']

长度

为了找出一个列表的长度或大小,我们使用内置的len()函数。

>>> my_list = ['A','B','C']
>>> len(my_list)
3
>>> my_list.append('D')
>>> len(my_list)
4

你可能会奇怪,为什么我们写my_list.append()而不写my_list.len()?原因是len()实际上可以用于列表之外的东西,所以它位于List类之外。在解释器中尝试这些:

>>> len('Hello, World!')
>>> len({})

第一个是字符串,第二个是(空的)字典,这是我们稍后将了解的另一个集合。

得到

要从列表中获取一个特定的条目,您需要知道该条目在列表中的位置。列表中项目的位置也称为索引。如果我们查看列表['A', 'B', 'C', 'D'],这些是项目如何被索引的。

index         0         1         2         3

item          A        B         C         D

请注意,索引从 0 开始。大多数计算机编程语言都是 0 索引的,这意味着计数从 0 开始。

为了获得列表中的第一项,我们使用索引 0:

>>> my_list = ['A', 'B', 'C', 'D']
>>> my_list[0]
A

请确保在指定列表索引时使用方括号,而不是圆括号。

要获得列表中的最后一项,我们可以使用len()函数来帮助:

>>> my_list = ['A', 'B', 'C', 'D']
>>> last_position = len(my_list) - 1
>>> my_list[last_position]
D

搜索

有两种简单的方法来搜索项目列表。第一个将告诉我们一个条目是否在列表中,第二个将告诉我们一个条目在列表中的什么位置。

如果你回想一下关于if语句的那一章,你会学到布尔运算符,比如==<。有一个特殊的布尔运算符可以用于列表,它就是单词in。下面是如何使用in操作符:

>>> 2 in [1, 2, 3]
True
>>> 5 in [1, 2, 3]
False
>>> 'A' in ['A', 'B', 'C']
True

有时,知道项目在列表中的位置是很有用的。为此,我们使用了index()函数。

>>> my_list = ['John', 'Jack', 'Ashton', 'Loretta']
>>> my_list.index('Ashton')
2

如果一个项目出现多次,则返回第一个索引。

>>> my_list = ['Buffalo', 'Buffalo', 'Buffalo']
>>> my_list.index('Buffalo')
0

如果列表中没有某个项目,就会抛出一个错误。

>>> my_list = ['John', 'Jack', 'Ashton', 'Loretta']
>>> my_list.index('Buffalo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'Buffalo' is not in list

你可以在 Python 文档 2 中读到其他有用的列表操作。

向游戏中添加列表

现在我们知道了列表,我们可以为玩家提供一个物品清单。在play函数的顶部,添加以下列表:

1   def play():
2   inventory = ['Dagger','Gold(5)','Crusty Bread']

A458925_1_En_6_Figa_HTML.jpgCustomization Point

您可以通过添加、更改或删除物品来更改玩家的物品清单。

我们还应该允许玩家查看库存,所以让我们用i键打印库存。将此添加到“向西走”行动的正下方:

13   elif action_input == 'i' or action_input == 'I':
14      print("Inventory:")
15      print(inventory)

运行游戏,并验证您可以打印库存。

我们现在也有机会通过将等价的动作(例如‘W’和‘W’)放入一个列表来使我们的代码更简洁。更新play函数中的if语句,如下所示:

 5   if action_input in ['n', 'N']:
 6       print("Go North!")
 7   elif action_input in ['s', 'S']:
 8       print("Go South!")
 9   elif action_input in ['e', 'E']:
10       print("Go East!")
11   elif action_input in ['w', 'W']:
12       print("Go West!")
13   elif action_input in ['i', 'I']:
14       print("Inventory:")
15       print(inventory)

这是个人喜好,但我觉得比以前的版本更容易阅读。使用列表还允许我们以更简单的方式添加字符。例如,如果我们愿意,我们可以通过简单地将>添加到列表:action_input in ['e', 'E', '>']中,使>成为“Go East”的别名。

家庭作业

尝试以下作业:

  1. 哪两个特征使集合成为列表?

  2. 编写允许用户输入他们最喜欢的三种食物的代码。把这些食物列成清单。

  3. 使用索引:['Mercury', 'Venus', 'Earth']打印出该列表的中间项。你能改变你的代码来处理任意大小的列表吗(假设有奇数个条目)?提示:回想一下将某物转换成整数的int()函数。

  4. 运行这段代码会发生什么?你知道为什么吗?

    >>> my_list = ['A','B','C']
    >>> my_list[len(my_list)]
    
    

Footnotes 1

在下一章学习了循环之后,我们可以编写一个行为一致的程序。

  2

https://docs.python.org/3.5/tutorial/datastructures.html#more-on-lists

七、循环

  • All work and no play makes Jack a dull boy
  • All work and no play makes Jack a dull boy
  • All work and no play makes Jack a dull boy
  • All work and no play makes Jack a dull boy

计算机的真正力量在于它们毫无怨言地执行重复任务的能力。一个 CPU 会非常高兴地处理翻转的位,直到它烧坏。只要你给计算器输入数字,它就会一直计算下去。当然——继续在您最喜欢的、目前服务器有问题的网站上使用 F5——您的路由器不会在意。

当我们希望一个计算机程序多次运行同一段代码时,我们将这段代码放在一个循环中。

While 循环

不同的编程语言有不同类型的循环,但大多数情况下有两个主要类别:“做某事直到我说停止”循环和“做某事 N 次”循环。通常,它们分别被称为 while 循环和 for 循环。Python 每种都有一个:一个while循环和一个for-each循环。

事实证明,您真正需要的唯一一个循环是一个while循环。然而,许多编程语言都提供了其他循环关键字来简化循环的编写。

一个while循环总是与一个布尔表达式配对。记住,布尔表达式的值可以是真或假。当条件为真时,循环将继续运行,因此得名。Python 中的while循环是使用while关键字编写的。这里有一个例子:

1   while True:
2   print("All work and no play makes Jack a dull boy")

尝试使用该代码创建一个脚本并运行它。准备好按 Ctrl+C 就行了!当"All work and no play makes Jack a dull boy"被打印到控制台时,您应该会看到文本飞快地闪过。如果不去管它,这段代码会一直运行到计算机关闭,因为布尔表达式True显然总是真的。

我们来看一个更现实的方案。基于上一章作业中的程序(如果你还没有做,现在就应该做!),如果我们希望用户一直输入喜欢的东西,直到完成为止,会怎么样呢?我们不知道他们是否想要输入一个、两个或 20 个项目,所以我们使用一个while循环来继续接受项目。将favorites.py中的代码更改如下:

 1   favorites = []
 2   more_items = True
 3   while more_items:
 4       user_input = input("Enter something you like: ")
 5       if user_input == '':
 6           more_items = False
 7       else:
 8           favorites.append(user_input)
 9
10   print("Here are all the things you like!")
11   print(favorites)

第一行创建一个空列表。每次循环运行时,都会向列表中添加另一个项目。循环中的布尔条件仅仅是more_items,这意味着为了让循环退出,more_items需要为假。我们也可以写while more_items == True,但这是不必要的冗长。要停止添加项目,用户应该输入一个空字符串,只需按 enter 键即可。继续运行这个脚本,看看输出是什么样子。这是我最后得到的结果:

Here are all the things you like!
['family', 'pizza', 'python!']

嗯,看起来不错,但是 Python 打印列表的默认行为不太好。如果我们对如何打印列表有更多的控制权,那就好了…

For-Each 循环

首先,让我们试着打印一份所有收藏项目的列表。为此,我们将使用一个for-each循环。for-each循环之所以得名,是因为它为集合中的每一件事都做了一些事情。这是完美的,因为我们想打印出favorites列表中的每一件事。让我们在文件的顶部添加一个函数,它将打印给定集合的有序列表。

1   def pretty_print_unordered(to_print):
2       for item in to_print:
3           print("* " + str(item))

for-each循环的 Python 语法可读性很强:for variable in collection。变量的名字由我们决定。每次循环运行时,变量都指向集合中的下一项。当循环到达集合末尾时,它将停止运行。

在循环内部,我们可以通过循环语法中定义的变量来访问当前项。为了确保我们可以打印当前项目,变量item被包装在str()函数中,以将项目强制转换为字符串。这个功能就像你以前用过的int()功能一样。如果我们不使用它,Python 会在遇到非字符串项时抛出一个错误。

要使用 pretty print 函数,请更改脚本的结尾并重新运行它。

14   print("Here are all the things you like!")
15   pretty_print_unordered(favorites)

您现在应该会看到类似这样的内容:

Here are all the things you like!
* family
* pizza
* python!

好吧,但是如果我们想要一个有序的数字列表呢?实际上有几种不同的方法可以做到这一点,我们将讨论其中的三种。

循环计数器

如果我们想为每个项目打印一个数字,我们需要有某种方法来跟踪循环中增加的数字和实际的项目。第一种方法是使用计数器。

1   def pretty_print_ordered(to_print):
2       i = 1
3       for item in to_print:
4           print(i + ". " + str(item))
5           i = i + 1

在这个循环中,我们设置i等于1,并且每次循环运行时,我们将i加 1。这种风格的缺点是它需要两行额外的代码,而且我们必须跟踪和更新我们的计数器。另一种选择是使用 Python 的range()函数。

范围

打开 Python shell 并尝试这些:

>>> list(range(5))
>>> list(range(3,7))
>>> list(range(7,3))
>>> list(range(-2,2))

list()函数将范围强制转换成一个我们易于阅读的列表。你有没有注意到int()str()list()的模式?

使用range()给我们一组数字。我们知道一个for-each循环可以操作一个集合。有了这些信息,我们可以按如下方式更改我们的排序列表:

1   def pretty_print_ordered(to_print):
2      for i in range(len(to_print)):
3          print(str(i + 1) + ". " + str(to_print[i]))

这里,我们再次使用len()函数来获取列表的大小,这个数字被传递给range()以给出一个数字列表,这些数字对应于to_print集合中的条目的索引。这似乎有点令人困惑,所以让我们看一个例子:

>>> to_print = ['abc', 'def', 'ghi']
>>> len(to_print)
3
>>> list(range(len(to_print)))
[0, 1, 2]

在这个例子中,列表中有三样东西。因此,我们得到的范围有三个数字:0、1 和 2。

为什么我们必须在 shell 中使用list()而不是在脚本中使用?答案是range()实际上返回的是一个 Python range对象,而不是一个list对象。当脚本运行时,Python 知道如何使用range对象。然而,当我们想要查看REPL中的对象时,我们需要强制它成为一个列表,这样我们就可以一次看到所有的值。使用print(range(3))将打印出无用的字符串"range(3)"

当循环运行时,我们使用来自range函数的数字来定位列表中当前索引处的项目。例如,str(to_print[2])将返回to_print集合中索引 2 处的项目。最后,为了使打印输出对用户友好,我们给str(i + 1)中的每个索引添加一个。如果我们不这样做,我们将得到如下列表:

0\. abc
1\. def
2\. xyz

它是正确的,但不是非常用户友好的。使用range()可能看起来比使用计数器更令人困惑,但是代码更短,也省去了我们维护计数器的麻烦。我们将了解的最后一个选项是我们已经看到的两个选项之间的一个很好的中间地带。

使用枚举

当我们想在一个变量中存储许多相似的东西时,比如教室里的学生,列表就非常有用。但有时我们只有两三个 2 相互紧密关联的变量。在这种情况下,一个列表可能是多余的,所以我们使用一个元组。像列表一样,元组中的内容是有序的,可以重复,但与列表不同,元组的长度是固定的。我们不能从元组中添加或删除项目。以下是元组可能有意义的几种情况:

1   first_name, last_name = ('Barack', 'Obama')
2   month, day, year = (10, 22, 2015)
3   dna_aminos = ('A','T','C','G')

元组语法允许您在左边定义变量名,在右边定义变量值。如果变量和值的数量匹配,则每个变量被赋予元组中出现的下一个值。所以在前面的例子中,month等于10。如果只使用了一个变量名,那么整个元组都被赋给该变量。dna_aminos的值是('A','T','C','G')的总和。

函数也可以返回元组。尝试以下三种不同的脚本:

1   def get_date():
2       return (10, 22, 2015)
3
4   month, day, year = get_date()
5   print(month)

1   def get_date():
2       return (10, 22, 2015)
3
4   date = get_date()
5   print(date)

1   def get_date():
2       return (10, 22, 2015)
3
4   month, day = get_date()
5   print(month)

第一个脚本像预期的那样工作:我们将函数中返回的元组解包成monthdayyear。第二个脚本没有解包元组,而是将所有部分存储在date变量中。最后一个脚本抛出了一个错误,因为返回的元组有三个值,但是我们只使用了两个变量。那么,为什么转向元组呢?好了,下一个我们将要学习的内置函数返回一个元组!

如果您将一个集合传递给enumerate()函数,您将获得一个特殊的 Python 对象 3 ,其行为类似于元组列表。每个元组包含两个值:当前索引和原始列表中的值。尝试运行以下代码:

>>> letters = ['a', 'b', 'c']
>>> list(enumerate(letters))
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> list(enumerate(letters, 1))
[(1, 'a'), (2, 'b'), (3, 'c')]

那么我们如何利用这一点呢?在打印内容的循环中,我们可以枚举列表。

1   def pretty_print_ordered(to_print):
2       for i, value in enumerate(to_print, 1):
3           print(str(i) + ". " + str(value))

for循环中,我们将元组解包到索引i中,并将列表中最喜欢的东西解包到变量value中。现在我们已经有了列表值,我们不必像使用range()函数那样从列表中提取它。我们也不需要知道列表使用了多长时间的len()——枚举器会帮我们处理这个问题。

那么哪个最好呢?正如编程中的许多事情一样,没有一个正确的答案。在不同的情况下,你可以认为这些都是最好的解决方案。在我看来,我更喜欢这个例子中的enumerator。它只需要几行代码,比range选项更容易阅读。你同意吗?如果不是,你更喜欢你的选择是什么?

嵌套

关于循环要介绍的最后一个概念是嵌套。嵌套循环只是另一个循环中的一个循环。这是一个非常简单的嵌套循环,它创建了一个小的乘法问题列表。

1   for i in range(3):
2       for j in range(3):
3           product = i * j
4           print(str(i) + " * " + str(j) + " = " + str(product))

注意,在第二个循环中,我们可以访问第一个循环的索引。 4 事实上第二个循环可以访问第一个循环和第二个循环之间声明的任何变量。

假设我们想找出一系列数字的因子。这需要我们为每个数字准备一个不同的因素列表。利用我们刚刚学到的知识,我们可以编写这个脚本来查找从 1 到 10 的每个数字的因子。

1   for i in range(1,11):
2       factors = []
3       for j in range(1, i + 1):
4           if i % j == 0:
5               factors.append(j)
6       print("The factors of " + str(i) + " are: " + str(factors))

%运算符被称为模或模数运算符,它返回两个数相除后的余数。如果a % b返回 0,那么a可以被b整除,就像在4 % 2中一样。这有一些方便的用途,比如每运行 n 次循环就做一些事情。我们不会经常使用它,但它是一个很好的工具,可以放在您的计算机编程技巧包中。

这段代码可以从第二个循环中访问factors列表,即使变量是在第一个循环中声明的。

游戏循环

在游戏中,大部分代码运行在所谓的游戏循环中。每次循环运行时,游戏世界都会发生变化,用户输入会被传递回程序。在一个 3D 游戏中,这可能每秒发生 60 次!在我们的游戏中,循环不需要运行那么快,因为没有图形需要重画。然而,每次循环运行时,世界将被更新,用户输入将被接受。

将这个循环添加到游戏的play()函数中,并确保缩进函数的其余部分:

1   def play():
2       inventory = ['Dagger','Gold(5)','Crusty Bread']
3       print("Escape from Cave Terror!")
4       while True:
5           action_input = get_player_command()
6           if action_input in ['n', 'N']:

为什么使用了while循环而不是for-each循环?我们不知道这个循环会运行多少次。现在,它无限运行,但即使在真实的游戏中,我们也需要循环运行,直到玩家赢或输。因为我们不知道玩家会走多少圈,所以我们使用了一个while循环。

现在我们知道了如何打印一个列表,让我们修改打印清单的代码。

14   elif action_input in ['i', 'I']:
15       print("Inventory:")
16       for item in inventory:
17           print('*' + str(item))

如果你现在运行游戏,你会看到while循环允许你继续输入命令,而for循环以更好的格式打印清单。要退出游戏,使用 Ctrl+D 或 Ctrl+C。

家庭作业

尝试以下作业:

  1. 对于以下各项,您会使用哪种循环:

    1. 每五秒钟检查一次温度的程序
    2. 在杂货店打印收据的程序
    3. 保龄球比赛中记录分数的程序
    4. 一个随机播放音乐库中歌曲的程序 
  2. 打开关于函数的第五章中的user_calculator.py,并添加一个while循环,允许用户持续添加两个数。

  3. 写一个脚本,显示 1 * 1 到 10 * 10 的乘法表。左上角应该是这样的:

    1 2 3 4
    2 4 6 8
    3 6 9 12
    4 8 12 16
    
    
  4. 使用enumerate%操作符打印列表中的每三个单词:

    ['alpha','beta','gamma','delta','epsilon','zeta','eta']
    
    

Footnotes 1

在有计数器的循环中使用变量i是一种惯例;这是少数不使用描述性变量名的例外情况之一!

  2

实际上,你可以在一个元组中存储大量的条目,但是如果你需要更多的变量,你应该重新考虑使用元组的选择。

  3

如果你想做更多的研究,这个对象叫做迭代器。range()函数也返回一个迭代器。

  4

就像使用i是约定一样,j是嵌套循环中的约定。如果你真的需要他们,kl是下一个。

八、对象

在计算机程序中,对象是存储在计算机内存中的容器,它保存一个或多个值。更简单地说,对象是程序中可用的“东西”。我们已经看到了一些对象:像"Hello, World!"这样的字符串是对象,列表[1, 2, 3]是对象,甚至函数print()也是对象!事实上,在 Python 中,一切都是幕后的对象。但大多数时候,我们实际上对自己创造的东西最感兴趣。本章结束时,你将能够在游戏中添加代表武器的物体。

对象成员

在代码中,对象通常被用作捆绑相关数据的手段。例如,Person对象可能包含一个人的姓名字符串,一个代表他们年龄的数字,以及他们最喜欢的食物列表。对象也可以有自己的函数,称为方法。这些方法通常处理存储在对象内部的数据。我们的Person对象可以有一个根据年龄计算人出生年份的方法。对象的数据和方法统称为对象的成员或属性。

用类定义对象

在创建对象之前,我们需要创建一个类来定义对象。你可以把一个类想象成一张蓝图——它告诉我们如何建造一所房子,但它不是一所房子。我们也可以重复使用相同的蓝图来建造许多相似的房子,即使这些房子在位置、颜色等方面有所不同。为了定义一个类,我们使用class关键字,后跟类的名称,这个名称是按照约定命名的。

创建census.py并添加这个类 1 :

1   class Person:
2       age = 15
3       name = "Rolf"
4       favorite_foods = ['beets', 'turnips', 'weisswurst']
5
6       def birth_year():
7           return 2015 - age

现在,让我们通过创建一些人并找到平均年龄来为我们的人口普查添加一些功能。要创建一个新对象,只需在类名后添加括号。这段代码将创建三个人:

1   people = [Person(), Person(), Person()]
2
3   sum = 0
4   for person in people:
5       sum = sum + person.age
6
7   print("The average age is: " + str(sum / len(people)))

注意,为了访问对象中的数据,我们使用了.操作符,就像访问函数一样。代码list.append()person.age非常相似,因为append()List类的成员,而agePerson类的成员。运行这个程序,不出所料,会告诉我们平均年龄是 15 岁。我们真正需要的是Person类的每个对象或实例能够有不同的年龄、名字和喜欢的食物的值。为此,我们将学习一个可以添加到任何 Python 对象的特殊函数,称为__init__()

使用 init() 初始化对象

在前面的例子中,我们创建了三个相同的人。但是Person类只有在我们可以用它来创建三个名字和年龄不同的人时才有用。一种选择是这样的:

 1   people = [Person(), Person(), Person()]
 2
 3   people[0].name = "Ed"
 4   people[0].age = "11"
 5   people[0].favorite_foods = ["hotdogs", "jawbreakers"]
 6   people[1].name = "Edd"
 7   people[1].age = "11"
 8   people[1].favorite_foods = ["broccoli"]
 9   people[2].name = "Eddy"
10   people[2].age = "12"
11   people[2].favorite_foods = ["chunky puffs", "jawbreakers"]

但是那是相当冗长和乏味的。为了简化对象创建,Python 围绕一个名为__init __()的方法定义了一些特殊行为。首先,该方法在对象创建时立即运行。其次,我们可以向方法添加参数,这些参数在创建对象时成为必需的参数。继续修改你的Person类,使其具有下面的初始化式:

1   class Person:
2       def __init__(self, name, age, favorite_foods):
3           self.name = name
4           self.age = age
5           self.favorite_foods = favorite_foods

初始化器接受传入的参数,并将它们分配给刚刚创建的对象。密切关注的读者可能已经注意到了初始化器中弹出的关键字self。那个关键字 2 是用来指特定的对象。这意味着Person类中的age成员不是某种通用的“年龄”,而是初始化器所作用的这个特定人的年龄。如果有帮助的话,每当你看到self,就认为这个物体指的是它自己。

已经定义了初始化器,我们可以像这样创建人:

1   people = [Person("Ed", 11, ["hotdogs", "jawbreakers"])
2       , Person("Edd", 11, ["broccoli"])
3       , Person("Eddy", 12, ["chunky puffs", "jawbreakers"])]

这比显式设置每个类成员要方便得多!

事实证明,我们还需要在类中访问或操作对象成员的任何地方使用self。这意味着改变birth_year()功能。下面是改装过的Person级:

1   class Person:
2       def __init__(self, name, age, favorite_foods):
3           self.name = name
4           self.age = age
5           self.favorite_foods = favorite_foods
6
7       def birth_year(self):
8           return 2015 - self.age

让我们更新我们的人口普查,也输出平均出生年份。为此,我们只需在每个 person 对象上调用birth_year()函数。当该函数运行时,它是为该特定对象运行的。我们知道这一点是因为函数通过关键字self引用它自己。

18   age_sum = 0
19   year_sum = 0
20   for person in people:
21   age_sum = age_sum + person.age
22   year_sum = year_sum + person.birth_year()
23
24   print("The average age is: " + str(age_sum / len(people)))
25   print("The average birth year is: " + str(int(year_sum / len(people))))

关于self关键字更神奇的是 Python 知道它指的是对象,所以你不必手动传递它。类似于person.birth_year(person)的东西是不必要的,实际上是不正确的。

我还选择将平均年份包含在一个int()函数中,因为年份“2003”比“2003”更有意义。46360 . 68668686666

使用 str()打印对象

假设我们想要显示人口普查中的原始信息。一种简单的方法可能是这样的:

18   print("The people polled in this census were:")
19   print(people)

但是现在当您运行该脚本时,您将会看到类似这样的内容:

The average age is: 11.333333333333334
The average birth year is: 2003
The people polled in this census were:
[<__main__.Person object at 0x10135eb00>, <__main__.Person object at 0x10135eb38>, <__main__.Person object at 0x10135eb70>]

那一点帮助都没有!你看到的是 Python 打印对象的默认实现。它告诉你这个东西是什么类型的对象,在这个例子中是Person,以及它在内存中的位置(内存位置会因计算机而异)。如果我们可以将一个Person传递到print()中,并查看这个人的信息,那就太好了。

幸运的是,Python 提供了一种简单的方法来做到这一点。像 Python 寻找的__init__()函数一样,在打印对象或使用str()函数将对象转换成字符串时,Python 也寻找一个__str__()函数。此方法必须返回一个字符串。继续将这个方法添加到Person类中:

10   def __str__(self):
11       return "Name: " + self.name \
12              + " Age: " + str(self.age) \
13              + " Favorite food: " + str(self.favorite_foods[0])

为了可读性,我们可以使用反斜杠将一个字符串换行到多行。所有这些字符串将被合并成一个字符串,以便函数返回。现在当你运行这个脚本时,同样的事情发生了。啊。

Python 在打印容器(比如列表)时的一个怪癖是没有为容器中的每个对象调用__str__()方法。 3 所以我们需要用循环自己做。

18   for person in people:
19   print(person)

现在,如果你运行这个程序,你应该看到这个:

The average age is: 11.333333333333334
The average birth year is: 2003
The people polled in this census were:
Name: Ed Age: 11 Favorite food: hotdogs
Name: Edd Age: 11 Favorite food: broccoli
Name: Eddy Age: 12 Favorite food: chunky puffs

让使用__str__()更容易的最后一件事是对字符串使用format()方法。使用format()让我们不用连接字符串。下面是编写该方法的另一种方式:

10   def __str__(self):
11       return "Name: {} Age: {} Favorite food: {}".format(
12          self.name, self.age, self.favorite_foods[0])

大括号{}用作占位符,传递给format()的每个参数都按顺序注入占位符中。当您需要连接一串字符串时,这通常更容易读写。你还可以用format()做很多其他的事情,比如填充字符串、截断长十进制数、打印货币等。如果您感兴趣,可以阅读 Python 文档中关于字符串格式化“迷你语言”的所有内容。 4

给游戏添加武器

现在我们知道了如何创建职业,让我们在游戏中添加一些来代表武器。在game.py的顶部添加以下内容:

 1   class Rock:
 2       def __init__(self):
 3           self.name = "Rock"
 4           self.description = "A fist-sized rock, suitable for bludgeoning."
 5           self.damage = 5
 6
 7       def __str__(self):
 8           return self.name
 9
10   class Dagger:
11       def __init__(self):
12           self.name = "Dagger"
13           self.description = "A small dagger with some rust. " \
14                              "Somewhat more dangerous than a rock."
15           self.damage = 10
16
17       def __str__(self):
18           return self.name
19
20   class RustySword:
21       def __init__(self):
22           self.name = "Rusty sword"
23           self.description = "This sword is showing its age, " \
24                              "but still has some fight in it."
25           self.damage = 20
26
27       def __str__(self):
28           return self.name

定义了一个实际的Dagger类后,我们现在可以更新起始库存,以包含一个Dagger对象,而不仅仅是一个表示"Dagger"的字符串。

1   def play():
2      inventory = [Dagger(),'Gold(5)','Crusty Bread']
3      print("Escape from Cave Terror!")

A458925_1_En_8_Figa_HTML.jpgCustomization Point

尝试定义一些你自己的武器类型,比如Crossbow或者Axe。或者如果你的游戏设定在科幻世界,也许你想拥有RayGunShockStick。请确保相应地更新玩家的库存。

当玩家选择展示他们的库存时,你认为会发生什么?试试看,看看你是否正确!

一点面向对象编程

在计算机编程中,我们应该努力遵循的一个原则是“不要重复自己!”还是“干”。如果您发现自己不止一次地键入相同的代码,可能有更好的方法来组织您的代码。你可能已经注意到,对于每种武器来说,__str__()的方法是完全一样的。现在假设我们想改变这个方法。我们必须在三个地方做出改变。谢天谢地,有更好的方法。

面向对象编程(或 OOP)是一种范式,涉及围绕对象的思想构建代码。如前所述,Python 中的一切都是对象,但我们在本章中只是明确地开始创建对象。在此之前,我们能够进行大量的编程,因为 Python 支持 OOP,但并不要求 OOP。在构建我们的游戏时,我们会在有帮助的地方使用一些 OOP,但我们不会为了坚持范例而不必要地强迫自己进入一个盒子。结构化武器代码是 OOP 可以帮助的一个地方。

OOP 中的两个重要概念是组合和继承。复合是指一个对象包含另一个对象。我们在人口普查中看到了这一点,因为每个Person包含一个List最喜欢的食物。继承是指一个类从另一个类继承行为。父类和子类的比喻在这里适用,我们有时将一个类称为“父类”,将从父类继承的类称为“子类”。或者,也使用术语“超类”和“子类”。

为了对武器应用继承,让我们从创建一个父类Weapon开始,并将复制的__str__()方法移动到该类中。

1   class Weapon:
2       def __str__(self):
3           return self.name

为了让一个类从Weapon继承,我们使用语法ClassName(Weapon):。任何从Weapon继承的类都将自动免费获得Weapon类的相同行为。这意味着如果我们让RockDaggerRustySwordWeapon继承,我们可以移除重复的__str__()方法。

 6   class Rock(Weapon):
 7       def __init__(self):
 8           self.name = "Rock"
 9           self.description = "A fist-sized rock, suitable for bludgeoning."
10           self.damage = 5
11
12
13   class Dagger(Weapon):
14       def __init__(self):
15           self.name = "Dagger"
16           self.description = "A small dagger with some rust. " \
17                              "Somewhat more dangerous than a rock."
18           self.damage = 10
19
20
21   class 
RustySword

(Weapon):
22       def __init__(self):
23           self.name = "Rusty sword"
24           self.description = "This sword is showing its age, " \
25                              "but still has some fight in it."
26           self.damage = 20

家庭作业

尝试以下作业:

  1. 类和对象的区别是什么?
  2. 一个类中的__init__()方法的目的是什么?
  3. __str__()str()有什么区别?
  4. 创建一个名为food.py的文件,其中包含一个类Food。这个类应该有四个成员:namecarbsproteinfat。这些成员应该在类的初始化器中设置。
  5. 向名为calories()Food类添加一个方法,计算食物中的卡路里数。每克碳水化合物含 4 卡路里,每克蛋白质含 4 卡路里,每克脂肪含 9 卡路里。
  6. 创建另一个名为Recipe的类,它的初始化器接受一个name和一个名为ingredients的食物列表。向该类添加一个名为calories()的方法,该方法返回食谱中的总热量。
  7. Recipe类添加一个__str__()方法,该方法只返回菜谱的名称。
  8. 创建两个(简单!)食谱,并打印出每个食谱的名称和总热量。如果你愿意,你可以编造碳水化合物、蛋白质和脂肪的数字。为了加分,试着用两个或 200 个食谱的方式来做。
  9. 这个脚本中的类是继承或组合的例子。哪一个,为什么?

Footnotes 1

在一个实际的应用程序中,我们当然会使用 Python 的日期和时间库来计算出生年份,但这将用于我们的演示。

  2

其实自我并不是一个保留的关键词,而是一个大家都遵循的约定。

  3

被搜索的方法称为__repr__()。我们在这里坚持使用__str__(),因为__str__()的目的是使对象可读。__repr__()的目的是在应用出现问题时帮助排除故障。在实际应用中,你可能也想实现__repr__(),但这超出了我们游戏的范围。

  4

https://docs.python.org/3.5/library/string.html#formatspec

九、异常

在一个完美的世界里,程序员从不犯错,用户也总是表现良好。在现实世界中,程序员总是会犯错,用户也从来不会循规蹈矩!当出错时,Python 会抛出异常,这是程序执行时遇到的错误。幸运的是,处理大多数异常并从中恢复是可能的。在这一章中,我们将学习一些我们应该预料到的常见异常,以及如何处理它们。

验证用户输入

假设您想收集一些关于用户的基本数据,然后将一些信息返回给他们。这似乎很简单:

1   name = input("Please enter your name: ")
2   age = input("Please enter your age: ")
3   print("You were born in {}.".format(2015 - int(age)))

你测试了程序,它运行得很好。然后你把它给你的朋友看,她输入“25 年”作为年龄。好吧,这个无用的信息转储到屏幕上,现在看起来你不知道如何编程。

Traceback (most recent call last):
  File "validate.py", line 3, in <module>
    print("You were born in {}.".format(2015 - int(age)))
ValueError: invalid literal for int() with base 10: '25 years'

这是一个异常(特别是一个ValueError),引发它是因为用户输入了一个不能用int()转换成数字的值。作为勤奋的程序员,我们可以预见和计划这种情况。

关键字try允许我们将代码块标记为可能引发异常的东西。接下来是except关键字,它标记了遇到异常时要运行的代码块。

1   name = input("Please enter your name: ")
2   age = input("Please enter your age: ")
3   try:
4       print("You were born in {}.".format(2015 - int(age)))
5   except ValueError:
6       print('Unable to calculate the year you were born, ' \
7             + '"{}" is not a number.'.format(age))

特别注意这里的语法,记住 Python 中的空格是有意义的。缩进告诉我们什么代码是try块的一部分,什么代码是except块的一部分。如果try块中的任何代码遇到ValueError,程序将立即跳转到except块并运行该块中的代码。

检查对象成员

在我们的游戏中,玩家有一些不同的物品清单。我们再补充一个:

inventory = [Rock(), Dagger(), 'Gold(5)', 'Crusty Bread']

有些是武器,有些不是。如果我们想在清单中找到最强大的武器,我们需要检查每个项目,看看它的损害是什么。

59   def most_powerful_weapon(inventory):
60       max_damage = 0
61       best_weapon = None
62       for item in inventory:
63           if item.damage > max_damage:
64              best_weapon = item
65              max_damage = item.damage
66
67       return best_weapon

这应该很简单。该函数循环遍历库存中的所有商品,并检查损失是否大于已经发现的损失。这里新增了一个关键词:None。这是一种价值的缺失。我们最初设置best_weapon等于None,因为如果玩家没有任何武器,这个函数不能返回武器!

如果运行这段代码,很不幸会引发一个异常:

Traceback (most recent call last):
  File "game.py", line 80, in <module>
    play()
  File "game.py", line 46, in play
    best_weapon = most_powerful_weapon(inventory)
  File "game.py", line 59, in most_powerful_weapon
    if item.damage > max_damage:
AttributeError: 'str' object has no attribute 'damage'

这是有道理的,因为“硬皮面包”和“黄金”不会造成损害。因为我们知道库存经常会有非武器,所以我们可以将代码包装在一个try中并处理AttributeError

59   def most_powerful_weapon(inventory):
60       max_damage = 0
61       best_weapon = None
62       for item in inventory:
63           try:
64               if item.damage > max_damage:
65                   best_weapon = item
66                   max_damage = item.damage
67           except AttributeError:
68               pass

69
70       return best_weapon
71
72   play()

如果遇到了一个AttributeError,我们实际上不需要做任何事情,因为我们并不在乎 Crusty Bread 没有一个damage属性。关键字pass可以在我们想跳过或忽略代码块的任何时候使用。请记住,对于大多数异常,您需要做一些事情,比如警告用户或遵循不同的代码路径。对于这种特定的情况,我们可以安全地忽略这个异常。

故意引发异常

乍一看,这似乎有悖常理,但在某些情况下,我们确实希望引发一个异常。当我们想对自己做错的事情大喊大叫时,我们通常会这样做!对坏代码进行检查将有助于我们在测试过程中发现错误。

当前代码中的一个漏洞与Weapon类有关。此代码将导致异常:

1   axe = Weapon()
2   print(axe)

为什么呢?因为Weapon类的__str__()方法在打印对象时会寻找一个name,但是该类没有那个属性。我们可以通过给Weapon指定一个名字来解决这个问题,但是这实际上没有意义,因为这个类太普通了,无法描述。真的,我们永远不应该创造一个Weapon对象;我们应该始终创建一个类似Dagger的特定子类。如果我们需要一个 axe 对象,我们应该创建一个继承自超类WeaponAxe类。

为了防止我们自己意外地创建了Weapon对象,我们可以在初始化器中引发一个异常。

1   class Weapon:
2       def __init__(self):
3           raise NotImplementedError("Do not create raw Weapon objects.")
4
5       def __str__(self):
6           return self.name

Python 内置了NotImplementedError异常,它是一个很好的标记,提醒我们正在做错事。我们可以为异常包含一条消息,以帮助提醒我们问题是什么。如果你想测试这个新代码,试着将Weapon()添加到玩家的物品清单中并运行游戏。您应该会看到以下错误:

Traceback (most recent call last):
  File "game.py", line 72, in <module>
    play()
  File "game.py", line 33, in play
    inventory = [Weapon(), Rock(), Dagger(), 'Gold(5)', 'Crusty Bread']
  File "game.py", line 3, in __init__
    raise NotImplementedError("Do not create raw Weapon objects.")
NotImplementedError: Do not create raw Weapon objects.

当你完成测试时,记得从清单中删除Weapon()

家庭作业

尝试以下作业:

  1. tryexcept更新user_calculator.py来处理没有输入数字的用户。
  2. None是什么意思,什么时候用?
  3. pass是什么意思,什么时候用?
  4. 创建一个Vehicle类,一个Motorcycle类是Vehicle的子类,其wheels属性设置为 2,一个Car类是Vehicle的子类,其wheels属性设置为 4。添加代码,如果程序员试图创建一个Vehicle,将引发一个异常。

十、休息一下

信不信由你,到现在为止,你实际上已经知道了本书将涉及的大部分 Python 材料。将会有一些新的东西需要学习,但是这本书的其余部分将会集中在游戏的制作上。在这个过程中,我们将获得一些构建应用程序的最佳实践和指南。首先,我们将把代码重新组织成几个文件。

将代码组织到多个文件中

首先,我们将创建items.py来存储玩家将与之交互的物品的所有类。现在,我们只有武器,但以后我们会增加更多。

items.py

 1   class Weapon:
 2       def __init__(self):
 3           raise NotImplementedError("Do not create raw Weapon objects.")
 4
 5       def __str__(self):
 6           return self.name
 7
 8
 9   class Rock(Weapon):
10       def __init__(self):
11           self.name = "Rock"
12           self.description = "A fist-sized rock, suitable for bludgeoning."
13           self.damage = 5
14
15
16   class Dagger(Weapon):
17       def __init__(self):
18           self.name = "Dagger"
19           self.description = "A small dagger with some rust. " \
20                              "Somewhat more dangerous than a rock."
21           self.damage = 10
22
23
24   class RustySword(Weapon):
25       def __init__(self):
26           self.name = "Rusty sword"
27           self.description = "This sword is showing its age, " \
28                              "but still has some fight in it."
29           self.damage = 20

接下来,我们将用一个Player类创建player. py。既然库存是真正和玩家关联的,我们就把它作为对象的一个属性。这也意味着与打印库存相关的方法需要转移到Player类中。我们完成重组后会报道import

player.py

 1   import items

 2
 3
 4   class Player:
 5       def __init__(self):
 6           self.inventory = [items.Rock(),
 7                             items.Dagger(),
 8                             'Gold(5)',
 9                             'Crusty Bread']
10
11       def print_inventory(self):
12           print("Inventory:")
13           for item in self.inventory:
14               print('* ' + str(item))
15           best_weapon = self.most_powerful_weapon()
16           print("Your best weapon is your {}".format(best_weapon))
17
18       def most_powerful_weapon(self):
19           max_damage = 0
20           best_weapon = None
21           for item in self.inventory:
22               try:
23                   if item.damage > max_damage:
24                       best_weapon = item
25                       max_damage = item.damage
26               except AttributeError:
27                   pass

28
29           return best_weapon

请注意,这些方法与前面的相似,但不完全相同。现在我们在一个对象内部,我们需要在适当的时候使用self

最后,我们需要清理我们的游戏功能来解释这些变化。

game.py

 1   from player import Player
 2
 3
 4   def play():
 5       print("Escape from Cave Terror!")
 6       player = Player()
 7       while True:
 8           action_input = get_player_command()
 9           if action_input in ['n', 'N']:
10               print("Go North!")
11           elif action_input in ['s', 'S']:
12               print("Go South!")
13           elif action_input in ['e', 'E']:
14               print("Go East!")
15           elif action_input in ['w', 'W']:
16               print("Go West!")
17           elif action_input in ['i', 'I']:
18               player.print_inventory()
19           else:
20               print("Invalid action!")
21
22
23   def get_player_command():
24       return input('Action: ')
25
26
27   play()

从其他文件导入

因为我们将代码移动到了其他文件(或模块)中,所以我们需要一种方法让代码能够引用那些模块。关键字import可以用来从其他模块中获取对象。它出现在 Python 文件的顶部。

有两种主要风格:

import module

from module import ClassName

第一种风格让我们可以访问被引用模块中的所有类。但是,我们必须在该模块中的任何类前面加上模块名。比如在玩家的库存里,我们要写items.Rock(),意思是items模块里的Rock类。如果我们只保留Rock(),Python 会搜索player模块,自然不会找到这个类。

当您只需要一个模块中的一两个类时,通常使用第二种风格。在我们的游戏中,player模块只有一个类,所以我们可以使用任何一种风格。为了可读性,我更喜欢player = Player()而不是player = player.Player(),所以我选择了第二种import样式。

现在运行游戏,并验证游戏是否像以前一样运行。这些变化是重构的一个例子。重构是我们在不影响代码行为的情况下提高代码质量的工作。定期后退一步重构代码总是一个好主意,否则你会发现自己有很多杂乱的代码。在企业界,我们通常称之为“遗留”代码。没人想碰遗留代码。

虽然这里的导入看起来很神奇,但那只是因为所有的模块都在我们运行代码的目录中。Python 在几个不同的位置搜索模块。如果你想了解更多,你可以阅读 PYTHONPATH。否则,请记住,你不能将模块放到文件系统中的任意位置,然后期望它们被 Python 拾取。

家庭作业

这一次,作业是复习:

  1. 回顾一下这些章节,回顾一下你曾经纠结过的事情。否则,休息一下,准备一头扎进世界大厦!

Footnotes 1

https://docs.python.org/3/using/cmdline.html

十一、构建你的世界

在早期,我们给了我们的玩家在游戏世界中移动的能力,但是到目前为止,那个世界只是我们想象中的虚构。在这一章中,我们将最终为玩家创造一个移动的世界。

X-Y 网格

由于这是一个文本冒险,我们只需要担心玩家在两个方向上的移动:向前/向后和向左/向右。这让我们可以像从上往下看玩家一样构建世界,类似于吃豆人或象棋。为了跟踪所有东西的位置,我们使用一个坐标平面,类似于你在数学课上学到的坐标平面。X 轴代表游戏对象的水平位置,Y 轴代表游戏对象的垂直位置。然而,在游戏编程中,我们对网格的定位略有不同。

数学和科学中一个典型的坐标平面是这样的:

(0,2)──(1,2)──(2,2)
  │       │      │
(0,1)──(1,1)──(2,1)
  │       │      │
(0,0)──(1,0)──(2,0)

但是在游戏编程中,我们翻转 Y 轴,使数字向下而不是向上增加。

(0,0)──(1,0)──(2,0)
  │       │      │
(0,1)──(1,1)──(2,1)
  │       │      │
(0,2)──(1,2)──(2,2)

如果我们标记空间而不是交叉点,我们最终会得到一个单元格网格。

╔═════╦═════╦═════╗
║ (0,0)  ║ (1,0)  ║ (2,0)  ║
╠═════╬═════╬═════╣
║ (0,1)  ║ (1,1)  ║ (2,1)  ║
╠═════╬═════╬═════╣
║ (0,2)  ║ (1,2)  ║ (2,2)  ║
╚═════╩═════╩═════╝

我们可以把每个网格单元想象成洞穴的不同部分(或者宇宙飞船中的房间,或者城市街区)。玩家在任何时候都会在一个单元中,在那个单元中他们可能会遇到敌人、战利品或一些可爱的风景。他们可以通过使用已经定义的动作北、南、东和西从一个单元移动到另一个单元。这些动作分别对应于向上(y - 1)、向下(y + 1)、向左(x - 1)和向右(x + 1)。

在我们继续深入之前,让我们将其中的一些内容写成代码。首先用下面的 tile 类创建一个名为world.py的新模块。

 1   class 
MapTile: 

 2       def __init__(self, x, y):
 3           self.x = x
 4           self.y = y
 5
 6       def intro_text(self):
 7           raise NotImplementedError("Create a subclass instead!")
 8
 9
10   class StartTile(MapTile):
11       def intro_text(self):
12           return """
13           You find yourself in a cave with a flickering torch on the wall.
14           You can make out four paths, each equally as dark and foreboding.
15           """
16
17
18   class BoringTile(MapTile):
19       def intro_text(self):
20           return """
21           This is a very boring part of the cave.
22           """
23
24
25   class VictoryTile(MapTile):
26       def intro_text(self):
27           return """
28           You see a bright light in the distance...
29           ... it grows as you get closer! It's sunlight!
30
31
32           Victory is yours!

A458925_1_En_11_Figa_HTML.jpgCustomization Point

更改磁贴的介绍文本以适应您的游戏世界。

MapTile类是定义初始化器的超类。以下子类是游戏中特定类型的瓷砖。(放心吧,我们会搞定BoringTile!)在下一节中将使用intro_text()方法,但是您应该能够猜出它的目的。注意,如果一个淘气的程序员试图直接使用MapTile,我们会抛出一个异常。

您可能已经注意到介绍文本周围的三重引号(""")。Python 允许我们通过用三重引号括起文本来编写多行字符串。这可以使编写长字符串变得更容易。

定义了类之后,我们需要将它们放到一个网格中。

35   world_map = [
36       [None,VictoryTile(1,0),None],
37       [None,BoringTile(1,1),None],
38       [BoringTile(0,2),StartTile(1,2),BoringTile(2,2)],
39       [None,BoringTile(1,3),None]
40   ]

这个列表是表示网格模式的一种方式。“外部”列表代表 Y 轴。因此,“外部”列表中的第一项是整个第一行,而“外部”列表中的第二项是整个第二行。每个“内部”列表代表一行。第一行的第一个项目是网格左上角的单幅图块。最后一行的最后一项是网格右下角的单幅图块。None值用于我们不希望地图图块存在的网格空间。

为了方便起见,我们还添加了一个在坐标上定位图块的函数。

42   def tile_at(x, y):
43       if x < 0 or y < 0:
44           return None
45       try:
46           return world_map[y][x]
47       except IndexError:
48           return None

world_map[y][x]语法可能看起来令人困惑,但那是因为我们正在处理一个列表的列表。world_map[y]部分选择地图的行,添加[x]选择该行中的特定单元格。捕捉IndexError将处理我们传入的坐标大于地图边界而if x < 0 or y < 0处理的坐标小于地图边界的情况。如果没有这个函数,每当我们想要查看一个图块是否存在时,我们就必须不断地检查世界的边界。

在世界上移动

我们添加到游戏中的第一个功能是让用户在游戏世界中移动。然而,到目前为止,这些只是安慰剂作用。为了让玩家移动,我们需要向Player类添加 X-Y 坐标来表示玩家的位置,并且我们需要添加修改这些坐标的方法。从在初始化器中添加self.xself.y开始。

 4   class Player:
 5       def __init__(self):
 6           self.inventory = [items.Rock(),
 7                             items.Dagger(),
 8                             'Gold(5)',
 9                             'Crusty Bread']
10
11           self.x = 1
12           self.y = 2

接下来,在类中添加这些方法:

34   def move(self, dx, dy):
35       self.x += dx
36       self.y += dy
37
38   def move_north(self):
39       self.move(dx=0, dy=-1)
40
41   def move_south(self):
42       self.move(dx=0, dy=1)
43
44   def move_east(self):
45       self.move(dx=1, dy=0)
46
47   def move_west(self):
48       self.move(dx=-1, dy=0)

如果你没有通读作业答案,语法move(dx=0, dy=-1)可能对你来说是新的。这段代码使用命名参数调用move方法。命名参数从来都不是必需的,但是它们可以使代码更容易阅读,特别是当方法中有相同类型的参数时。名字dxdy来自数学,分别表示“x 方向的变化”和“y 方向的变化”。因此move()方法接受 x 和/或 y 方向上的一般变化,特定的方法定义变化的数量。

最后,我们的主游戏循环需要使用这些方法,而不仅仅是打印出占位符文本。跳到game.py并按如下方式更改play()功能。

12   if action_input in ['n', 'N']:
13       player.move_north()
14   elif action_input in ['s', 'S']:
15       player.move_south()
16   elif action_input in ['e', 'E']:
17       player.move_east()
18   elif action_input in ['w', 'W']:
19       player.move_west()
20   elif action_input in ['i', 'I']:
21       player.print_inventory()

现在玩家可以在地图上四处移动,但是我们也应该显示每个区块的介绍文字,让玩家知道他们在哪里。不要忘记导入world模块。

 1   from player import Player
 2   import world

 3
 4
 5   def play():
 6       print("Escape from Cave Terror!")
 7       player = Player()
 8       while True:
 9           room = world.tile_at(player.x, player.y)
10           print(room.intro_text())
11           action_input = get_player_command()

A458925_1_En_11_Figb_HTML.jpgHelp! What’s an AttributeError?

在游戏的这一点上,一个非常常见的问题是你得到一个错误,显示AttributeError: 'NoneType' object has no attribute 'intro_text'

What does it mean?

这意味着 Python 代码说在一个对象上运行intro_text()方法,但是那个对象实际上是None类型。

Why does it happen?

当玩家进入一个不存在的房间时,就会出现错误。更具体地说,当玩家移动到地图上标记为None的部分时。

How do I fix it?

如果错误立即出现,这可能意味着你的播放器的起始位置是错误的。检查Player级的__init()__,确保self.xself.y坐标正确。记得从零开始计数!

如果在你四处走动的时候错误出现了,你是在走进一个不存在的房间。如果你想让这个房间存在,改变你的地图。如果您不小心移动到那里,您会发现一个错误,我们将很快修复它!

你现在应该可以测试游戏,验证你可以在世界各地移动。现在有一些错误。值得注意的是,当你到达VictoryTile时,游戏并没有结束,玩家还可以环绕地图。我们会修复这些错误,但是现在,享受这开始感觉更像一个游戏的事实吧!