Python-少儿编程第二版-全-

Python 少儿编程第二版(全)

原文:zh.annas-archive.org/md5/511a357c0eb5b9cb48a60882cf0a6c9f

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

图片

为什么要学习计算机编程?编程能够促进创造力、推理能力和问题解决能力。程序员有机会从无到有地创造东西,运用逻辑将编程结构转化为计算机可以执行的形式,当事情不像预期那样顺利时,使用问题解决技能找出出错的地方。编程是一项有趣的、有时具有挑战性(并且偶尔令人沮丧)的活动,从中获得的技能在学校和工作中都能派上用场——即使你的职业与计算机无关。而且,如果什么都没有,编程也是一个度过阴雨下午的好方式。

为什么选择 Python

Python 是一种易于学习的编程语言,对于初学者来说,它具有一些非常有用的功能。与其他编程语言相比,Python 的代码非常容易阅读,而且它提供了一个交互式 shell,你可以在其中输入程序并查看程序运行结果。

除了简单的语言结构和可以进行实验的交互式 shell,Python 还具有一些功能,这些功能极大地增强了学习过程,并允许你为自己创建简单的游戏动画。其中之一是 turtle 模块,灵感来自于 1960 年代 Logo 编程语言使用的 Turtle 图形,旨在用于教育用途。另一个是 tkinter 模块,它是 Tk 图形用户界面(GUI)工具包的接口,提供了一种创建稍微复杂一些的图形和动画程序的简单方法。

如何学习编程

就像任何第一次尝试的事情一样,最好的方法是从基础开始,所以从第一章开始,抵制跳到后面章节的冲动。没有人能在第一次拿起乐器时就演奏一首交响乐。学员飞行员在理解基本操控之前是不能飞飞机的。体操运动员通常不会在第一次尝试时就做后空翻。如果你过于急于跳跃,你不仅会发现基础概念没有记住,而且后面章节的内容会显得比实际复杂。

在你学习本书的过程中,尝试所有示例代码,以便你能够了解它们是如何工作的。大多数章节还包括一些编程难题,让你尝试,这将有助于提高你的编程技能。记住,你越是理解基础,后来理解更复杂的概念就会越容易。当你遇到困难或感到挑战时,这里有一些我觉得有用的方法:

  1. 将一个问题分解成更小的部分。尝试理解一小段代码在做什么,或者只考虑一个难点的某个小部分(集中注意力在一小段代码上,而不是一次性试图理解全部内容)。

  2. 如果这还不能解决问题,最好先放一放,休息一会儿,等第二天再来看看。这是解决许多问题的好方法,对于计算机程序员尤为有用。

谁应该阅读本书

本书适合任何对计算机编程感兴趣的人,无论是第一次接触编程的孩子还是成年人。如果你想学会如何编写自己的软件,而不是仅仅使用别人开发的程序,Python for Kids 是一个很好的起点。

在接下来的章节中,你将找到帮助你安装 Python、启动 Python Shell 进行基本计算、在屏幕上打印文本、创建列表并使用 if 语句和 for 循环进行简单控制流操作的信息(你还将学习什么是 if 语句和 for 循环!)。你将学习如何通过函数重用代码、类和对象的基础知识,并了解一些 Python 内置函数和模块。

你还会看到关于简单和高级海龟图形的章节,以及使用 tkinter 模块在计算机屏幕上绘图的内容。许多章节结尾处有不同复杂度的编程难题,帮助你通过自己动手写小程序来巩固新学的知识。掌握了基本的编程知识后,你将学习如何编写自己的游戏。你将开发两个图形化游戏,并了解基本的碰撞检测、事件处理和不同的动画技巧。

本书中的大多数示例使用 Python 的 IDLE(集成开发环境)Shell。IDLE 提供了语法高亮、复制粘贴功能(类似于其他应用程序的操作)以及一个编辑窗口,可以让你保存代码以供日后使用。这意味着 IDLE 既是一个交互式的实验环境,也像一个文本编辑器。虽然这些示例在标准控制台和普通文本编辑器中也能正常工作,但 IDLE 的语法高亮和稍微更人性化的环境有助于理解,因此第一章会教你如何进行设置。

本书内容

下面是你将在每一章中找到的内容概览。

第一章 是编程入门,提供了首次安装 Python 的指导。

第二章 介绍了基本的计算和变量,第三章 描述了一些基本的 Python 数据类型,如字符串、列表和元组。

第四章 是对海龟模块的初步了解。我们将从基础编程跳跃到让海龟(形状像箭头)在屏幕上移动。

第五章 介绍了条件语句和 if 语句的变化,第六章 则讲解了 for 循环和 while 循环。

第七章 是我们开始使用和创建函数的地方,接着在 第八章 介绍类和对象。我们涵盖了足够的基本概念,以支持本书后面游戏开发章节中的一些编程技巧。到此为止,内容开始变得有些复杂。

第九章 将回到 turtle 模块,帮助你尝试更复杂的图形。第十章 则讲解如何使用 tkinter 模块来创建更高级的图形。

第十一章第十二章 中,我们将创建第一个游戏 Bounce!,它基于前面章节所学的知识,而在 第十三章第十六章 中,我们将制作另一个游戏 Mr. Stick Man Races for the Exit。游戏开发章节是最容易出错的地方。如果遇到问题,可以从配套网站下载代码 (python-for-kids.com),并与这些可运行的示例代码进行比较。

最后,在 后记 中,我们简要介绍了如何使用 Python 包管理工具(pip)安装 PyGame 模块,并给出了一个简短的 PyGame 示例,之后还会展示其他编程语言的一些示例。

附录 A 中,你可以找到 Python 关键字的列表,在 附录 B 中列出了部分有用的内置函数(你将在书中的后续部分了解 关键字函数 的概念)。附录 C 提供了常见问题的故障排除信息。

Python for Kids 网站

如果在阅读过程中遇到困难,可以访问 python-for-kids.com 寻求帮助。在网站上,你可以找到本书所有示例的下载链接,并提供进一步的信息,包括如何下载本书中使用的源代码。

祝你玩得开心!

在你学习本书的过程中,记住编程是有趣的。不要把它当作工作来看待。把编程当作一种创建有趣游戏或应用程序的方式,这些你可以与朋友或他人分享。

学习编程是一个极好的脑力锻炼过程,成果也可以非常有价值。但最重要的是,无论你做什么,享受其中的乐趣!

第一部分**

学习编程

第一章:并非所有的蛇都会滑行

Image

计算机程序是一组指令,使计算机执行某种操作。它不是计算机的物理部件——例如电线、微芯片、卡片、硬盘等——而是在这些硬件上运行的隐形内容。我通常将计算机程序简称为程序,它是一组指令,告诉硬件该做什么。软件是计算机程序的集合。

没有计算机程序,几乎你每天使用的每个设备都将停止工作或变得不如现在那么有用。计算机程序以某种形式控制着你个人的计算机,也控制着视频游戏系统、手机以及汽车中的 GPS 单元。软件还控制着像 LCD 电视及其遥控器、以及一些最新的收音机、DVD 播放器、烤箱和冰箱等设备。甚至汽车发动机、交通信号灯、街道灯、火车信号、电子广告牌和电梯也都由程序控制。

程序有点像思想。如果你没有思想,你可能只是坐在地板上,空洞地盯着一面墙。你的思想“从地板上起来”是一条指令或命令,它告诉你的身体站起来。同样,计算机程序也使用命令来告诉计算机该做什么。

如果你知道如何编写计算机程序,你就能做各种有用的事情。当然,你可能不能写出控制汽车、交通信号灯或冰箱的程序(至少一开始不能),但你可以创建网页,编写自己的游戏,甚至制作一个帮助做作业的程序。

关于语言的几句话

像人类一样,计算机使用多种语言进行沟通——这些语言叫做编程语言。编程语言只是一种通过使用人类和计算机都能理解的指令与计算机沟通的方式。

有些编程语言是以人名命名的(如 Ada 和 Pascal),有些是使用简单的缩写命名的(如 BASIC 和 FORTRAN),甚至还有一些编程语言是以电视节目命名的,比如 Python。是的,Python 编程语言是以蒙提·派森飞行马戏团(Monty Python’s Flying Circus)电视节目命名的,而不是因为蛇。

注意

蒙提·派森飞行马戏团是一个 1970 年代首次播出的另类英国喜剧节目,今天在某些观众群体中依然非常受欢迎。节目包含了像“傻乎乎的走路部”、 “打鱼舞蹈”和“奶酪店”(它根本不卖奶酪)这样的滑稽小品。

Python 编程语言有许多特性,使其对初学者极为有用。最重要的是,你可以快速编写简单而高效的程序。Python 不像其他编程语言那样使用许多复杂的符号,这使得它更容易阅读,也对初学者更友好。(这并不是说 Python 不使用符号——只是它不像许多其他语言那样频繁使用符号。)

安装 Python

安装 Python 是相当简单的。这里,我们将介绍在 Windows、macOS、Ubuntu 和 Raspberry Pi 上安装 Python 的步骤。在安装 Python 时,你还将安装 IDLE 程序,它是 Integrated DeveLopment Environment(集成开发环境),让你能够编写 Python 程序。如果你的电脑已经安装了 Python,可以跳到 第 10 页的“安装 Python 后”部分继续。

在 Windows 上安装 Python

要在 Microsoft Windows 11 上安装 Python,请下载一个版本为 3.10 或更高的 Windows 版 Python,下载地址是 www.python.org/downloads/。你下载的具体版本不重要,只要它至少是 3.10 版本。然而,如果你使用的是旧版 Windows(如 Windows 7),最新版本的 Python 将无法使用——在这种情况下,你需要安装 Python 3.8。有关哪个版本的 Python 可以与您的 Windows 版本兼容,请参见 Windows 下载页面 (www.python.org/downloads/windows/)

Image

图 1-1:Windows 的 Python 下载

如果浏览器询问是否保存或打开文件,请选择保存。下载完 Python Windows 安装文件后,你应该会看到提示让你运行它。如果没有提示,请打开 下载 文件夹并双击该文件。接下来,按照屏幕上的安装说明,将 Python 安装到默认位置,步骤如下:

  1. 点击 立即安装

  2. 当询问是否允许该应用对你的设备进行更改时,选择

  3. 安装完成后点击 关闭,然后你应该能在 Windows 开始菜单中看到多个 Python 3.1x 的图标:

Image

图 1-2:你的开始菜单可能会因使用的 Python 版本不同而有所不同。

现在跳到 第 10 页的“安装 Python 后”部分,开始使用 Python。

在 macOS 上安装 Python

如果你使用的是 Mac,应该会预装一个版本的 Python,但可能是较旧版本的语言。为了确保你使用的是足够新的版本,点击右上角的聚光灯图标(放大镜),在出现的对话框中输入 terminal。当终端打开后,输入 python3 --version(注意是两个短横线,后面跟着 version)并按下 ENTER 键。

如果你看到“命令未找到”或者版本低于 3.10,请在浏览器中访问以下 URL 以下载适用于 macOS 的最新安装程序: www.python.org/downloads/

Image

图 1-3:macOS 的 Python 下载

下载完成后,双击该文件(它应该叫做类似于 python-3.10.0-macosx11.pkg 的名字)。同意许可协议并按照屏幕上的提示安装软件。安装 Python 之前,系统会提示你输入 Mac 的管理员密码。如果你没有密码,向你的父母或计算机所有者询问。

Image

图 1-4:Mac Finder 中的 Python

跳转到 第 10 页的“安装 Python 后”部分,开始使用 Python。

在 Ubuntu 上安装 Python

Python 在 Ubuntu Linux 上预装,但可能不是最新版本。按照这些说明操作,获取最新版本的 Python(请注意,接下来的命令中可能需要更改版本号,以反映最新版本)。

  1. 点击显示应用程序图标(通常位于屏幕左下角,形状是九个点)。

  2. 在输入框中输入terminal(如果已显示终端,则点击它)。

  3. 当终端窗口出现时,输入:

    sudo apt update
    sudo apt install python3.10 idle-python3.10
    

    输入第一个命令后,系统可能会提示你输入计算机的管理员密码(如果没有管理员密码,你可能需要向父母或老师请求输入)。

Image

图 1-5:Ubuntu 终端中的 Python 安装;根据你下载的版本,输出可能略有不同

跳转到 第 10 页的“安装 Python 后”部分,开始使用 Python。

在树莓派上安装 Python(树莓派操作系统或 Raspbian)

树莓派的操作系统预装了 Python 3,但在写这篇文章时,版本是 3.7。安装更高版本比其他操作系统稍微复杂一些——你需要自己下载并构建 Python 安装。听起来可能有点吓人,但其实并不复杂。只需依次输入以下命令,并等待每个命令完成(请注意,如果你下载的 Python 版本高于 3.10,可能需要更改版本号):

sudo apt update
sudo apt install libffi-dev libssl-dev tk tk-dev
wget https://www.python.org/ftp/python/3.10.0/Python-3.10.0.tar.xz
tar -xvf Python3.10.0.tar.xz
cd Python-3.10.0
./configure --prefix=/usr/local/opt/python-3.10.0
make -j 4
sudo make altinstall

倒数第二步将花费最长的时间,因为它正在构建所有的代码,这些代码将被加入到 Python 应用程序中。

Image

图 1-6:树莓派终端中的 Python 安装;根据你下载的 Python 版本,输出可能会略有不同。

安装完 Python 后,你需要将一个名为 IDLE 的程序添加到菜单中(这会使后续操作更方便):

  1. 点击屏幕左上角的树莓派图标,然后点击首选项主菜单编辑器

  2. 在弹出的窗口中,点击编程,然后点击新建项目按钮。

  3. 在图 1-7 所示的启动器属性对话框中,输入名称为idle3.10,并将此作为命令输入,必要时更改版本号:

    /usr/local/opt/python-3.10.0/bin/idle3.10
    
  4. 点击 确定,然后在主编辑窗口中再次点击 确定 来完成。然后你就可以继续进行下一部分了。

Image

图 1-7:树莓派上的启动器设置

安装 Python 后

安装 Python 后,让我们在 IDLE(也叫 Shell)中编写第一个程序。

如果你使用的是 Windows,在 Windows 搜索框(屏幕左下角)输入 idle,然后在最佳匹配菜单中选择 IDLE (Python 3.1x 64-bit)

如果你使用的是 Mac,导航到 前往应用程序,然后打开 Python 3.1x 文件夹找到 IDLE。

如果你使用的是 Ubuntu,当你点击 显示应用程序,然后点击底部的 所有 标签,你应该能看到一个名为 IDLE (使用 Python-3.1x) 的条目 —— 如果看不见它,你也可以在搜索框中输入 IDLE。

如果你使用的是树莓派,点击屏幕左上角的树莓派图标,点击 编程,然后从显示的列表中选择 idle3.1x

当你打开 IDLE 时,应该会看到如下窗口:

Image

图 1-8:Windows 中的 IDLE Shell

这就是 Python Shell,它是 Python 集成开发环境的一部分。三个大于号(>>>)叫做 提示符

让我们从提示符处输入一些命令,开始时输入以下内容:

>>> print("Hello World")

确保包括双引号(“ ”)。完成输入后按键盘上的 ENTER 键。如果你输入正确的命令,应该会看到如下内容:

>>> print("Hello World")
Hello World
>>>

提示符应该会重新出现,提醒你 Python Shell 已经准备好接受更多命令。

恭喜你!你刚刚创建了第一个 Python 程序。单词 print 是一种 Python 命令,叫做 函数,它会把括号内的内容打印到屏幕上。从本质上讲,你已经给计算机下达了一个指令,显示出 “Hello World” —— 这个指令既是你理解的,也是计算机能够执行的。

Image

保存你的 Python 程序

如果每次使用 Python 程序时都需要重新编写它们,那这些程序就没有太大用处,更别提打印出来方便查看了。虽然重写短程序可能没问题,但一个大型程序,如文字处理器,可能包含数百万行代码。如果把这些都打印出来,可能会有超过 100,000 页。试想一下,拿着这么一大摞纸回家会是什么样子。希望你不会遇到大风。

幸运的是,我们可以保存程序以备后用。要创建并保存一个新程序,打开 IDLE,选择 文件新建 窗口。一个空白窗口会出现,菜单栏中显示 无标题。在新出现的 Shell 窗口中输入以下代码:

>>> print("Hello World")

现在,选择 文件保存。当提示输入文件名时,输入 hello.py,并将文件保存到桌面。然后选择 运行运行模块

如果一切顺利,你保存的程序应该会运行,像这样:

Image

图 1-9:IDLE 中的 Hello World

现在,如果你关闭了 shell 窗口,但保持 hello.py 窗口打开,然后选择 运行运行模块,Python Shell 应该会重新出现,且你的程序将再次运行(如果只想重新打开 Python Shell 而不运行程序,选择 运行Python Shell)。

运行代码后,你会在桌面上看到一个新的图标,名为 hello.py。如果你双击该图标,一个黑色的窗口会短暂出现,然后消失。发生了什么?

你看到的是 Python 命令行控制台(类似于 shell)启动,打印 Hello World,然后退出。如果你拥有超能力般的视力,能在窗口关闭前看到它,这就是你会看到的内容:

Image

图 1-10:控制台中的 Hello World

注意

根据你的操作系统,这可能无法正常工作——或者它可能会使用与我们安装的版本不同的 Python 版本运行。

除了菜单,你还可以使用键盘快捷键来创建新的 shell 窗口、保存文件并运行程序:

  • 在 Windows、Ubuntu 和 Raspberry Pi 上,按 CTRL-N 创建新的 shell 窗口,按 CTRL-S 保存文件(编辑完成后),按 F5 运行程序。

  • 在 macOS 上,按 COMMAND-N 创建新的 shell 窗口,按 COMMAND-S 保存文件,按住功能键(Fn)并按 F5 运行程序。

你学到了什么

在本章中,我们从一个简单的 Hello World 应用程序开始——这是几乎每个人学习计算机编程时都会首先编写的程序。在下一章,我们将使用 Python Shell 做一些更有用的事情。

第二章:计算与变量

Image

现在你已经安装了 Python,并且知道如何启动 Python Shell,你准备开始使用它了。我们将从一些简单的计算开始,然后学习如何使用变量。变量是用来在计算机程序中存储数据的,它们能帮助你编写有用的程序。

使用 Python 进行计算

通常,当要求找出两个数字的积,例如 8 × 3.57,你会使用计算器或铅笔和纸。那么,如何使用 Python Shell 来进行这个计算呢?我们试试看。

双击桌面上的 IDLE 图标启动 Python Shell,或者如果你使用的是 Ubuntu,可以在应用程序菜单中点击 IDLE 图标。在提示符下,输入这个计算:

>>> 8 * 3.57
28.56

在 Python 中输入乘法计算时,你需要使用星号符号(*)而不是乘号(×)。

我们来试试一个更有用的方程怎么样?

假设你在后院挖掘,发现了一个装有 20 枚金币的袋子。第二天,你偷偷跑到地下室,把金币放进了你祖父的蒸汽驱动复制机器里(幸运的是,你可以刚好把 20 枚金币塞进去)。你听到了一阵嗖嗖声和一声爆响,几个小时后,又飞出另外 10 枚闪闪发光的金币。

如果你每天都这样做一年,你的宝箱里会有多少枚金币?在纸面上,这些方程可能长得像这样:

10 × 365 = 3650

3650 + 20 = 3670

当然,用计算器或者纸笔做这些计算很简单,但我们同样可以用 Python Shell 来做这些计算。首先,我们将 10 枚金币乘以一年 365 天,得到 3650。接着,我们再加上原来的 20 枚金币,得到 3670。

>>> 10 * 365
3650
>>> 3650 + 20
3670

现在,如果一只乌鸦看到你房间里闪亮的金币,每周飞进来偷走三枚金币,那你一年后还剩多少金币呢?这个计算在 Python Shell 中长得像这样:

>>> 3 * 52
156
>>> 3670 - 156
3514

首先,我们将 3 枚金币乘以一年 52 周。结果是 156。然后我们从总金币数(3670)中减去这个数字,得出一年结束时我们剩下的金币数是 3514。

尽管你可以轻松地用计算器完成这个计算,但通过 Python Shell 来处理它,有助于学习编写简单的计算机程序。在本书中,你将学习如何扩展这些思路,编写更有用的程序。

Python 运算符

你可以在 Python Shell 中进行加法、减法、乘法和除法等数学运算,我们稍后会探索更多其他运算。Python 用来进行数学运算的基本符号,被称为运算符,它们列在 表 2-1 中。

表 2-1: 基本 Python 运算符

符号 运算
+ 加法
- 减法
* 乘法
/ 除法

斜杠(/)用于除法,因为它类似于你在写分数时使用的除法线。例如,如果你有 100 个海盗和 20 个大桶,你想计算每个桶可以藏多少个海盗,你可以通过在 Python Shell 中输入 100 / 20 来计算 100 个海盗除以 20 个桶(100 ÷ 20)。只需要记住,斜杠的上半部分是向右倾斜的。

运算顺序

我们在编程语言中使用括号来控制运算顺序。运算是任何使用运算符的操作。乘法和除法的优先级高于加法和减法,因此它们会先执行。换句话说,如果你在 Python 中输入一个方程式,乘法或除法会先于加法或减法执行。

例如,在下面的方程中,首先将数字 30 和 20 相乘,然后将数字 5 加到它们的积上:

>>> 5 + 30 * 20
605

这个方程是另一种表达方式:“将 30 乘以 20,然后将结果加上 5。” 结果是 605。我们可以通过在前两个数字周围加上括号来改变运算顺序,像这样:

>>> (5 + 30) * 20
700

这个方程的结果是 700(而不是 605),因为括号告诉 Python 先做括号内的运算,然后再做括号外的运算。这个例子表示:“将 5 加到 30,然后将结果乘以 20。”

括号可以嵌套,这意味着括号可以嵌套在括号内,像这样:

>>> ((5 + 30) * 20) / 10
70.0

在这种情况下,Python 首先计算最内层的括号,然后是外层的括号,最后是除法运算符。换句话说,这个方程表示:“将 5 加到 30,然后将结果乘以 20,再将结果除以 10。” 发生了以下情况:

  1. 将 5 加到 30 得到 35。

  2. 将 35 乘以 20 得到 700。

  3. 将 700 除以 10 得到最终答案 70。

如果我们没有使用括号,结果会稍有不同:

>>> 5 + 30 * 20 / 10
65.0

在这种情况下,30 首先乘以 20(得到 600),然后 600 除以 10(得到 60)。最后,加上 5 得到结果 65。

注意

记住,除非使用括号来控制运算顺序,否则乘法和除法总是先于加法和减法。

变量就像标签一样

在编程中,变量一词描述了一个存储信息的地方,如数字、文本、数字和文本的列表等等。变量本质上是某物的标签。

例如,要创建一个名为 fred 的变量,我们使用等号( = )然后告诉 Python 这个变量应该为哪个信息贴上标签。这里,我们创建了变量 fred,并告诉 Python 它是数字 100 的标签(请注意,这并不意味着另一个变量不能具有相同的值):

>>> fred = 100

要找到一个变量的值,在 Python Shell 中输入 print,然后是变量名并加上括号,像这样:

>>> print(fred)
100

我们还可以告诉 Python 改变变量 fred 使其标记其他内容。例如,下面是如何将 fred 改为数字 200:

>>> fred = 200
>>> print(fred)
200

在第一行,我们说 fred 给数字 200 贴上了标签。在第二行,我们打印出 fred 的值,只是为了确认变化。Python 在最后一行打印结果。

我们还可以为同一项使用多个标签(或变量):

>>> fred = 200
>>> john = fred
>>> print(john)
200

在这个例子中,我们告诉 Python,我们希望变量 john 和 fred 标签相同的内容,通过在 john 和 fred 之间使用等号。

当然,fred 可能不是一个很有用的变量名,因为它很可能并没有告诉我们变量的用途。让我们把变量叫做 number_of_coins,而不是 fred,如下所示:

>>> number_of_coins = 200
>>> print(number_of_coins)
200

这清楚地表明我们在讨论 200 枚硬币。变量名可以由字母、数字和下划线(_)组成,但不能以数字开头。你可以使用从单个字母(如 a)到长句子的变量名。(变量名不能包含空格,所以使用下划线分隔单词。)有时,如果你只是做些快速操作,一个简短的变量名会比较好。你选择的变量名应该根据你对变量名的意义要求来定。

现在你知道了如何创建变量,接下来让我们看看如何使用它们。

使用变量

还记得我们用来计算如果你能用祖父在地下室的神秘发明魔法地创造新硬币,到年底你会有多少硬币的方程吗?我们有这个方程:

>>> 20 + 10 * 365
3670
>>> 3 * 52
156
>>> 3670 - 156
3514

我们可以将其转化为一行代码:

>>> 20 + 10 * 365 - 3 * 52
3514

这不太容易阅读,但如果我们把数字转成变量呢?试试输入以下内容:

>>> found_coins = 20
>>> magic_coins = 10
>>> stolen_coins = 3

这些条目创建了变量 found_coins、magic_coins 和 stolen_coins。

现在,我们可以像这样重新输入方程:

>>> found_coins + magic_coins * 365 - stolen_coins * 52
3514

你可以看到,这样我们得到了相同的结果。那么,谁在乎呢,对吧?啊,但这就是变量的魔力。如果你在窗户上放了一个稻草人,而乌鸦只偷了两枚硬币而不是三枚呢?当我们使用变量时,我们只需将变量更改为新的数字,它会在方程式中的所有使用位置自动更新。我们可以通过输入以下内容将 stolen_coins 变量改为 2:

>>> stolen_coins = 2

然后我们可以复制并粘贴方程来重新计算结果,如下所示:

  1. 通过点击鼠标并从行首拖动到行尾来选择要复制的文本,如图 2-1 所示。

Image

图 2-1:在 Python Shell 中选择内容

  1. 按住 CTRL 键(如果你使用的是 Mac,则是 COMMAND 键 ⌘)并按 C 键复制选中的文本。(从现在起你会看到 CTRL-C)。

  2. 点击最后一行提示(在 stolen_coins = 2 后)。

  3. 按住 CTRL(或 COMMAND)键,然后按 V 键粘贴选中的文本。(从现在起你会看到 CTRL-V)。

  4. 按 ENTER 键查看新结果。

Image

图 2-2:在 Python Shell 中粘贴内容

这比重新输入整个方程要简单得多!

你可以尝试改变其他变量,然后复制(CTRL-C)并粘贴(CTRL-V)计算结果,看看你改变的效果。例如,如果你在正确的时刻敲打你祖父的发明,它每次会吐出三个额外的硬币,你会发现到年底你将得到 4661 个硬币:

>>> magic_coins = 13
>>> found_coins + magic_coins * 365 stolen_coins * 52
4661

当然,对于像这样的简单方程使用变量仍然只是稍微有用。我们还没有进入真正有用的部分。现在,记住变量是标记事物的一种方式,方便你稍后使用它们。

你学到的内容

在本章中,你学习了如何使用 Python 运算符进行简单的方程计算,以及如何使用括号来控制运算顺序(即 Python 评估方程各部分的顺序)。接着,我们创建了变量来标记值,并在计算中使用了这些变量。

第三章:字符串、列表、元组和字典

Image

在第二章中,我们用 Python 做了一些基本的计算,并学习了变量。在本章中,我们将处理 Python 程序中的其他元素:字符串、列表、元组和字典。你将使用字符串在程序中显示消息(例如游戏中的“准备好”和“游戏结束”消息)。你还将发现如何使用列表、元组和字典来存储事物的集合。

字符串

在编程中,我们通常将文本称为字符串。把字符串想象成字母的集合。书中的所有字母、数字和符号都可以是一个字符串,你的名字和地址也是如此。事实上,我们在第一章中创建的第一个 Python 程序就使用了一个字符串:“Hello World”。

创建字符串

在 Python 中,我们通过将引号放在文本周围来创建字符串,因为编程语言需要区分不同类型的值。(我们需要告诉计算机一个值是数字、字符串还是其他类型。)例如,我们可以从第二章中取出 fred 变量,并用它来标记一个字符串:

Image

fred = "Why do gorillas have big nostrils? Big fingers!!"

然后,为了查看“fred”里面的内容,我们可以输入 print(fred):

>>> print(fred)
Why do gorillas have big nostrils? Big fingers!!

你也可以使用单引号来创建一个字符串,像这样:

>>> fred = 'What is pink and fluffy? Pink fluff!!'
>>> print(fred)
What is pink and fluffy? Pink fluff!!

然而,如果你试图使用单引号(′)或双引号(")输入超过一行的文本,或者如果你以一种类型的引号开始,以另一种类型的引号结束,你将会在 Python Shell 中看到一个错误消息。例如,输入以下内容:

>>> fred = "How do dinosaurs pay their bills?

你会看到这个结果:

SyntaxError: EOL while scanning string literal

这是一个关于语法错误的错误信息,因为你没有遵循用单引号或双引号结束字符串的规则。

语法意味着句子中单词的排列和顺序,或者在这种情况下,程序中单词和符号的排列和顺序。所以语法错误意味着你做了一些 Python 没有预料到的事情,或者 Python 期待你做的事情你却漏掉了。EOL表示行尾,因此错误消息的其余部分告诉你 Python 到达了行尾,但没有找到一个双引号来关闭(或结束)字符串。

要在字符串中使用多行文本(称为多行字符串),请使用三个单引号(’’’),然后在行与行之间按 ENTER 键,像这样:

>>> fred = '''How do dinosaurs pay their bills?
    With tyrannosaurus checks!'''

让我们打印出 fred 的内容,看看是否成功:

>>> print(fred)
How do dinosaurs pay their bills?
With tyrannosaurus checks!

处理字符串问题

现在考虑这个傻乎乎的字符串示例,它会导致 Python 显示错误消息:

>>> silly_string = 'He said, "Aren't can't shouldn't wouldn't."'
SyntaxError: invalid syntax

在第一行,我们试图创建一个由单引号包围的字符串(定义为变量 silly_string),但它也包含了单引号(在单词 can't、shouldn't 和 wouldn't 中)和双引号。真是一团糟!

请记住,Python 没有人类那样的智慧,它看到的只是一个字符串,其中包含了 He said, "Aren,然后是一些它无法预期的字符。当 Python 看到一个引号(无论是单引号还是双引号)时,它期望在第一个引号之后跟随一个字符串,并且该字符串会在下一个匹配的引号(无论是单引号还是双引号)出现时结束。在这种情况下,字符串的开始是 He 前的单引号,而对于 Python 来说,字符串的结束是 Aren 后的单引号。

在最后一行中,Python 告诉我们发生了什么类型的错误——在这个例子中,是一个语法错误。

使用双引号代替单引号仍然会产生错误:

>>> silly_string = "He said, "Aren't can't shouldn't wouldn't.""
SyntaxError: invalid syntax

在这里,Python 看到的是一个由双引号括起来的字符串,包含字母 He said,(和一个空格)。接下来跟着的内容(从 Aren’t 开始)会导致错误。

这是因为从 Python 的角度来看,所有多余的内容本不应该存在。Python 会寻找下一个匹配的引号,并且不知道你希望它如何处理紧跟在该引号后面的内容。

解决这个问题的方法是使用多行字符串,我们之前学过的,采用三个单引号(''')。这使得我们可以在字符串中同时使用单引号和双引号而不会导致错误。事实上,如果我们使用三个单引号,我们可以在字符串内放置任何组合的单引号和双引号(只要我们不尝试在其中放入三个单引号)。我们字符串的无错版本如下所示:

Image

silly_string = '''He said, "Aren't can't shouldn't wouldn't."'''

但等等,还有更多。如果你真的想使用单引号或双引号来包围 Python 中的字符串,那么可以在每个引号前面添加一个反斜杠(∖)。这被称为转义。这是一种告诉 Python,“是的,我知道我的字符串中有引号,我希望你忽略它们,直到你看到结束引号。”

转义字符串可能会使它们更难以阅读,因此通常建议使用多行字符串。不过,你仍然可能会遇到使用转义的代码,因此了解为什么反斜杠存在是有帮助的。

下面是一些转义如何工作的示例:

➊ >>> single_quote_str = 'He said, "Aren\'t can\'t shouldn\'t
       wouldn\'t."'
➋ >>> double_quote_str = "He said, \"Aren't can't shouldn't 
       wouldn't.\""
   >>> print(single_quote_str)
   He said, "Aren't can't shouldn't wouldn't."
   >>> print(double_quote_str)
   He said, "Aren't can't shouldn't wouldn't."

首先,在 ➊ 处,我们创建了一个包含单引号的字符串,并在该字符串中的单引号前使用了反斜杠。接着,在 ➋ 处,我们创建了一个包含双引号的字符串,并在这些引号前使用了反斜杠。在接下来的几行中,我们打印了刚刚创建的变量。注意,当我们打印它们时,反斜杠字符并不会出现在字符串中。

将值嵌入字符串

如果你想使用变量的内容显示消息,你可以将其嵌入到一个特殊的字符串中,称为f-string(也叫格式化字符串字面量)。你将变量名放在大括号中,这样它就会被实际的值替代。(嵌入值,也叫做字符串替换,是程序员用语,意思是“插入值”。)

例如,为了让 Python 计算或存储你在游戏中得分的点数,然后将其添加到类似 “I scored 10 points” 的句子中,你只需在第一个引号前加上 f,然后将数字 10 替换为用大括号 {} 包围的变量,像这样:

>>> myscore = 1000
>>> message = f'I scored {myscore} points'
>>> print(message)
I scored 1000 points

在这里,我们创建了变量 myscore,值为 1000,并创建了包含 f-string 'I scored {myscore} points' 的变量 message。在下一行中,我们调用 print(message) 来查看字符串替换的结果。打印此消息的结果是 "I scored 1000 points"。我们不需要为消息使用变量。我们可以做同样的示例,只使用这个:

print(f'I scored {myscore} points')

我们还可以在字符串中使用多个变量:

>>> first = 0
>>> second = 8
>>> print(f'What did the number {first} say to the number {second}? Nice belt!!')
What did the number 0 say to the number 8? Nice belt!!

我们甚至可以在 f-string 中放入表达式,像这样:

>>> print(f'Two plus two equals {2 + 2}')
Two plus two equals 4

在这个示例中,Python 会计算大括号中的简单方程式,所以打印出来的字符串包含了结果。

乘法字符串

10 乘以 5 是多少?答案当然是 50。那么 10 乘以a呢?这是 Python 的回答:

>>> print(10 * 'a')
aaaaaaaaaa

Python 程序员可能出于多种原因乘以字符串,例如在 Python Shell 中显示消息时,按照特定的空格数量来对齐文本。试着在 Python Shell 中打印以下字母(选择文件新建文件,然后输入以下代码):

spaces = ' ' * 25
print('{} 12 Butts Wynd')
print('{} Twinklebottom Heath')
print('{} West Snoring')
print()
print()
print('Dear Sir')
print()
print('I wish to report that tiles are missing from the')
print('outside toilet roof.')
print('I think it was bad wind the other night that blew them away.')
print()
print('Regards')
print('Malcolm Dithering')

一旦你在 Python Shell 窗口中输入了代码,选择文件另存为。将文件命名为myletter.py。然后你可以通过选择运行运行模块来运行代码(就像我们之前做的那样)。

在这个示例的第一行,我们通过将一个空格字符乘以 25 来创建变量空间。然后,我们在接下来的三行中使用这个变量,将文本对齐到 Python Shell 的右侧。你可以在下面看到这些打印语句的结果:

Image

图 3-1:在 Python Shell 中运行字母代码

什么是文件和文件夹?

一个文件是某种类型的数据(或信息),可以存储在你的计算机上。文件可能包括照片、视频、电子书,甚至是你在 Microsoft Word 中写的学校报告。

一个文件夹(也叫做目录)是其他文件夹和文件的集合。当你点击另存为来保存你的myletter.py文件时,它被保存在一个文件夹里。

正如我们所看到的,文件和文件夹在编程中非常重要。

除了使用乘法进行对齐,我们还可以用它来填充屏幕上烦人的消息。试试以下示例:

>>> print(1000 * 'snirt')

列表比字符串更强大

“蜘蛛腿、青蛙脚趾、蝙蝠翅膀、蛞蝓黄油和蛇的头皮屑”不是一个非常正常的购物清单(除非你恰好是个巫师),但我们将它作为字符串和列表之间差异的第一个例子。我们可以通过使用如下的字符串将这个物品清单存储在 wizard_list 变量中:

Image

>>> wizard_list = 'spider legs, toe of frog, bat wing, slug butter, snake dandruff'
>>> print(wizard_list)
spider legs, toe of frog, bat wing, slug butter, snake dandruff

但我们也可以创建一个列表,这是一个可以操作的有些神奇的 Python 对象。以下是将这些物品写成列表后的样子:

>>> wizard_list = ['spider legs', 'toe of frog', 'bat wing',
                   'slug butter', 'snake dandruff']
>>> print(wizard_list)
['spider legs', 'toe of frog', 'bat wing', 'slug butter',
'snake dandruff']

创建一个列表需要比创建字符串更多的输入,但列表比字符串更有用,因为列表中的项可以轻松地进行操作。我们可以通过在方括号内输入一个数字(称为索引位置)来打印列表中的某个项,如下所示:

>>> print(wizard_list[2])
bat wing

如果你本来期待第二项是“青蛙脚趾”,你可能会想知道为什么打印的是“蝙蝠翅膀”。这是因为列表从索引位置 0 开始,所以列表中的第一个项是 0,第二个是 1,第三个是 2。对人类来说可能没有什么意义,但对计算机来说是可以理解的。

我们也可以改变列表中的项。也许我们的巫师朋友刚刚告诉我们,我们需要为他们拿蜗牛舌头,而不是蝙蝠翅膀。以下是我们如何改变列表中的项:

>>> wizard_list[2] = 'snail tongue'
>>> print(wizard_list)
['spider legs', 'toe of frog', 'snail tongue', 'slug butter',
'snake dandruff']

这将把索引位置 2(之前是蝙蝠翅膀)的项设置为蜗牛舌头。

我们还可以显示列表中的子列表。我们通过在方括号内使用冒号(:)来做到这一点。例如,输入以下内容来查看我们列表中的第三到第五项(这是制作美味三明治的绝佳配料):

Image

>>> print(wizard_list[2:5])
['snail tongue', 'slug butter', 'snake dandruff']

写 [2:5] 就等同于说,“显示从索引位置 2 到(但不包括)索引位置 5 的项”——换句话说,是项 3、4 和 5。

列表可以用来存储各种各样的项,比如数字:

>>> some_numbers = [1, 2, 5, 10, 20]

它们也可以保存字符串:

>>> some_strings = ['Which', 'Witch', 'Is', 'Which']

它们可能包含数字和字符串的混合:

>>> numbers_and_strings = ['Why', 'was', 6, 'afraid', 'of', 7, 
                           'because', 7, 8, 9]
>>> print(numbers_and_strings)
['Why', 'was', 6, 'afraid', 'of', 7, 'because', 7, 8, 9]

并且列表甚至可以存储其他列表:

>>> numbers = [1, 2, 3, 4]
>>> strings = ['I', 'kicked', 'my', 'toe', 'and', 'it', 
               'is', 'sore']
>>> mylist = [numbers, strings]
>>> print(mylist)
[[1, 2, 3, 4], ['I', 'kicked', 'my', 'toe', 'and', 'it',
'is', 'sore']]

这个列表中的列表示例创建了三个变量:包含四个数字的 numbers,包含八个字符串的 strings,以及使用数字和字符串的 mylist。第三个列表(mylist)只有两个元素,因为它是变量名的列表,而不是变量的内容。

我们可以尝试分别打印 mylist 的两个元素:

>>> print(mylist[0])
[1, 2, 3, 4]
>>> print(mylist[1])
['I', 'kicked', 'my', 'toe', 'and', 'it', 'is', 'sore']

在这里,我们可以看到 mylist[0] 包含了数字列表,而 mylist[1] 是字符串列表。

向列表中添加项

要向列表的末尾添加项,我们使用 append 函数。例如,要将一声熊打嗝(我敢肯定确实有这么一回事)添加到巫师的购物清单中,可以输入以下内容:

>>> wizard_list.append('bear burp')
>>> print(wizard_list)
['spider legs', 'toe of frog', 'snail tongue', 'slug butter', 
'snake dandruff', 'bear burp']

你可以像这样继续添加更多的魔法物品到巫师的清单中:

>>> wizard_list.append('mandrake')
>>> wizard_list.append('hemlock')
>>> wizard_list.append('swamp gas')

现在巫师的清单看起来是这样的:

>>> print(wizard_list)
['spider legs', 'toe of frog', 'snail tongue', 'slug butter',
'snake dandruff', 'bear burp', 'mandrake', 'hemlock', 'swamp gas']

巫师显然准备施展一些真正的魔法了!

从列表中删除项

要从列表中删除项,可以使用 del 命令(简写为delete)。例如,要删除巫师清单中的第五项蛇的头皮屑,可以这样做:

>>> del wizard_list[4]
>>> print(wizard_list)
['spider legs', 'toe of frog', 'snail tongue', 'slug butter',
'bear burp', 'mandrake', 'hemlock', 'swamp gas']

注意

记住,位置从零开始,所以 wizard_list[4] 实际上指的是列表中的第五个项目。

尝试通过输入以下内容来移除我们刚刚添加的项目(曼德拉草、毒芹和沼气):

>>> del wizard_list[7]
>>> del wizard_list[6]
>>> del wizard_list[5]
>>> print(wizard_list)
['spider legs', 'toe of frog', 'snail tongue', 'slug butter',
'bear burp']

列表算术

我们可以通过将列表相加来连接它们,就像加法一样,使用加号 (+) 符号。例如,假设我们有两个列表:list1,包含数字 1 到 4,以及 list2,包含一些单词。我们可以使用 + 符号将它们加在一起,如下所示:

>>> list1 = [1, 2, 3, 4]
>>> list2 = ['I', 'tripped', 'over', 'and', 'hit', 'the', 'floor']
>>> print(list1 + list2)
[1, 2, 3, 4, 'I', 'tripped', 'over', 'and', 'hit', 'the', 'floor']

我们也可以将两个列表相加,并将结果赋值给另一个变量:

>>> list1 = [1, 2, 3, 4]
>>> list2 = ['I', 'ate', 'chocolate', 'and', 'I', 'want', 'more']
>>> list3 = list1 + list2
>>> print(list3)
[1, 2, 3, 4, 'I', 'ate', 'chocolate', 'and', 'I', 'want', 'more']

我们还可以通过数字来乘以一个列表。例如,要将 list1 乘以 5,我们写 list1 * 5:

>>> list1 = [1, 2]
>>> print(list1 * 5)
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]

这告诉 Python 将 list1 重复五次,结果是 1, 2, 1, 2, 1, 2, 1, 2, 1, 2。另一方面,除法(/)和减法(-)会导致错误,如以下示例所示:

>>> list1 / 20
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
    list1 / 20
TypeError: unsupported operand type(s) for /: 'list' and 'int'

>>> list1 - 20
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
    list1 - 20
TypeError: unsupported operand type(s) for -: 'list' and 'int'

那为什么呢?嗯,使用 + 连接列表和用 * 重复列表是足够直接的操作。这些概念在现实世界中也很有意义。例如,如果我递给你两张纸质购物清单并说:“把这两个清单加在一起,”你可能会把所有项目按顺序写在另一张纸上,从头到尾。如果我说:“将这个清单乘以 3,”你也可以想象把列表中的所有项目写三遍在另一张纸上。

但你如何将一个列表划分呢?例如,考虑一下如何将六个数字(1 到 6)的列表分成两部分。这里有三种不同的方式:

[1, 2, 3]     [4, 5, 6]
[1]           [2, 3, 4, 5, 6]
[1, 2, 3, 4]  [5, 6]

我们是将列表从中间分开,分开第一个项目后再分,还是随便挑个地方分开呢?没有简单的答案,当你请求 Python 对列表进行除法操作时,它也不知道该怎么做。这就是为什么它会返回错误的原因。

Image

由于同样的原因,你不能将除列表之外的任何东西添加到列表中。例如,下面是我们试图将数字 50 加入 list1 时发生的情况:

>>> list1 + 50
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
    list1 + 50
TypeError: can only concatenate list (not "int") to list

为什么这里会出现错误呢?嗯,将 50 加入一个列表是什么意思?是将 50 加到每个项目吗?但如果项目不是数字呢?是将数字 50 加到列表的末尾还是开头呢?

在计算机编程中,命令每次输入时应该表现得完全相同。你的计算机只能看到黑白的东西。让它做一个复杂的决策时,它就会抛出错误。

元组

元组就像一个使用括号的列表,如这个例子所示:

>>> fibs = (0, 1, 1, 2, 3)
>>> print(fibs[3])
2

这里我们将变量 fibs 定义为数字 0、1、1、2 和 3。然后,就像操作列表一样,我们使用 print(fibs[3]) 来打印元组中索引位置 3 的项目。

元组和列表的主要区别在于,一旦你创建了一个元组,它就不能改变。例如,如果我们试图将元组 fibs 中的第一个值替换为数字 4(就像我们在 wizard_list 中替换值一样),我们会收到错误消息:

>>> fibs[0] = 4
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
    fibs[0] = 4
TypeError: 'tuple' object does not support item assignment

为什么你会选择使用元组而不是列表呢?有时候,使用一个你知道永远不会改变的东西是有用的。如果你创建一个包含两个元素的元组,它将始终包含这两个元素。

Python 字典

在 Python 中,dict(字典的缩写)是一个集合,类似于列表和元组。字典和列表或元组的区别在于,字典中的每个项都有一个和相应的

例如,假设我们有一个人及其最爱运动的列表。我们可以将这些信息放入 Python 列表中,每个人的名字后面跟着他们的运动,像这样:

>>> favorite_sports = ['Ralph Williams, Football',
                       'Michael Tippett, Basketball',
                       'Edward Elgar, Baseball',
                       'Rebecca Clarke, Netball',
                       'Ethel Smyth, Badminton',
                       'Frank Bridge, Rugby']

如果我问你丽贝卡·克拉克最喜欢的运动是什么,你可以浏览这个列表并找到答案是网球。但是如果这个列表包括 100 个(或者更多)人呢?

图片

如果我们将相同的信息存储在字典中,以人的名字为键,最爱运动为值,那么代码将是这样:

>>> favorite_sports = {'Ralph Williams' : 'Football', 
                       'Michael Tippett' : 'Basketball',
                       'Edward Elgar' : 'Baseball',
                       'Rebecca Clarke' : 'Netball',
                       'Ethel Smyth' : 'Badminton',
                       'Frank Bridge' : 'Rugby'}

我们使用冒号将每个键与其值分开,每个键和值都被单引号括起来。还要注意,字典中的项被大括号({})括起来,而不是圆括号或方括号。结果是一个字典(每个键指向一个特定的值),如表 3-1 所示。

表 3-1: 字典中指向值的键:最爱运动

拉尔夫·威廉姆斯 足球
迈克尔·蒂佩特 篮球
爱德华·埃尔加 棒球
丽贝卡·克拉克 网球
埃塞尔·史密斯 羽毛球
弗兰克·布里奇 橄榄球

现在,要获取丽贝卡·克拉克最喜欢的运动,我们通过使用她的名字作为键,访问我们的字典 favorite_sports,如下所示:

>>> print(favorite_sports['Rebecca Clarke'])
Netball

答案是网球。

要在字典中删除一个值,使用它的键。例如,让我们移除埃塞尔·史密斯:

>>> del favorite_sports['Ethel Smyth']
>>> print(favorite_sports)
{'Rebecca Clarke': 'Netball', 'Michael Tippett': 'Basketball', 
'Ralph Williams': 'Football', 'Edward Elgar': 'Baseball', 
'Frank Bridge': 'Rugby'}

要在字典中替换一个值,我们也需要使用它的键。假设我们需要将拉尔夫·威廉姆斯的最爱运动从足球改为冰球,我们可以像这样操作:

>>> favorite_sports['Ralph Williams'] = 'Ice Hockey'
>>> print(favorite_sports)
{'Rebecca Clarke': 'Netball', 'Michael Tippett': 'Basketball', 
'Ralph Williams': 'Ice Hockey', 'Edward Elgar': 'Baseball', 
'Frank Bridge': 'Rugby'}

我们通过使用拉尔夫·威廉姆斯这个键,将最爱运动从足球替换为冰球。

如你所见,处理字典有点像处理列表和元组,唯一的不同是,你不能使用加号运算符(+)连接字典。如果你尝试这样做,你会收到一个错误消息,正如下面的例子所示:

>>> favorite_sports = {'Rebecca Clarke': 'Netball',
                       'Michael Tippett': 'Basketball',
                       'Ralph Williams': 'Ice Hockey',
                       'Edward Elgar': 'Baseball',
                       'Frank Bridge': 'Rugby'}
>>> favorite_colors = {'Malcolm Warner' : 'Pink polka dots',
                       'James Baxter' : 'Orange stripes',
                       'Sue Lee' : 'Purple paisley'}
>>> favorite_sports + favorite_colors

Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
    favorite_sports + favorite_colors
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

Python 无法理解连接字典,所以它放弃了。

你学到的内容

在本章中,你学到了 Python 如何使用字符串存储文本,以及如何使用列表和元组处理多个项。你看到列表中的项是可以修改的,并且你可以将一个列表与另一个列表连接,但元组中的值不能改变。你还学到了如何使用字典来存储带有键的值,以便识别它们。

编程难题

以下是你可以自己尝试的一些实验。答案可以在python-for-kids.com找到。

#1: 收藏夹

列出你最喜欢的爱好,并将这个列表命名为 games。现在列出你最喜欢的食物,并将该列表命名为 foods。将这两个列表合并,命名合并后的结果为 favorites。最后,打印变量 favorites。

#2: 计算战斗者

如果有三座楼,每座楼的屋顶上藏着 25 个忍者,而有两个隧道,每个隧道里藏着 40 个武士,那么忍者和武士即将展开战斗的数量是多少?(你可以在 Python Shell 中通过一个方程式来解决这个问题。)

#3: 问候!

创建两个变量:一个指向你的名字,一个指向你的姓氏。现在创建一个字符串,使用占位符来打印你的名字并附带一条消息,类似于“你好,Brando Ickett!”

#4: 多行信件

使用我们在本章前面创建的信件,尝试通过一次打印调用(并使用多行字符串)打印出完全相同的文本。

第四章:使用海龟绘图

Image

Python 中的海龟与现实中的海龟有些不同。我们知道海龟是一种爬行动物,移动非常缓慢,并且背上背着自己的壳。在 Python 中,海龟是一个小的黑色箭头,缓慢地在屏幕上移动。实际上,考虑到 Python 海龟在屏幕上移动时会留下痕迹,它更像是蜗牛或蛞蝓,而不太像海龟。

在本章中,我们将使用 Python 的 Turtle 库,通过绘制一些简单的形状和线条来学习计算机图形学的基础知识。

使用 Python 的 Turtle 模块

在 Python 中,模块是程序员为其他程序员提供有用代码的一种方式。(模块中可以包含我们可以使用的函数。)我们将在第七章中深入学习模块和函数。

海龟是 Python 中的一个特殊模块,我们可以用它来学习计算机如何在屏幕上绘制图像。海龟模块是一种编程向量图形的方式,基本上就是用简单的线条、点和曲线进行绘图。

Image

让我们看看海龟是如何工作的。首先,启动 Python Shell。接下来,通过导入 turtle 模块告诉 Python 使用海龟,命令如下:

>>> import turtle

导入一个模块告诉 Python 你想要使用它。

创建画布

现在我们已经导入了 turtle 模块,我们需要创建一个 画布——一个空白的绘图区域,就像艺术家的画布一样。为此,我们调用 turtle 模块中的 Turtle 函数,它会自动创建一个画布(我们将在第七章中了解更多关于函数的内容)。

在 Python Shell 中输入以下内容:

>>> t = turtle.Turtle()

你应该能看到一个空白的框(画布),中央有一个箭头(类似于图 4-1)。屏幕中央的箭头就是海龟,而你是对的——它看起来并不像真正的海龟。

Image

图 4-1:在 Python Shell 中运行海龟

注意

如果 turtle 模块似乎没有正常工作,可以在附录 C 中尝试一些额外的步骤。

移动海龟

你可以通过使用我们刚才创建的变量 t 上的函数来向海龟发送指令,这就像在 turtle 模块中使用 Turtle 函数一样。例如,forward 指令告诉海龟向前移动。要让海龟前进 50 像素,可以输入以下命令:

Image

>>> t.forward(50)

你应该能看到类似于图 4-2 的内容。

Image

图 4-2:海龟向前移动

海龟已经向前移动了 50 个像素。像素是屏幕上的一个点——它是可以表示的最小元素。你在计算机显示器上看到的所有内容,都是由像素组成的,它们是微小的正方形点。如果你能够放大画布并查看海龟画的线条,你会看到,表示海龟路径的箭头其实就是由一堆像素组成的。这是简单计算机图形学的基础。

Image

图 4-3:像素就是点!

现在我们将告诉海龟向左转 90 度,使用以下命令:

>>> t.left(90)

如果你还没有学习过度数,可以想象你站在一个圆的中心:

  • 你面朝的方向是 0 度。

  • 如果你伸出左臂,那就是向左 90 度。

  • 如果你伸出右臂,那就是向右 90 度。

你可以在图 4-4 中看到这个 90 度的左转或右转。

Image

图 4-4:向左和向右 90 度

如果你从右臂指向的方向继续沿着圆周向右转,180 度是你身后的方向,270 度是左臂指向的方向,360 度是回到起点;度数从 0 到 360。顺时针旋转的度数增量可以在图 4-5 中看到,每次增量为 45 度。

Image

图 4-5:45 度增量

当 Python 的海龟向左转时,它会旋转并面向新的方向(就像你转身面向手臂指向的方向,向左转 90 度)。t.left(90) 命令将箭头指向上方(因为它最初指向的是右侧)。你可以在图 4-6 中看到这个效果。

Image

图 4-6:海龟左转后的状态

注意

当你调用 t.left(90) 时,它的效果与调用 t.right(270) 在海龟最终朝向的方向上是相同的。当调用 t.right(90) 时,效果也与 t.left(270) 相同。只需要想象那个圆,并跟随度数来理解。

现在我们将绘制一个正方形。将以下代码添加到你已经输入的代码中:

>>> t.forward(50)
>>> t.left(90)
>>> t.forward(50)
>>> t.left(90)
>>> t.forward(50)
>>> t.left(90)

你的海龟应该已经画出了一个正方形,并且现在面朝它开始时的方向,如图 4-7 所示。

Image

图 4-7:海龟绘制正方形

要清除画布,可以输入 t.reset()。这将清空画布,并将海龟恢复到起始位置。

>>> t.reset()

你也可以使用 t.clear(),它只会清空屏幕,并让海龟保持当前位置。

>>> t.clear()

我们还可以让海龟向右转或向后移动。我们可以使用 up 来抬起“画笔”(也就是说,告诉海龟停止绘图),使用 down 来重新开始绘制。这些功能的写法与我们之前使用的其他命令相同。

让我们尝试使用这些命令进行另一个绘图。这一次,我们将让海龟画两条线。请输入以下代码:

>>> t.reset()
>>> t.backward(100)
>>> t.up()
>>> t.right(90)
>>> t.forward(20)
>>> t.left(90)
>>> t.down()
>>> t.forward(100)

首先,我们清空画布,并使用 t.reset() 将海龟移回起始位置。接着,我们使用 t.backward(100) 将海龟向后移动 100 像素,然后用 t.up() 抬起画笔停止绘制。

然后,使用 t.right(90) 命令,我们将海龟向右转 90 度,指向屏幕底部,再用 t.forward(20) 向前移动 20 像素。由于在第三行我们使用了 up 命令,所以没有绘制任何内容。接着,我们用 t.left(90) 将海龟向左转 90 度,面朝右侧,然后使用 down 命令让海龟重新开始绘制。最后,我们画出一条与我们之前绘制的第一条平行的线,使用 t.forward(100)。这两条平行线最终看起来像图 4-8。

图片图片

图 4-8:海龟绘制平行线

你学到了什么

在本章中,你学习了如何使用 Python 的海龟模块。我们绘制了一些简单的线条,使用了左转、右转以及向前和向后的命令。你了解了如何使用 up 命令停止海龟绘制,并使用 down 命令重新开始绘制。你还发现海龟是按角度转动的。

编程谜题

尝试用海龟绘制以下一些形状。解决方案可以在 python-for-kids.com 上找到。

#1:一个矩形

使用海龟模块的 Turtle 函数创建一个新的画布,然后绘制一个矩形。

#2:一个三角形

创建另一个画布并绘制一个三角形。回顾一下圆形的图示(“移动海龟”在第 45 页),提醒自己应该将海龟转向哪个方向。

#3:没有角的框

编写一个程序,绘制图 4-9 中显示的四条线(大小不重要,形状最重要)。

图片

图 4-9:海龟画的没有角的框

#4:没有角的倾斜框

编写一个程序,绘制图 4-10 中显示的四条线(与之前的谜题类似,但框的方向倾斜)。同样,框的大小并不重要——只要形状正确即可。

图片

图 4-10:海龟画的没有角的倾斜框

第五章:使用 if 和 else 提问

图片

在编程中,我们经常提出“是”或“否”的问题,并根据答案执行某些操作。例如,我们可能会问:“你是否超过 20 岁?”如果答案是“是”,则回应“你太老了!”这类问题叫做条件,我们将条件及其响应组合到 if 语句中。条件可以比一个简单的问题更复杂,if 语句可以结合多个问题,并根据每个问题的答案执行不同的响应。在本章中,你将学习如何使用 if 语句来构建程序。

if 语句

我们可能会在 Python 中写出这样的 if 语句:

>>> age = 13
>>> if age > 20:
        print('You are too old!')

if 语句由 if 关键字组成,后面跟着条件和冒号(:),例如 this if age > 20: 语句。冒号后面的行必须属于一个代码块;如果问题的答案是“是的”(或 True),那么代码块中的命令将会执行。现在,让我们探索如何编写代码块和条件。

图片

注意

True 是一个 布尔 值,以数学家 George Boole 命名。布尔值只能有两个值:True 或 False。

一个代码块是一个编程语句的集合

一个代码块是一个编程语句的集合。例如,当 if age > 20: 为 True 时,你可能不仅仅想打印出“You are too old!”也许你想打印更多的句子,例如:

>>> age = 25
>>> if age > 20:
        print('You are too old!')
        print('Why are you here?')
        print('Why aren\'t you mowing a lawn or sorting papers?')

这个代码块由三个 print 调用组成,只有当条件 age > 20 被判断为 True 时才会执行。代码块中的每一行前面都有四个空格。让我们再看看这段代码,并显示空格:

>>> age = 25
>>> if age > 20:
 .... print('You are too old!')
 .... print('Why are you here?')
 .... print('Why aren\'t you mowing a lawn or sorting papers?')

在 Python 中,空白字符——比如制表符(按下 TAB 键时插入)或空格(按下空格键时插入)——是有意义的。处于相同位置(或从左边距缩进相同数量的空格)的代码会被归为一个代码块。每当你用比上一行更多的空格开始新的一行时,就意味着你开始了一个新的代码块。这个新代码块也是前一个代码块的一部分,就像图 5-1 所示。

图片

图 5-1:代码块如何工作

我们将语句归为代码块,因为它们是相关的,且需要一起执行。当你改变代码的缩进时,通常是在创建新的代码块。图 5-2 展示了仅通过改变缩进就可以创建三个代码块。

图片

图 5-2:第二个示例展示了代码块如何工作

在这里,尽管第 2 和第 3 个代码块具有相同的缩进,它们仍然被视为不同的代码块,因为它们之间存在一个缩进较少(空格较少)的代码块。

一个包含四个空格的代码块在一行上,而下一行包含六个空格时,在运行时会产生缩进错误。这是因为 Python 期望在一个代码块中的所有行使用相同数量的空格。以下是一个示例:

>>> if age > 20:
 .... print('You are too old!')
 ...... print('Why are you here?')

我已经将空格显示出来,以便你看到差异。注意,第二行的 print 前面有六个空格,而不是四个。我们尝试运行这段代码时,Python 会用红色方块高亮显示出问题的那一行,并显示一个解释性的语法错误(SyntaxError)消息:

>>> age = 25
>>> if age > 20:
        print('You are too old!')
          print('Why are you here?')
SyntaxError: unexpected indent

Python 没有预期在第二行的 print 前面看到两个额外的空格。

注意

使用一致的空格来使代码更易读。如果你在开始编写程序时在一个代码块的开头使用了四个空格,那么在程序中的其他代码块开头也应该继续使用四个空格。确保每个代码块中的每一行都使用相同数量的空格进行缩进。

条件帮助我们比较事物

条件是一个编程表达式,用来比较事物,并告诉我们比较设定的标准是否为真(是)或假(否)。例如,age > 10 是一个条件,询问“age 变量的值是否大于 10?”另一个条件可能是 hair_color == 'mauve',即“hair_color 变量的值是否是紫色?”

我们在 Python 中使用符号——称为 运算符——来创建条件,比如 等于大于小于。表 5-1 列出了常见的运算符。

表 5-1: 条件符号

符号 定义
== 等于
!= 不等于
> 大于
< 小于
>= 大于或等于
<= 小于或等于

例如,如果你 10 岁,条件 your_age == 10 会返回 True;否则,它会返回 False。如果你 12 岁,条件 your_age > 10 会返回 True。

注意

定义等于条件时,务必使用双等号(==)。

让我们尝试一些示例。在这里,我们将年龄设置为 10,并写一个条件语句,如果年龄大于 10,就打印“你太老,不能理解我的笑话!”:

>>> age = 10
>>> if age > 10:
        print('You are too old for my jokes!')

当我们在 Python Shell 中输入这些并按下 ENTER 时会发生什么?

什么也没有。

因为 age 返回的值不大于 10,Python 不会执行 print 代码块。但是,如果我们将 age 设置为 20,消息就会被打印出来。

现在让我们将前一个示例改成使用大于或等于(>=)条件:

Image

>>> age = 10
>>> if age >= 10:
        print('You are too old for my jokes!')

你应该看到“你太老,不能理解我的笑话!”被打印到屏幕上,因为 age 的值等于 10。

接下来,让我们尝试使用等于(==)条件:

>>> age = 10
>>> if age == 10:
        print("What's brown and sticky? A stick!!")

你应该看到消息“什么又棕又黏?一根棍子!!”被打印到屏幕上。

If-Then-Else 语句

除了使用 if 语句在条件成立(True)时执行某些操作,我们还可以在条件不成立时使用 if 语句来执行操作。例如,如果你的年龄是 12,则打印一条消息,否则打印另一条消息。

这里的技巧是使用 if-then-else 语句,它的基本意思是,“如果某事为真,那么做这个;否则,做那个。”

让我们创建一个 if-then-else 语句。在 Python Shell 中输入以下内容:

>>> print('Want to hear a dirty joke?')
Want to hear a dirty joke?
>>> age = 12
>>> if age == 12:
 print('A pig fell in the mud!')
    else:
        print("Shh. It's a secret.")

A pig fell in the mud!

由于我们将 age 设置为 12,并且条件检查 age 是否等于 12,所以你应该在屏幕上看到第一个打印消息。现在尝试将 age 的值更改为 12 以外的数字,如下所示:

Image

>>> print('Want to hear a dirty joke?')
Want to hear a dirty joke?
>>> age = 8
>>> if age == 12:
        print('A pig fell in the mud!')
    else:
        print("Shh. It's a secret.")

Shh. It's a secret.

这次,你应该看到第二个打印消息。

IF 和 ELIF 语句

我们可以通过 elif 扩展 if 语句,elifelse-if 的缩写。这些语句与 if-then-else 语句不同,因为同一个语句中可以有多个 elif。例如,我们可以检查一个人的年龄是否为 10、11 或 12(等等),并根据答案让程序做不同的事情:

>>> age = 12
>>> if age == 10:
        print('What do you call an unhappy cranberry?')
        print('A blueberry!')
    elif age == 11:
        print('What did the green grape say to the blue grape?')
        print('Breathe! Breathe!')
    elif age == 12:
 print('What did 0 say to 8?')
        print('Hi guys!')
    elif age == 13:
        print("Why wasn't 10 afraid of 7?")
        print('Because rather than eating 9, 7 8 pi.')
    else:
        print('Huh?')

What did 0 say to 8?
Hi guys!

在这个例子中,第二行的 if 语句检查 age 的值是否等于 10。如果是,那么后面的 print 函数就会执行。然而,因为我们将 age 设置为 12,所以计算机会跳到 if 语句的下一个部分(第一个 elifelse if)并检查 age 是否等于 11。结果不是,所以计算机会跳到下一个 elif 检查 age 是否等于 12。它是,所以这次计算机会执行接下来的 print 调用。

当你在 IDLE 中输入这段代码时,它会自动缩进,所以确保在输入每个 print 语句后按下 BACKSPACE 键(如果你使用的是 Mac,按 DELETE 键)。这样,ifelifelse 语句将从最左侧的边缘开始。这与没有提示符(>>>)时 if 语句的位置相同。

Image

组合条件

你可以使用 andor 关键字组合条件,这样可以生成更短更简洁的代码。以下是一个使用 or 的示例:

>>> if age == 10 or age == 11 or age == 12 or age == 13:
        print('What is 13 + 49 + 84 + 155 + 97? A headache!')
    else:
        print('Huh?')

在这段代码中,如果第一行的任何条件为真(即年龄为 10、11、12 或 13),则下一行的 print 语句将会执行。

如果第一行的条件不成立(else),Python 将在屏幕上显示 "Huh?"。

为了进一步缩小这个例子,我们可以使用 and 关键字,结合大于或等于运算符 (>=) 和小于或等于运算符 (<=),如下所示:

>>> if age >= 10 and age <= 13:
        print('What is 13 + 49 + 84 + 155 + 97? A headache!')
    else:
        print('Huh?')

这里,如果 age 大于或等于 10 且小于或等于 13,下一行的 print 语句将会执行。例如,如果 age 的值是 12,那么 "What is 13 + 49 + 84 + 155 + 97? A headache!" 会被打印到屏幕上,因为 12 大于 10 且小于 13。

Image

没有值的变量—None

就像我们可以将数字、字符串和列表赋值给变量一样,我们也可以赋值为空值,或者说一个空的值。在 Python 中,空值称为 None。需要注意的是,None 的值与 0 的值不同,因为它表示的是 没有 值,而不是一个值为 0 的数字。以下是一个将变量设置为 None 的示例:

>>> myval = None
>>> print(myval)
None

将 None 赋值给变量意味着告诉 Python 该变量不再有任何值(或者说,它不再代表某个值)。这也是一种定义变量但不设置其值的方法。当你知道在程序后续部分会使用该变量时,提前定义所有变量可能会很有用。

注意

程序员通常在程序(或函数)开始时定义他们的变量,以便快速查看代码块中使用的所有变量。

你也可以在 if 语句中检查 None,像下面的例子一样:

>>> myval = None
>>> if myval is None:
        print("The variable myval doesn't have a value")

The variable myval doesn't have a value

当你只想在变量尚未计算时才计算一个值时,这会很有用。在这种情况下,检查 None 会告诉 Python 仅在变量没有值时才进行计算。

字符串与数字的区别

用户输入是指用户在键盘上输入的内容——无论是字符、按下的箭头键、回车键还是其他任何内容。在 Python 中,用户输入是一个字符串,这意味着当你在键盘上输入数字 10 时,Python 会将数字 10 保存为字符串,而不是数字。

比较数字 10 和字符串‘10’。虽然我们看到两者唯一的区别是一个被引号包围,但对于计算机来说,它们是完全不同的。

例如,我们可以将变量 age 的值与一个数字进行比较,在 if 语句中,像这样:

>>> if age == 10:
        print("What's the best way to speak to a monster?")
        print("From as far away as possible!")

如果我们先将变量 age 设置为数字 10:

>>> age = 10
>>> if age == 10:
        print("What's the best way to speak to a monster?")
        print("From as far away as possible!")

What's the best way to speak to a monster?
From as far away as possible!

如你所见,print 语句执行了。

接下来,我们将 age 设置为字符串‘10’(带引号),像这样:

>>> age = '10'
>>> if age == 10:
        print("What's the best way to speak to a monster?")
        print("From as far away as possible!")

>>>

在这里,print 语句没有执行,因为 Python 并不认为字符串是一个数字。

Image

幸运的是,Python 有一些函数可以将字符串转化为数字,也可以将数字转化为字符串。例如,你可以使用 int 函数将字符串‘10’转换为数字:

>>> age = '10'
>>> converted_age = int(age)

变量 converted_age 现在保存的是数字 10(而不是字符串)。

要将一个数字转换为字符串,可以使用 str,像这样:

>>> age = 10
>>> converted_age = str(age)

现在,converted_age 保存的是字符串‘10’而不是数字 10。

记得之前提到的 if age == 10 语句吗?当变量被设置为字符串(age = '10')时,它没有打印任何内容。如果我们先转换变量,结果就完全不同了:

>>> age = '10'
>>> converted_age = int(age)
>>> if converted_age == 10:
        print("What's the best way to speak to a monster?")
        print("From as far away as possible!")

What's the best way to speak to a monster?
From as far away as possible!

但是请注意:如果你尝试将一个带有小数点的数字(也称为浮动点数,因为小数点可以在数字中“移动”)转换成整数,你会遇到错误,因为 int 函数期望的是一个整数(没有小数点的数字):

>>> age = '10.5'
>>> converted_age = int(age)
Traceback (most recent call last):
    File "<pyshell#35>", line 1, in <module>
    converted_age = int(age)
ValueError: invalid literal for int() with base 10: '10.5'

Python 会抛出一个 ValueError 来告诉你,你尝试使用的值不合适。要解决这个问题,可以使用 float 而不是 int,因为 float 函数能够处理非整数的数字:

>>> age = '10.5'
>>> converted_age = float(age)
>>> print(converted_age)
10.5

如果你尝试将一个不包含数字的字符串转换成数字,也会遇到 ValueError:

>>> age = 'ten'
>>> converted_age = int(age)
Traceback (most recent call last):
    File "<pyshell#1>", line 1, in <module>
    converted_age = int(age)}
ValueError: invalid literal for int() with base 10: 'ten'

因为我们使用了单词“ten”而不是数字 10,Python 无法处理这个情况。

你学到的内容

在这一章中,你学习了如何使用 if 语句来创建只有在特定条件为真时才执行的代码块。你还了解了如何通过使用 elif 扩展 if 语句,从而根据不同的条件执行不同的代码块,以及如何使用 else 关键字在条件都不成立时执行代码。

你通过使用 and 和 or 关键字将条件组合起来,以检查数字是否在某个范围内,并使用 int、str 和 float 函数将字符串转换为数字。你还发现 None 可以将变量重置为其初始的空状态。

编程谜题

尝试使用 if 语句和条件来解答以下谜题。答案可以在 python-for-kids.com 下载。

#1: 你富有吗?

你认为下面的代码会做什么?尝试在不输入到 Python Shell 中的情况下猜测答案,然后检查你的结果。

>>> money = 2000
>>> if money > 1000:
        print("I'm rich!!")
    else:
        print("I'm not rich!!")
          print("But I might be later...")

#2: Twinkies!

创建一个 if 语句,检查 Twinkie 的数量(保存在变量 twinkies 中)是否小于 100 或大于 500。如果条件为真,程序应该打印出“Too few or too many”(太少或太多)。

#3: 正确的数量

创建一个 if 语句,检查 money 变量中包含的钱是否在 100 到 500 之间,或者在 1,000 到 5,000 之间。

#4: 我能打败那些忍者

创建一个 if 语句,当变量 ninjas 的值小于 50 时打印出“That's too many”(太多了);当值小于 30 时打印出“It’ll be a struggle, but I can take ’em”(会有点困难,但我能打败他们);当值小于 10 时打印出“I can fight those ninjas!”(我能打败那些忍者)。你可以尝试以下代码:

>>> ninjas = 5

第六章:循环进行中

Image

没有什么比需要一遍又一遍地做同样的事情更糟糕了。我们被告知在难以入睡时数羊是有原因的,这与羊毛哺乳动物的神奇催眠能力无关。原因是无休止的重复让人感到无聊,如果你不专注于某些有趣的事情,你的大脑很容易就会漂移进入睡眠状态。

程序员也不喜欢重复自己,除非他们也在试图入睡。幸运的是,大多数编程语言都有 for 循环,它可以自动重复像语句和代码块这样的内容。

在本章中,我们将探讨 for 循环以及 Python 提供的另一种循环类型:while 循环。

Image

使用 for 循环

若要在 Python 中打印五次 hello,你 可以 这样做:

>>> print('hello')
hello
>>> print('hello')
hello
>>> print('hello')
hello
>>> print('hello')
hello
>>> print('hello')
hello

但这相当繁琐。相反,你可以使用 for 循环来减少打字量和重复,如下所示:

➊ >>> for x in range(0, 5):
       ➋ print('hello')

  hello
  hello
  hello
  hello
  hello

range 函数 ➊ 可以创建一个从起始数字到接近结束数字的数字列表。可能这有点令人困惑,所以让我们将 range 函数与 list 函数结合起来,看看它到底是如何工作的。range 函数实际上并不创建数字列表;它返回一个 迭代器,这是一个专为循环设计的 Python 对象。不过,如果我们将 range 与 list 结合使用,就能得到一个数字列表:

>>> print(list(range(10, 20)))
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

在我们之前的示例中,for x in range(0,5): 实际上是告诉 Python 执行以下操作:

  1. 从 0 开始计数,并在达到 5 之前停止。

  2. 对于我们计数的每个数字,将其值存储在 x 变量中。

然后,Python 执行 print(’hello’) 语句。注意第二行 ➋ 开头的四个额外空格(与第一行 ➊ 相比)。IDLE 应该已经自动为你缩进了。当我们按下 ENTER 键后,Python 会打印五次 hello。

我们还可以在 print 语句中使用变量 x 来计算 hello 的数量:

>>> for x in range(0, 5):
        print(f'hello {x}')
hello 0
hello 1
hello 2
hello 3
hello 4

如果我们去掉 for 循环,代码可能会变成这样:

>>> x = 0
>>> print(f'hello {x}')
hello 0
>>> x = 1
>>> print(f'hello {x}')
hello 1
>>> x = 2
>>> print(f'hello {x}')
hello 2
>>> x = 3
>>> print(f'hello {x}')
hello 3
>>> x = 4
>>> print(f'hello {x}')
hello 4

使用 for 循环使我们免去了写八行额外代码!避免重复做事是最佳实践,因此 for 循环在程序员中非常流行。

在制作 for 循环时,你不必坚持使用 range 函数。你还可以使用你已经创建的列表,比如来自第三章的购物清单,如下所示:

>>> wizard_list = ['spider legs', 'toe of frog', 'snail tongue', 
                   'bat wing', 'slug butter', 'bear burp']
>>> for ingredient in wizard_list:
        print(ingredient)
spider legs
toe of frog
snail tongue
bat wing
slug butter
bear burp

这段代码告诉 Python:“对于 wizard_list 中的每一项,将其值存储在 i 变量中,然后打印该变量的内容。”如果去掉 for 循环,我们将需要做如下操作:

Image

>>> wizard_list = ['spider legs', 'toe of frog', 'snail tongue', 
                   'bat wing', 'slug butter', 'bear burp']
>>> print(wizard_list[0])
spider legs
>>> print(wizard_list[1])
toe of frog
>>> print(wizard_list[2])
snail tongue
>>> print(wizard_list[3])
bat wing
>>> print(wizard_list[4])
slug butter
>>> print(wizard_list[5])
bear burp

再一次,for 循环为我们节省了大量的打字工作。

让我们再创建一个循环。在 Python Shell 中输入以下代码;它应该会自动为你缩进代码:

>>> hugehairypants = ['huge', 'hairy', 'pants']
>>> for i in hugehairypants:
        print(i)
        print(i)

huge
huge
hairy
hairy
pants
pants

在第一行,我们创建了一个包含 ‘huge’、‘hairy’ 和 ‘pants’ 的列表。在接下来的一行中,我们遍历列表中的项目,并将每个项目赋值给变量 i。然后,在接下来的两行代码中,我们打印变量的内容两次。按下 ENTER 键进入下一行空白行,告诉 Python 结束代码块。接着,它会执行代码并打印列表中的每个元素两次。

Image

记住,如果你输入了错误的空格数量,就会收到错误信息。如果你在第四行输入了额外的空格,Python 会显示缩进错误:

>>> hugehairypants = ['huge', 'hairy', 'pants']
>>> for i in hugehairypants:
        print(i)
         print(i)

IndentationError: unexpected indent

如你在第五章中学到的,Python 要求代码块中的空格数量保持一致。无论你插入多少空格,只要每一行使用相同的空格数就可以(这也使得代码更易于人类阅读)。

这是一个更复杂的例子,展示了一个包含两个代码块的 for 循环:

>>> hugehairypants = ['huge', 'hairy', 'pants']
>>> for i in hugehairypants:
        print(i)
        for j in hugehairypants:
            print(j)

那么在这段代码中,哪些是代码块呢?第一个代码块是第一个 for 循环:

hugehairypants = ['huge', 'hairy', 'pants']
for i in hugehairypants:
    print(i)                  #
    for j in hugehairypants:  # These lines are the FIRST block.
        print(j)              #

下一个代码块是第二个 for 循环中的单一 print 行:

hugehairypants = ['huge', 'hairy', 'pants']
for i in hugehairypants:
    print(i)
    for j in hugehairypants:
       print(j)              # This line is also the SECOND block.

你能猜到这段代码会做什么吗?

在创建了一个名为 hugehairypants 的列表后,我们可以从接下来的两行代码中看出,Python 会遍历列表中的项目并打印出每一个。然而,在 for j in hugehairypants 这一行时,它会再次遍历列表,这次将值赋给变量 j,然后再次打印每个项目。这最后两行代码依然属于第一个 for 循环,这意味着它们会随着 for 循环遍历列表而为每个项目执行。

当这段代码运行时,我们应该看到 huge 后跟 huge,再后面是 hairy,pants,接着是 hairy 后跟 huge,hairy,pants,如此循环。

将代码输入 Python Shell,亲自试试看:

   >>> hugehairypants = ['huge', 'hairy', 'pants']
   >>> for i in hugehairypants:
           print(i)
           for j in hugehairypants:
               print(j)

→ huge
   huge
   hairy
   pants
→ hairy
   huge
   hairy
   pants
→ pants
   huge
   hairy
   pants

Python 进入第一个循环并打印列表中的一个项目。接着,它进入第二个循环并打印出列表中的所有项目。之后,它用 print(i) 打印出列表中的第二个项目,再用 print(j) 打印出完整的列表一次。最后,它再次用 print(i) 打印出列表中的第三个项目,再次通过内层循环打印完整列表。在输出中,标有 的行是由 print(i) 打印的,未标记的行是由 print(j) 语句打印的。

让我们尝试做一些比打印愚蠢单词更实际的事情。还记得我们在第二章中提出的计算公式吗?它用来算出如果你使用祖父的复制机,年末你会有多少金币?它长这样:

>>> found_coins + magic_coins * 365 - stolen_coins * 52

这表示 20 个找到的硬币加上 10 个魔法硬币,再乘以一年中的 365 天,减去每周被渡鸦偷走的 3 个硬币。

让我们看看你的金币堆每周是如何增长的。我们可以通过 for 循环做到这一点,但首先我们需要修改 magic_coins 变量的值,使其代表每周的金币总数。我们每天获得 10 枚魔法金币,一周有 7 天,所以 magic_coins 应该是 70:

图片

>>> found_coins = 20
>>> magic_coins = 70
>>> stolen_coins = 3

我们可以通过创建另一个变量,称为 coins,并使用 for 循环,看到我们的财富每周增加:

   >>> found_coins = 20
   >>> magic_coins = 70
   >>> stolen_coins = 3
➊ >>> coins = found_coins
   >>> for week in range(1, 53):
           coins = coins + magic_coins - stolen_coins
           print(f'Week {week} = {coins}')

coins 变量加载了 found_coins 变量的值 ➊;这是我们的起始值。接下来的代码设置了 for 循环,它将执行代码块中的命令。每次循环时,week 变量都会加载 1 到 52 范围内的下一个数字。

包含 coins = coins + magic_coins - stolen_coins 的这一行稍微复杂一些。每周我们都想增加我们魔法创造的金币数量,并减去乌鸦偷走的金币数量。可以把 coins 变量想象成一个宝箱。每周,新的金币都会被堆进宝箱。所以这一行是在告诉 Python:“用当前金币的数量,加上这周我创造的金币数量,来替换 coins 的内容。”等号(=)是一个指令,意思是:“先计算右边的内容,然后把结果保存到左边的变量中。”

print 语句将周数和(到目前为止的)金币总数打印到屏幕上。(请考虑重新阅读《嵌入字符串中的值》一节,见 第 29 页。)如果你运行这个程序,你会看到类似于 图 6-1 的内容。

图片

图 6-1:运行循环

既然我们在谈论循环...

for 循环并不是你在 Python 中可以创建的唯一类型的循环。还有 while 循环。while 循环不同于 for 循环,它没有固定的长度。如果你事先不知道循环何时停止,就可以使用 while 循环。

想象一段有 20 个台阶的楼梯。楼梯在室内,你知道你可以轻松爬完这 20 个台阶。for 循环就像这样:

>>> for step in range(0, 20):
        print(step)

现在想象一下有一段通往山顶的楼梯。山很高,你可能在到达山顶之前就筋疲力尽了,或者天气变坏,迫使你停下来。这就像一个 while 循环:

step = 0
while step < 10000:
    print(step)
    if tired == True:
        break
 elif badweather == True:
        break
    else:
        step = step + 1

如果你尝试运行这段代码,你会遇到一个错误,因为我们没有创建 tired 和 badweather 变量。虽然这里的代码不够完整,无法运行一个完整的程序,但它演示了一个简单的 while 循环。

图片

我们首先通过 step = 0 创建了 step 变量。接下来,我们创建了一个 while 循环,检查 step 的值是否小于 10,000(step < 10000),这代表从山脚到山顶的步数。只要 step 小于 10,000,Python 就会执行其余的代码。

使用 print(step) 打印 step 的值,然后使用 if tired == True 条件检查变量 tired 的值是否为 True。如果是,我们使用 break 退出循环。break 关键字是一种立即跳出(或停止)循环的方式,适用于 whilefor 循环。

在这个示例中,break 使得 Python 跳出代码块并跳到步骤 = 步骤 + 1 这一行后面的任何命令。

语句 elif badweather == True 检查 badweather 是否被设置为 True;如果是,break 就会退出循环。如果 tiredbadweather 都不为真(在 else 中看到),我们使用 step = step + 1step 增加 1,循环继续。

while 循环的步骤如下:

  1. 检查条件。

  2. 执行代码块中的代码。

  3. 重复。

更常见的是,可能会创建一个带有多个条件的 while 循环,而不仅仅是一个条件,像这样:

>>> x = 45
>>> y = 80
>>> while x < 50 and y < 100:
        x = x + 1
        y = y + 1
        print(x, y)

在这里,我们创建了一个值为 45 的 x 变量和一个值为 80 的 y 变量。循环检查两个条件:x 是否小于 50,y 是否小于 100。只要两个条件都成立,接下来的代码行就会执行,分别将 1 加到这两个变量上,然后打印它们。该代码的输出如下:

46 81
47 82
48 83
49 84
50 85

你能弄明白它是如何工作的吗?

我们从 x = 45 和 y = 80 开始计数,然后每次运行循环中的代码时,递增(将 1 加到每个变量)。只要 x 小于 50 且 y 小于 100,循环将继续运行。经过五次循环后,x 的值达到 50。现在,第一个条件(x < 50)不再成立,因此 Python 停止循环。

我们还可以使用 while 循环创建一个 半永恒的循环,它可能会永远持续下去,但会在代码中的某个地方发生某些事情,导致它跳出循环。

这里是一个示例:

while True:
    lots of code here
    lots of code here
    lots of code here
    if some_value == True:
        break

这个 while 循环的条件是 True,它总是成立,因此代码块中的代码将永远运行(因此,这个循环是永恒的)。只有当变量 some_value 为真时,Python 才会跳出循环。

你学到了什么

在这一章中,我们使用了两种类型的循环来执行重复任务:for 循环和 while 循环。它们相似,但可以以不同的方式使用。我们通过将任务写在代码块中,然后将这些代码块放入循环中,告诉 Python 我们想要重复的内容。我们还使用了 break 关键字来停止循环。

编程谜题

这里有一些循环示例供你尝试。解决方案可以在 python-for-kids.com 找到。

#1: Hello 循环

你认为以下代码会做什么?猜一猜它会发生什么,然后在 Python 中运行代码看看你猜的是否正确。

>>> for x in range(0, 20):
        print(f'hello {x}')
        if x < 9:
           break

#2: 偶数

创建一个循环,打印出偶数,直到达到你的年龄(如果你的年龄是奇数,创建一个循环打印出奇数,直到达到你的年龄)。例如,它可能会打印出如下内容:

2
4
6
8
10
12
14

#3: 我的五个最爱配料

创建一个包含五种不同三明治配料的列表,如下所示:

>>> ingredients = ['snails', 'leeches', 'gorilla belly-button lint', 
                   'caterpillar eyebrows', 'centipede toes']

现在创建一个循环,打印出列表(包括数字):

1 snails
2 leeches
3 gorilla belly-button lint
4 caterpillar eyebrows
5 centipede toes

#4:你在月球上的体重

如果你现在站在月球上,你的体重将是地球体重的 16.5%。你可以通过将地球体重乘以 0.165 来计算。

如果你每年增加两磅体重,接下来的 15 年里,你的体重在每年访问月球时以及 15 年后的体重大概是多少?写一个使用for循环的程序,打印出你每年在月球上的体重。

第七章:使用函数和模块回收你的代码

Image

想想你每天扔掉多少东西:水瓶、汽水罐、薯片袋、塑料三明治包装袋、曾装胡萝卜条或苹果片的袋子、购物袋、报纸、杂志等等。现在想象一下,所有这些垃圾都被堆放在你车道的尽头,且没有分类纸张、塑料和铁罐。

你可能会尽量回收利用,这很好,因为没有人喜欢在上学的路上爬过一堆垃圾。与其坐在一堆又大又脏的垃圾堆里,回收后的玻璃瓶被熔化并转变成新的罐子和瓶子;纸张被制成再生纸;塑料被转化为更重的塑料制品。我们重新利用那些本应被丢弃的物品。

在编程世界中,回收同样很重要。你的程序可能不会在一堆垃圾下消失,但如果你从不重用任何代码,你将输入大量内容,最终手指可能会疼到无法忍受!回收还可以使你的代码更简洁、更易读。

Image

正如你将在本章中学到的,Python 提供了多种重用代码的方法。

使用函数

函数 是告诉 Python 做某事的代码块。它们是重用代码的一种方式——你可以在程序中反复使用函数。Python 提供了许多可以使用的函数,这些被称为内置函数,或内建函数(有关内建函数的更多信息,请参见 附录 B)。模块中也有可用的函数(下文会讲到),你甚至可以自己编写函数。

我们在上一章就开始学习函数,当时我们使用 range 和 list 来实现 Python 的计数:

>>> list(range(0, 5))
[0, 1, 2, 3, 4]

自己输入一串连续的数字并不太难,但列表越大,输入的内容就越多。使用函数时,你可以轻松地创建一个包含千个数字的列表。

以下是一个使用 list 和 range 函数生成数字列表的示例:

>>> list(range(0, 1000))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16..., 997, 998, 999]

当你写简单程序时,函数非常方便。一旦你开始编写长且复杂的程序——比如游戏——函数就变得不可或缺(假设你希望在这个世纪完成编写程序)。

让我们来看看如何编写我们自己的函数。

函数的组成部分

一个函数有三个部分:名称、参数和函数体。以下是一个简单函数的示例:

>>> def testfunc(myname):
        print(f'hello {myname}')

这个函数的名称是 testfunc。它有一个参数 myname,函数体是紧随 def 这一行(define的缩写)后的代码块。参数 是在函数使用期间存在的变量。

你可以通过调用函数的名称并使用括号将参数值传递进去来运行函数:

>>> testfunc('Mary')
hello Mary

函数可以接受任意数量的参数:

>>> def testfunc(fname, lname):
        print(f'Hello {fname} {lname}')

使用多个参数时,确保用逗号分隔各个值:

>>> testfunc('Mary', 'Smith')
Hello Mary Smith

我们也可以先创建变量,然后使用它们来调用函数:

>>> firstname = 'Joe'
>>> lastname = 'Robertson'
>>> testfunc(firstname, lastname)
Hello Joe Robertson

函数可以通过 return 语句返回一个值。例如,你可以写一个函数来计算你节省了多少钱:

>>> def savings(pocket_money, paper_route, spending):
        return pocket_money + paper_route - spending

这个函数有三个参数。它将前两个参数(pocket_money 和 paper_route)相加,再减去最后一个参数(spending)。结果会返回,并且可以赋值给一个变量(就像我们设置其他值给变量一样)或打印出来:

>>> print(savings(10, 10, 5))
15

我们传入 10、10 和 5 三个参数,savings 函数计算出 15 作为结果,然后返回这个值。

变量和作用域

一个在函数体内的变量在函数运行结束后不能再次使用,因为它只存在于函数内部。在编程的世界中,一个变量可以在哪些地方使用被称为它的作用域。让我们来看一个使用几个变量但没有任何参数的简单函数:

>>> def variable_test():
        first_variable = 10
        second_variable = 20
        return first_variable * second_variable

在这个例子中,我们创建了变量 _test 函数,它将 first_variable 和 second_variable 相乘并返回结果:

>>> print(variable_test())
200

如果我们使用 print 调用这个函数,我们得到 200。然而,如果我们尝试在函数的代码块外打印 first_variable(或 second_variable,实际上是一样的)的内容,我们会得到一个错误信息:

>>> print(first_variable)
Traceback (most recent call last):
  File "<pyshell#50>", line 1, in <module>
    print(first_variable)
NameError: name 'first_variable' is not defined

如果一个变量在函数外定义,它有不同的作用域。例如,让我们在创建函数之前定义一个变量,然后在函数内部尝试使用它:

➊ >>> another_variable = 100
  >>> def variable_test2():
          first_variable = 10
          second_variable = 20
       ➋ return first_variable * second_variable * another_variable

在这段代码中,尽管变量 first_variable 和 second_variable 不能在函数外使用,但变量 another_variable(它是在函数外创建的➊)可以在函数内部使用➋。

这是调用这个函数的结果:

>>> print(variable_test2())
20000

现在,假设你正在用一种经济的材料,比如回收的锡罐,来建造一艘宇宙飞船。你每周可以压扁两个罐子来打造宇宙飞船的弯曲墙壁,但你需要大约 500 个罐子才能完成机身。让我们尝试写一个函数,打印出每周压扁的总罐子数量,持续一整年。

图片

我们的函数将计算每周我们压扁了多少罐子,罐子的数量作为参数(这使得以后修改罐子数量更容易):

>>> def spaceship_building(cans):
        total_cans = 0
        for week in range(1, 53):
 total_cans = total_cans + cans
            print(f'Week {week} = {total_cans} cans')

在函数的第一行,我们创建了 total_cans 变量并将其值设置为 0。然后,我们创建一个循环来遍历一年的每周,并将每周压扁的罐子数量加起来。这段代码构成了我们函数的内容,最后两行构成了 for 循环的另一个代码块。

让我们尝试在 Python Shell 中输入这个函数,并用不同的罐子数量调用它,从 2 开始:

>>> spaceship_building(2)
Week 1 = 2 cans
Week 2 = 4 cans
Week 3 = 6 cans
Week 4 = 8 cans
Week 5 = 10 cans
Week 6 = 12 cans
Week 7 = 14 cans
Week 8 = 16 cans
...
Week 50 = 100 cans
Week 51 = 102 cans
Week 52 = 104 cans

>>> spaceship_building(10)
Week 1 = 10 cans
Week 2 = 20 cans
Week 3 = 30 cans
Week 4 = 40 cans
Week 5 = 50 cans
...
Week 48 = 480 cans
Week 49 = 490 cans
Week 50 = 500 cans
Week 51 = 510 cans
Week 52 = 520 cans

这个函数可以在每周压扁不同数量的罐子的情况下重复使用,比每次想尝试不同数量时重新输入 for 循环更高效。当我们运行 spaceship_building(10)时,我们可以看到在第 50 周时,我们会有足够的罐子来建造宇宙飞船的墙壁。

函数也可以组合成模块,这使得 Python 非常有用,而不仅仅是稍微有用。

使用模块

模块用于将函数、变量和其他内容组织成更大、更强大的程序。有些模块是内置的,有些则需要单独下载。比如有帮助编写游戏的模块(如内置的 tkinter 和非内置的 PyGame),也有用于处理图像的模块(如 Python 图像库 Pillow),还有用于绘制三维图形的模块(如 Panda3D)。

Image

模块可以用来做各种有用的事情。例如,如果你在设计一个模拟游戏,并且希望世界根据现实世界发生变化,你可以使用内置的 time 模块计算当前的日期和时间:

>>> import time

import 命令告诉 Python 我们想使用 time 模块。

然后,我们可以通过使用点符号调用此模块中可用的函数。(我们在 第四章 中使用了类似的函数来处理 turtle 模块,如 t.forward(50)。)例如,下面是我们如何调用 time 模块中的 asctime 函数:

>>> print(time.asctime())
Tue Aug 12 07:05:32 2025

asctime 函数是 time 模块的一部分,它返回当前的日期和时间作为一个字符串。

现在假设你想要求某人输入一个值,比如他们的出生日期或年龄。你可以使用 print 语句显示一条信息,并使用 sys(即 系统)模块,这个模块包含了与 Python 系统交互的工具。首先,我们导入 sys 模块:

Image

>>> import sys

sys 模块中,有一个特殊的对象叫做 stdin(即 标准输入),它提供了一个有用的函数 readlinereadline 函数用于读取键盘上输入的一行文本,直到你按下 ENTER 键。(我们将在 第八章 中查看对象的工作原理。)要测试 readline,可以在 Python Shell 中输入以下代码:

>>> import sys
>>> print(sys.stdin.readline())

如果你接着输入一些文字并按下 ENTER,这些文字会在 Python Shell 中显示出来。

回想一下我们在 第五章 中编写的代码,使用了 if 语句:

>>> if age >= 10 and age <= 13:
        print('What is 13 + 49 + 84 + 155 + 97? A headache!')
    else:
        print('Huh?')

我们现在可以让某人输入值,而不需要在 if 语句之前就创建 age 变量并赋予它一个具体的值。首先,让我们将代码改成一个函数:

>>> def silly_age_joke(age):
        if age >= 10 and age <= 13:
            print('What is 13 + 49 + 84 + 155 + 97? A headache!')
        else:
            print('Huh?')

现在,你可以通过输入函数的名称并在括号中输入数字来调用函数。这样能正常工作吗?

>>> silly_age_joke(9)
Huh?
>>> silly_age_joke(10)
What is 13 + 49 + 84 + 155 + 97? A headache!

它工作了!现在让我们让函数要求输入一个人的年龄。(你可以随时增加或修改函数的内容。)

>>> def silly_age_joke():
        print('How old are you?')
     ➊ age = int(sys.stdin.readline())
     ➋ if age >= 10 and age <= 13:
            print('What is 13 + 49 + 84 + 155 + 97? A headache!')
        else:
            print('Huh?')

你认出了函数 int ➊ 吗?它将字符串转换为数字。我们包含了这个函数,因为 sys.stdin.readline() 返回的是用户输入的字符串,但我们需要一个数字来与数字 10 和 13 ➋ 进行比较。要自己尝试一下,请调用该函数而不传递任何参数,然后在显示 “How old are you?” 时输入一个数字:

>>> silly_age_joke()
How old are you?
10
What is 13 + 49 + 84 + 155 + 97? A headache!
>>> silly_age_joke()
How old are you?
15
Huh?

第一次调用函数时,它会显示“你多大了?”然后我们输入 10,它打印出笑话。第二次我们输入 15,它打印出“哈?”

输入函数

sys.stdin.readline 并不是读取键盘输入的唯一方式。一个更简单的选择是内置的 input 函数。input 函数接受一个可选的提示参数(你想要显示的消息字符串),然后返回输入的内容,直到按下 ENTER 键。图 7-1 展示了运行这段代码时的结果:

i = input('??? ')
print(i)

Image

图 7-1:使用输入函数

让我们将 silly_age_joke 函数重写为使用输入:

>>> def silly_age_joke():
        age = int(input('How old are you?\n'))
        if age >= 10 and age <= 13:
            print('What is 13 + 49 + 84 + 155 + 97? A headache!')
        else:
            print('Huh?')

除了行数略少外,早期代码与此版本的另一个区别在于我们在字符串末尾添加了一个换行字符(∖n)('How old are you?∖n')。换行符只是将光标从当前行移动到下一行。使用 print 时这一操作会自动发生,但在输入函数中则不会。

不过,这段代码的效果和以前完全一样。

你学到了什么

在本章中,你学会了如何在 Python 中使用函数制作可重用的代码块,并使用模块提供的函数。你了解了变量的作用域如何控制它们是否可以在函数内外访问。你还学会了如何使用 def 关键字创建函数,并如何导入模块来使用其内容。

编程难题

尝试以下示例来实验创建你自己的函数。解决方案可以在 python-for-kids.com 找到。

#1: 基本月球体重函数

在 第六章 的编程难题中,我们创建了一个 for 循环来确定你在月球上的体重,时间跨度为 15 年。这个 for 循环可以很容易地转化为一个函数。尝试创建一个接受起始体重并每年增加一定体重的函数。你可以使用如下代码来调用这个新函数:

>>> moon_weight(30, 0.25)

#2: 月球体重函数与年数

拿到你刚创建的函数,并将其修改为计算不同时间段的体重变化,例如 5 年或 20 年。务必修改函数,使其接受三个参数:初始体重、每年增加的体重和年数:

>>> moon_weight(90, 0.25, 5)

#3: 月球体重程序

你可以不使用简单的函数并将值作为参数传入,而是使用 sys.stdin.readline()input() 创建一个小程序来提示输入这些值。在这种情况下,你可以直接调用没有任何参数的函数:

>>> moon_weight()

该函数将显示一条消息,询问起始体重,第二条消息询问每年增加的体重,最后一条消息询问年份。你会看到类似下面的内容:

Please enter your current Earth weight
45
Please enter the amount your weight might increase each year
0.4
Please enter the number of years
12

如果你使用 sys.stdin.readline(),记得在创建函数之前先导入 sys 模块:

>>> import sys

#4: 火星体重程序

让我们修改月球体重计算程序,改为计算在火星上的体重——这次是为你全家计算。这个函数应该要求输入每个家庭成员的体重,计算他们在火星上的体重(通过将体重乘以 0.3782),然后加总并在最后显示总重量。你可以用多种方式来编写这个代码;重要的是它最终能够显示总重量。

第八章:如何使用类和对象

Image

为什么长颈鹿像人行道一样?因为长颈鹿和人行道都是事物,在英语中被称为名词,在 Python 中被称为对象。在编程中,对象是组织代码的一种方式,并且将事物拆解成更易于操作复杂想法的模块。(我们在第四章中使用过对象,当时我们使用了乌龟模块的 Turtle 对象。)

要全面理解 Python 中对象的工作方式,我们需要思考对象的类型。我们从长颈鹿和人行道开始。

长颈鹿是一种哺乳动物,哺乳动物又是动物的一种。长颈鹿也是一种有生命的物体——它是活的。

关于人行道,没有什么好说的,它只是一个非生物体。我们可以称它为无生命体(换句话说,它是死的)。哺乳动物动物有生命无生命这些术语都是分类事物的方式。

Image

将事物拆分成类别

在 Python 中,对象由定义,类将对象分为不同的组。例如,图 8-1 中的树状图显示了长颈鹿和人行道根据我们之前的定义所属于的类。

Image

图 8-1:一些类的树状图

主要类是 Thing。在 Thing 类下,我们有 Inanimate 和 Animate。这些类进一步细分为 Inanimate 的 Sidewalk 和 Animate 的 Animal、Mammal 和 Giraffe。

我们可以使用类来组织代码片段。例如,考虑乌龟模块。Python 的乌龟模块能做的所有事情——例如前进、后退、左转和右转——都是 Turtle 类中的函数。一个对象是某个类的成员,我们可以为一个类创建任意数量的对象,这一点我们很快就会讲到。

现在,让我们从顶部开始创建与树状图中相同的类集合。我们使用 class 关键字来定义类,后面跟着一个名称。Thing 是最广泛的类,因此我们首先创建它:

>>> class Thing:
        pass

我们命名类为 Thing,并使用 pass 语句告诉 Python 我们不会提供更多信息。pass 关键字用于当我们想提供一个类或函数,但暂时不想填充具体细节时。

接下来,我们将添加其他类并建立它们之间的关系。

子类与父类

如果一个类是另一个类的组成部分,它被认为是该类的子类,而另一个类则是它的父类。类可以既是其他类的子类,也可以是其他类的父类。在我们的树状图中,一个类位于另一个类上方时,它是父类,位于下方的类则是子类。例如,Inanimate 和 Animate 都是 Thing 类的子类,这意味着 Thing 是它们的父类。

要告诉 Python 一个类是另一个类的子类,我们在新类的名称后加上父类的名称,像这样:

>>> class Inanimate(Thing):
        pass

>>> class Animate(Thing):
        pass

在这里,我们创建了一个名为 Inanimate 的类,并告诉 Python 它的父类是 Thing。接下来,我们创建了一个名为 Animate 的类,并告诉 Python 它的父类也是 Thing。

让我们创建一个名为 Sidewalk 的类,父类为 Inanimate:

>>> class Sidewalk(Inanimate):
        pass

我们还可以使用它们的父类来组织 Animal、Mammal 和 Giraffe 类:

>>> class Animal(Animate):
        pass

>>> class Mammal(Animal):
        pass

>>> class Giraffe(Mammal):
        pass

向类中添加对象

现在我们有了一堆类,但我们如何将更多信息添加到这些类中呢?假设我们有一只名叫 Reginald 的长颈鹿。我们知道他属于 Giraffe 类,但我们在编程术语中用什么来描述这只名叫 Reginald 的单独的长颈鹿呢?

我们称 Reginald 为 Giraffe 类的对象(也称为实例)。为了将 Reginald “引入” Python,我们使用这段小代码:

>>> reginald = Giraffe()

这段代码告诉 Python 创建一个 Giraffe 类的对象,并将其赋值给 reginald 变量。就像调用函数时一样,类名后面跟着括号。在本章后面,我们将看到如何创建对象并使用括号中的参数。

但是,reginald 对象做什么呢?目前,什么也不做。为了使我们的对象更有用,当我们创建类时,我们还需要定义可以与该类中的对象一起使用的函数。与其在定义类后立即使用 pass 关键字,我们可以添加函数定义。

定义类的函数

第七章介绍了函数作为重用代码的一种方式。当我们定义与类相关联的函数时,我们的做法与定义其他函数相同,只不过我们将其缩进到类定义之下。例如,这是一个与类无关的普通函数:

>>> def this_is_a_normal_function():
        print('I am a normal function')

这里有几个为类定义的函数:

>>> class ThisIsMySillyClass:
        def this_is_a_class_function():
            print('I am a class function')
        def this_is_also_a_class_function():
            print('I am also a class function. See?')

添加类特征

考虑我们在“子类和父类”一节中定义的 Animate 类的子类。我们可以为每个类添加描述它是什么以及它能做什么的特征。特征是所有该类(及其子类)成员所共有的属性。

例如,所有动物有什么共同点?首先,它们都会呼吸、移动和进食。那么哺乳动物呢?哺乳动物用乳汁喂养后代,它们也会呼吸、移动和进食。我们知道长颈鹿吃树上的高处叶子。像所有哺乳动物一样,它们也用乳汁喂养后代,呼吸、移动和进食。当我们将这些特征添加到我们的树形图时,得到的结果类似于图 8-2。

Image

图 8-2:具有特征的类

这些特征可以被视为动作或函数——即该类的对象可以做的事情。

要向类中添加函数,我们使用 def 关键字。所以 Animal 类看起来会像这样:

>>> class Animal(Animate):
        def breathe(self):
            pass
        def move(self):
            pass
        def eat_food(self):
            pass

在这段代码的第一行,我们像之前一样定义了类,但在下一行我们不再使用 pass,而是定义了一个名为 breathe 的函数,并给它提供了 self 参数。self 参数是类中一个函数调用另一个函数(包括父类中的函数)的一种方式。稍后我们会看到这个参数的使用。

Image

在下一行,pass 告诉 Python 我们不会提供更多关于 breathe 函数的信息,因为它现在什么也不做。然后我们添加了 move 和 eat_food 函数,并且对这两个函数都使用了 pass 关键字。稍后我们会重新创建类,并在函数中加入适当的代码。这是开发程序的一种常见方式。

注意

通常,程序员会创建没有任何功能的类,作为在深入细节之前确定类应执行什么的方式。

我们还可以向 Mammal 和 Giraffe 类添加函数。每个类都能够使用其父类的特征(或函数),这意味着你不需要创建一个非常复杂的类。相反,你可以将函数放在最高的父类中,只要该特征适用即可。(这使得你的类更加简洁且易于理解。)

>>> class Mammal(Animal):
        def feed_young_with_milk(self):
            pass

>>> class Giraffe(Mammal):
        def eat_leaves_from_trees(self):
            pass

在上面的代码中,Mammal 类提供了一个名为 feed _young_with_milk 的函数。Giraffe 类是 Mammal 类的子类(或子类),并提供了另一个函数:eat_leaves_from_trees。

为什么使用类和对象?

我们现在已经给类添加了函数,但如果可以仅仅写出像 breathe、move、eat_food 这样的普通函数,为什么还要使用类和对象呢?

为了回答这个问题,我们将使用我们之前创建的名为 Reginald 的长颈鹿对象,它是 Giraffe 类的一个实例,像这样:

>>> reginald = Giraffe()

因为 Reginald 是一个对象,我们可以调用(或运行)Giraffe 类及其父类提供的函数。我们通过使用点号(.)操作符和函数名称来对对象调用函数。要告诉 Reginald 移动或吃东西,我们可以像这样调用这些函数:

>>> reginald = Giraffe()
>>> reginald.move()
>>> reginald.eat_leaves_from_trees()

假设 Reginald 有一个长颈鹿朋友叫 Harriet。让我们再创建一个名为 harriet 的 Giraffe 对象:

>>> harriet = Giraffe()

因为我们正在使用对象和类,所以当我们想要运行 move 函数时,可以告诉 Python 我们在谈论哪个长颈鹿。例如,如果我们想让 Harriet 移动,但让 Reginald 保持原地,我们可以通过使用我们的 harriet 对象来调用 move 函数,像这样:

>>> harriet.move()

在这种情况下,只有 Harriet 会移动。

让我们稍微修改一下我们的类,让这一点更加明显。给每个函数添加一个 print 语句,而不是使用 pass:

>>> class Animal(Animate):
        def breathe(self):
            print('breathing')
        def move(self):
            print('moving')
        def eat_food(self):
            print('eating food')
>>> class Mammal(Animal):
        def feed_young_with_milk(self):
            print('feeding young')

>>> class Giraffe(Mammal):
        def eat_leaves_from_trees(self):
            print('eating leaves')

现在,当我们创建我们的 Reginald 和 Harriet 对象并对它们调用函数时,我们可以看到一些实际的效果发生:

>>> reginald = Giraffe()
>>> harriet = Giraffe()
>>> reginald.move()
moving
>>> harriet.eat_leaves_from_trees()
eating leaves

在前两行中,我们创建了变量 reginald 和 harriet,它们是 Giraffe 类的对象。接下来,我们调用 reginald 的 move 函数,Python 在下一行输出“moving”。同样,我们调用 harriet 的 eat_leaves_from_trees 函数,Python 输出“eating leaves”。如果这些是现实中的长颈鹿,而不是计算机中的对象,那么一只长颈鹿会走路,另一只则在吃叶子。

图片

注意

为类定义的函数实际上称为方法。两者几乎可以互换使用,唯一的区别是方法只能在类的对象上调用。另一种说法是,方法与类关联,而函数则不与类关联。由于它们几乎相同,因此在本书中我们将使用函数这个术语。

图片中的对象和类

让我们尝试采用更具图形化的方法来理解对象和类,并回到我们在第四章中玩过的海龟模块。当我们使用 turtle.Turtle()时,Python 会创建一个由海龟模块提供的 Turtle 类对象(类似于我们的 reginald 和 harriet 对象)。我们可以像创建两只长颈鹿一样,创建两个 Turtle 对象(分别命名为 Avery 和 Kate):

>>> import turtle
>>> avery = turtle.Turtle()
>>> kate = turtle.Turtle()

每个海龟对象(avery 和 kate)都是 Turtle 类的成员。

现在,物体开始展现它们的强大之处。创建了我们的 Turtle 对象后,我们可以在每个对象上调用函数,它们将独立绘制。试试下面这段代码:

>>> avery.forward(50)
>>> avery.right(90)
>>> avery.forward(20)

通过这一系列指令,我们告诉 Avery 向前移动 50 个像素,向右转 90 度,然后向前移动 20 个像素,最终面朝下。记住,海龟总是从面朝右方开始。

现在是时候移动 Kate 了。

>>> kate.left(90)
>>> kate.forward(100)

我们让 Kate 向左转 90 度,然后向前移动 100 个像素,这样她最终面朝上。到目前为止,我们已经有了一条带箭头的线,箭头朝向两个不同的方向,每个箭头的头部表示一个不同的海龟对象:Avery 指向下方,Kate 面朝上(参见图 8-3)。

图片

图 8-3:Kate 和 Avery

现在让我们再添加一只海龟 Jacob,并在不干扰 Kate 或 Avery 的情况下移动它:

>>> jacob = turtle.Turtle()
>>> jacob.left(180)
>>> jacob.forward(80)

图片

首先,我们创建一个新的海龟对象,名为 jacob;然后将它向左转 180 度并向前移动 80 个像素。我们使用三只海龟的绘图应当像图 8-4 那样。

图片

图 8-4:Kate、Avery 和 Jacob

每次我们调用 turtle.Turtle()来创建一只海龟时,我们都会添加一个新的独立对象。每个对象仍然是 Turtle 类的实例,我们可以在每个对象上使用相同的函数。但由于我们使用的是对象,我们可以独立地移动每只海龟。就像我们独立的长颈鹿对象(Reginald 和 Harriet)一样,Avery、Kate 和 Jacob 是独立的 Turtle 对象。如果我们使用与已创建对象相同的变量名来创建新对象,旧的对象不会一定消失。

对象和类的其他有用功能

类和对象使得组织函数变得更加简单。当我们想将一个程序分成更小的块来思考时,它们也非常有用。

例如,考虑一个庞大的软件应用程序,比如文字处理器或 3D 电脑游戏。对于大多数人来说,完全理解像这样的庞大程序几乎是不可能的,因为代码量非常大。但是,如果将这些巨大的程序拆分成较小的部分,每一部分都会变得容易理解——当然,前提是你了解它的编程语言!

在编写大型程序时,将程序拆分开还允许你将工作分配给其他程序员。最复杂的程序(如你的网页浏览器)是由许多人或不同团队的人在全球范围内同时在不同部分工作时写成的。

假设我们想扩展本章中创建的一些类(动物哺乳动物长颈鹿),但是我们有太多工作要做,而我们的朋友们愿意提供帮助。我们可以将写代码的工作分配给不同的人,一个人负责动物类,另一个人负责哺乳动物类,另一个人负责长颈鹿类。

Image

继承的函数

你可能会意识到,最终负责长颈鹿类的人很幸运,因为由负责动物类和哺乳动物类的人创建的任何函数,也可以被长颈鹿类使用。长颈鹿继承哺乳动物类的函数,而哺乳动物类又继承了动物类的函数。换句话说,当我们创建一个长颈鹿对象时,我们可以使用定义在长颈鹿类中的函数,也可以使用定义在哺乳动物动物类中的函数。同样,如果我们创建一个哺乳动物对象,我们可以使用哺乳动物类以及它的父类动物类中定义的函数。

再次查看动物(Animal)、哺乳动物(Mammal)和长颈鹿(Giraffe)类之间的关系。动物类是哺乳动物类的父类,哺乳动物类是长颈鹿类的父类(图 8-5)。

Image

图 8-5:类和继承的函数

即使Reginald长颈鹿类的对象,我们仍然可以调用在动物类中定义的move函数,因为在任何父类中定义的函数都可以在其子类中使用:

>>> reginald = Giraffe()
>>> reginald.move()
moving

实际上,我们在动物类和哺乳动物类中定义的所有函数都可以从我们的Reginald对象中调用,因为这些函数是继承过来的:

>>> reginald = Giraffe()
>>> reginald.breathe()
breathing
>>> reginald.eat_food()
eating food
>>> reginald.feed_young_with_milk()
feeding young

在这段代码中,我们创建了一个名为Reginald长颈鹿类对象。当我们调用每个函数时,无论该函数是定义在长颈鹿类中,还是在父类中,都将打印一条信息。

函数调用其他函数

当我们在对象上调用函数时,我们使用对象的变量名。例如,我们可以像这样在长颈鹿Reginald上调用move函数:

>>> reginald.move()

要让长颈鹿类中的某个函数调用 move 函数,我们需要使用 self 参数。self 参数是类中一个函数调用另一个函数的一种方式。例如,假设我们向长颈鹿类中添加一个名为 find_food 的函数:

>>> class Giraffe(Mammal):
        def find_food(self):
            self.move()
            print('I\'ve found food!')
            self.eat_food()

我们创建了一个将两个其他函数结合的函数,这在编程中是很常见的。通常,你会编写一个有用的函数,然后可以在另一个函数中使用它。(我们将在第十一章中做这件事,那时我们将编写更复杂的函数来创建一个游戏。)

让我们使用 self 向长颈鹿类添加一些函数:

>>> class Giraffe(Mammal):
        def find_food(self):
            self.move()
            print('I\'ve found leaves!')
            self.eat_food()
        def eat_leaves_from_trees(self):
            print('tear leaves from branches')
            self.eat_food()
        def dance_a_jig(self):
            self.move()
            self.move()
            self.move()
            self.move()

我们使用来自父类 Animal 的 eat_foodmove 函数来定义长颈鹿类的 eat_leaves_from_treesdance_a_jig,因为这些是继承来的函数。通过添加调用其他函数的函数,当我们创建这些类的对象时,我们可以调用一个同时完成多项任务的函数。看看当我们调用 dance_a_jig 函数时会发生什么:

Image

>>> reginald = Giraffe()
>>> reginald.dance_a_jig()
moving
moving
moving
moving

在这段代码中,我们的长颈鹿移动了四次(也就是说,移动的文字被打印了四次)。

如果我们调用 find_food 函数,打印出来的会有三行:

>>> reginald.find_food()
moving
I've found leaves!
eating food

初始化对象

有时在创建对象时,我们希望为以后使用设置一些值(也叫做 属性)。当我们 初始化 一个对象时,我们就是在为它的后续使用做准备。

例如,假设我们希望在创建(或初始化)长颈鹿对象时设置它们的斑点数。为此,我们创建一个 __init__ 函数(注意每边有两个下划线字符,总共四个)。__init__ 函数在对象首次创建时设置对象的属性。Python 会在我们创建新对象时自动调用这个函数。以下是如何使用它。

>>> class Giraffe(Mammal):
        def __init__(self, spots):
            self.giraffe_spots = spots

首先,我们使用 selfspots 参数定义 __init__ 函数。与我们在类中定义的其他函数一样,__init__ 函数也需要将 self 作为第一个参数。接下来,我们通过 self 参数将 spots 参数设置为一个名为 giraffe_spots 的对象变量(即它的属性)。你可以把这一行代码理解为:“将 spots 参数的值保存起来供以后使用(使用 giraffe_spots 对象变量)。”就像类中的一个函数可以使用 self 参数调用另一个函数一样,类中的变量也通过 self 来访问。

接下来,如果我们创建几个新的长颈鹿对象(分别命名为 ozwald 和 gertrude),并显示它们的斑点数,你可以看到初始化函数的作用:

>>> ozwald = Giraffe(100)
>>> gertrude = Giraffe(150)
>>> print(ozwald.giraffe_spots)
100
>>> print(gertrude.giraffe_spots)
150

首先,我们使用参数值 100 创建了一个长颈鹿类的实例。这样会调用 __init__ 函数,并将 100 作为斑点参数的值。接着,我们创建了另一个斑点数为 150 的长颈鹿类实例。最后,我们打印每个长颈鹿对象的 giraffe_spots 变量,结果是 100 和 150。它成功了!

请记住,当我们创建一个类的对象时,比如在这个例子中的 ozwald,我们可以使用点操作符和我们想要使用的变量或函数的名称来引用它们(例如,ozwald.giraffe_spots)。但是当我们在类内创建函数时,我们通过使用 self 参数来引用这些相同的变量(以及其他函数)(例如 self.giraffe_spots)。

你学到了什么

在这一章中,我们使用类来创建事物的类别,并创建了这些类的对象(或实例)。你学到了类的子类如何继承父类的函数,而且即使两个对象属于同一个类,它们也不一定是克隆。例如,两个长颈鹿对象可以有自己独特的斑点数。

你学会了如何在对象上调用(或运行)函数,以及对象变量如何用来保存这些对象中的值。最后,我们在函数中使用了 self 参数来引用其他函数和变量。这些概念是 Python 的基础,你将在本书的其余部分多次看到它们。

编程难题

尝试以下例子,实验一下创建你自己的函数。解决方案可以在python-for-kids.com找到。

#1:长颈鹿舞蹈

为长颈鹿类添加函数来移动长颈鹿的左右脚向前和向后。一个让左脚向前移动的函数可能如下所示:

>>> def left_foot_forward(self):
        print('left foot forward')

然后创建一个名为 dance 的函数来教我们的长颈鹿跳舞(该函数将调用你刚才创建的四个脚步函数)。调用这个新函数的结果将是一个简单的舞蹈:

>>> harriet = Giraffe()
>>> harriet.dance()
left foot forward
left foot back
right foot forward
right foot back
left foot back
right foot back
right foot forward
left foot forward 

#2:乌龟叉形图

使用四个 Turtle 对象创建以下侧向叉形图(线条的确切长度不重要)。记得首先导入 turtle 模块!

Image

#3:两个小螺旋

使用两个 Turtle 对象创建以下两个小螺旋图形(同样,螺旋的大小不重要)。

Image

#4:四个小螺旋

让我们拿来在前面代码中创建的两个螺旋图形,并制作它们的镜像,创造出四个螺旋图形,应该看起来像以下的图像。

Image

第九章:更多的 Turtle 图形

图片

让我们回到我们在第四章中开始使用的 turtle 模块。在本章中,我们将学习 Python 海龟能做的不仅仅是绘制简单的黑色线条。你可以使用它们绘制更复杂的几何形状,创建不同的颜色,甚至给形状填充颜色。

从基础正方形开始

我们之前已经使用 turtle 模块绘制了简单的形状。让我们导入 turtle 模块并创建 Turtle 对象:

>>> import turtle
>>> t = turtle.Turtle()

我们在第四章中使用了以下代码来创建一个正方形:

>>> t.forward(50)
>>> t.left(90)
>>> t.forward(50)
>>> t.left(90)
>>> t.forward(50)
>>> t.left(90)
>>> t.forward(50)
>>> t.left(90)

在第六章中,我们学习了 for 循环。利用我们新学到的知识,我们可以使用 for 循环来简化这段代码,如下所示:

>>> t.reset()
>>> for x in range(1, 5):
        t.forward(50)
        t.left(90)

在第一行,我们告诉 Turtle 对象重置自己。接下来,我们启动一个 for 循环,范围从 1 到 4(即 range(1, 5))。在接下来的每一行中,每次循环执行时,我们会前进 50 像素并向左转 90 度。因为我们使用了 for 循环,这段代码比之前的版本短了一些——忽略重置行,我们从八行代码缩减到了三行。

图片

绘制星星

现在,通过对我们的 for 循环进行一些简单的修改,我们可以创建更有趣的东西。输入以下代码:

>>> t.reset()
>>> for x in range(1, 9):
        t.forward(100)
        t.left(225)

这段代码生成了一个八点星,如图 9-1 所示。

图片

图 9-1:八点星星

这段代码本身与我们用来绘制正方形的代码非常相似,只是有一些不同:

  • 我们不再使用 range(1, 5) 循环四次,而是使用 range(1, 9) 循环八次。

  • 我们不再前进 50 像素,而是前进 100 像素。

  • 我们不再转动 90 度,而是向左转 225 度。

让我们再进一步改进我们的星星。通过使用 175 度的角度并循环 37 次,我们可以制作一个有更多点的星星。输入以下代码:

>>> t.reset()
>>> for x in range(1, 38):
        t.forward(100)
        t.left(175)

你可以在图 9-2 中看到运行这段代码的结果。

图片

图 9-2:多点星星

现在,尝试输入这段代码来生成一个螺旋星:

>>> t.reset()
>>> for x in range(1, 20):
        t.forward(100)
        t.left(95)

通过改变转向的角度并减少循环次数,海龟最终绘制出一种完全不同风格的星星,你可以在图 9-3 中看到它。

图片

图 9-3:螺旋星

使用类似的代码,我们可以创建多种形状,从基本的正方形到螺旋星星。正如你所看到的,使用 for 循环让绘制这些形状变得更加简单。如果没有 for 循环,我们的代码将需要大量繁琐的输入。

图片

让我们尝试使用 if 语句来控制海龟的转向方式,并绘制另一个星星的变体。在这个例子中,我们希望海龟第一次转动一个角度,接下来转动另一个角度:

   t.reset()
➊ for x in range(1, 19):
    ➋ t.forward(100)
       if x % 2 == 0:
           t.left(175)
       else:
           t.left(225)

在这里,我们创建了一个循环,将运行 18 次 ➊,并告诉海龟向前移动 100 像素 ➋。我们还添加了 if 语句,它检查变量 x 是否包含偶数,使用了 取余运算符。取余运算符是代码中的 %,例如 x % 2 == 0,表示“x 除以 2 的余数为 0”。

代码 x % 2 问的是:“将变量 x 中的数字分成两等份后剩下多少?”例如,如果我们把 7 个球分成两部分,我们会得到两组各 3 个球(共计 6 个球),剩下 1 个球,如 图 9-4 所示。

Image

图 9-4: 将 7 个球分成两等份

如果我们把 13 个球分成两部分,我们会得到两组各 6 个球,剩下 1 个球(见 图 9-5)。

Image

图 9-5: 将 13 个球分成两等份

当我们检查 x 除以 2 后的余数是否为零时,实际上是在问这个数是否能被分成两部分且没有余数。这种方法是检查变量中数字是否为偶数的好方法,因为偶数总是可以被均分为两等份。

在我们代码的第五行,我们告诉海龟如果 x 中的数字是偶数(if x % 2 == 0),就向左转 175 度(t.left(175));否则(else),在最后一行,我们告诉它转 225 度(t.left(225))。

图 9-6 显示了结果。

Image

图 9-6: 九点星

如果你尝试过上一章的四个螺旋挑战,可能已经创建了四个海龟对象,并且为每个海龟复制了四次代码,每次略有不同,以便它们能够在正确的方向上绘制螺旋。使用 for 循环和 if 语句,你可以用更简洁的代码实现相同的功能。

绘制一辆车

海龟还可以更改颜色并绘制特定的形状。在这个例子中,我们将绘制一辆简单的、虽然不太酷的汽车。

首先,我们将绘制汽车的车身。在 IDLE 中,选择 文件 ▸ 新建文件,然后在窗口中输入以下代码:

t.reset()
t.color(1,0,0)
t.begin_fill()
t.forward(100)
t.left(90)
t.forward(20)
t.left(90)
t.forward(20)
t.right(90)
t.forward(20)
t.left(90)
t.forward(60)
t.left(90)
t.forward(20)
t.right(90)
t.forward(20)
t.left(90)
t.forward(20)
t.end_fill() 

接下来,我们将绘制第一个车轮:

t.color(0,0,0)
t.up()
t.forward(10)
t.down()
t.begin_fill()
t.circle(10)
t.end_fill()

最后,我们将绘制第二个车轮:

t.setheading(0)
t.up()
t.forward(90)
t.right(90)
t.forward(10)
t.setheading(0)
t.begin_fill()
t.down()
t.circle(10)
t.end_fill()

选择 文件 ▸ 另存为,并为文件命名,例如 car.py。选择 运行 ▸ 运行模块 来尝试这段代码。我们的汽车可以在 图 9-7 中看到。

Image

图 9-7: 海龟绘制汽车

你可能注意到,代码中多了一些新的海龟函数:

  • color 用于更改画笔的颜色。

  • begin_fill 和 end_fill 用于用颜色填充画布上的区域。

  • circle 绘制一个特定大小的圆形。

  • setheading 用于将海龟转向特定方向。

让我们来看看如何使用这些函数为我们的图形添加颜色。

上色

颜色函数接受三个参数。第一个指定红色的量,第二个指定绿色的量,第三个指定蓝色的量。例如,为了得到汽车的鲜红色,我们使用了 color(1,0,0),这告诉海龟使用 100%的红色。

这种红色、绿色和蓝色的颜色配方叫做RGB,它是计算机显示器上颜色的表示方式。混合这些基本颜色可以产生其他颜色,就像你混合蓝色和红色油漆会得到紫色,或者黄色和红色会得到橙色一样。红色、绿色和蓝色被称为原色,因为你无法通过混合其他颜色来得到它们。

Image

虽然我们在计算机显示器上不是使用油漆来创建颜色(我们使用的是光),但通过想象你有三罐油漆:一罐红色,一罐绿色和一罐蓝色,可能有助于理解 RGB。每罐油漆都是满的,我们将每罐满罐油漆的值设为 1(或 100%)。然后我们将所有的红色油漆和所有的绿色油漆混合在一个大桶里,得到黄色(这就是每种颜色的 1 和 1,或者 100%的每种颜色)。

现在让我们回到代码的世界。要用海龟画一个黄色圆圈,我们需要使用 100%的红色和绿色油漆,但不使用蓝色,就像这样:

   >>> t.color(1,1,0)
   >>> t.begin_fill()
➊ >>> t.circle(50)
   >>> t.end_fill()

第一行中的 1,1,0 表示 100%的红色,100%的绿色和 0%的蓝色。在下一行,我们告诉海龟用这个 RGB 颜色填充它绘制的形状,然后我们告诉它画一个圆圈 ➊。最后一行告诉海龟用这个 RGB 颜色填充圆圈。

画一个填充圆圈的函数

为了更方便地实验颜色,我们可以从我们用来画填充圆圈的代码中创建一个函数:

>>> def mycircle(red, green, blue):
        t.color(red, green, blue)
        t.begin_fill()
        t.circle(50)
        t.end_fill()

我们可以通过只使用绿色油漆来画一个明亮的绿色圆圈,就像这样:

>>> mycircle(0, 1, 0)

或者我们可以通过只使用一半的绿色油漆(0.5)来画一个较暗的绿色圆圈:

>>> mycircle(0, 0.5, 0)

为了在你的屏幕上玩弄 RGB 颜色,试着先画一个完全红色的圆圈,然后是半红色(1 和 0.5),再画一个完全蓝色的圆圈,然后是半蓝色的圆圈,就像这样:

>>> mycircle(1, 0, 0)
>>> mycircle(0.5, 0, 0)
>>> mycircle(0, 0, 1)
>>> mycircle(0, 0, 0.5)

注意

如果你的画布开始变得混乱,可以使用 t.reset()来删除旧的绘图。还记得你可以通过使用 t.up()抬起笔和 t.down()把笔放下来来移动海龟而不画线。

红色、绿色和蓝色的各种组合将产生多种多样的颜色,比如金色:

>>> mycircle(0.9, 0.75, 0)

或者浅粉色:

>>> mycircle(1, 0.7, 0.75)

这里有两种不同的橙色:

>>> mycircle(1, 0.5, 0)
>>> mycircle(0.9, 0.5, 0.15)

尝试自己混合一些颜色!

创建纯黑色和白色

晚上关掉所有灯会发生什么?一切都变黑了。在计算机上,颜色也会发生同样的事情。没有光就没有颜色,所以一个所有原色都为 0 的圆圈会变成黑色:

Image

>>> mycircle(0, 0, 0)

图 9-8 展示了结果。

Image

图 9-8:黑色圆圈

如果你使用 100%的三种颜色,你会得到白色。输入以下代码可以擦除你的黑色圆圈:

>>> mycircle(1, 1, 1)

画一个正方形的函数

现在我们将尝试进行更多的形状实验。让我们使用本章开头的画方形函数,并将方形的大小作为参数传递:

>>> def mysquare(size):
        for x in range(1, 5):
            t.forward(size)
            t.left(90)

通过调用该函数并传入 50 的大小来测试它,如下所示:

>>> mysquare(50)

这会产生图 9-9 中的小方形。

Image

图 9-9:海龟画一个小方形

现在让我们尝试使用不同的大小来运行我们的函数。以下代码创建了五个连续的方形,大小分别为 25、50、75、100 和 125:

>>> t.reset()
>>> mysquare(25)
>>> mysquare(50)
>>> mysquare(75)
>>> mysquare(100)
>>> mysquare(125)

这些方形应该像图 9-10 一样。

Image

图 9-10:海龟画多个方形

绘制填充的方形

要绘制一个填充的方形,我们需要重置画布,开始填充,然后再次调用方形函数,代码如下:

>>> t.reset()
>>> t.begin_fill()
>>> mysquare(50)

在你结束填充之前,你应该看到一个空方形:

>>> t.end_fill()

你的方形应该像图 9-11 一样。

Image

图 9-11:海龟画一个填充的方形

让我们修改这个函数,以便既能绘制填充的方形,也能绘制不填充的方形。为此,我们需要另一个参数,并稍微复杂一点的代码:

>>> def mysquare(size, filled):
        if filled == True:
            t.begin_fill()
        for x in range(1, 5):
            t.forward(size)
            t.left(90)
        if filled == True:
            t.end_fill()

在第一行中,我们将函数的定义修改为接受两个参数:大小和填充。接下来,我们检查 filled 的值是否为 True,使用 if filled == True。如果是,我们调用 begin_fill,告诉海龟填充我们绘制的形状。然后我们循环四次(for x in range(1, 5)),绘制方形的四个边(前进并向左转),之后再次检查 filled 是否为 True。如果是,我们用 t.end_fill 停止填充,海龟就会用颜色填充方形。

现在我们可以通过这一行代码绘制一个填充的方形:

>>> mysquare(50, True)

或者我们可以用这一行代码创建一个不填充的方形:

>>> mysquare(150, False)

在这两次调用 mysquare 函数之后,我们得到图 9-12,它有点像一个方形的眼睛。

Image

图 9-12:海龟画一个方形眼睛

但在这里停下没有意义,你可以绘制各种形状并用颜色填充它们。

绘制填充的星形

在我们的最终示例中,我们将为之前绘制的星形添加颜色。原始代码如下:

for x in range(1, 19):
    t.forward(100)
    if x % 2 == 0:
        t.left(175)
    else:
        t.left(225)

现在我们将创建一个 mystar 函数。我们将使用 mysquare 函数中的 if 语句并添加大小参数:

>>> def mystar(size, filled):
        if filled == True:
            t.begin_fill()
        for x in range(1, 19):
            t.forward(size)
            if x % 2 == 0:
                t.left(175)
            else:
                t.left(225)
        if filled == True:
            t.end_fill()

在这个函数的前两行,我们检查 filled 是否为 True;如果是,我们开始填充。在最后两行,我们再次检查,如果 filled 为 True,则停止填充。此外,像 mysquare 函数一样,我们将星形的大小作为参数 size 传递,并在调用 t.forward 时使用该值。

现在让我们将颜色设置为金色(90%的红色,75%的绿色,0%的蓝色),然后再次调用该函数:

>>> t.color(0.9, 0.75, 0)
>>> mystar(120, True)

海龟将在图 9-13 中绘制填充的星形。

Image

图 9-13:绘制金色的星形

要为星形添加轮廓,请将颜色改为黑色,并在不填充的情况下重新绘制星形:

>>> t.color(0,0,0)
>>> mystar(120, False)

现在,星星是金色的,并带有黑色轮廓,就像图 9-14 一样。

图片

图 9-14:绘制一个金色星星并带有轮廓

你学到了什么

在本章中,你学会了如何使用海龟模块绘制几何形状,使用 for 循环和 if 语句来控制海龟在屏幕上做什么。我们改变了海龟线条的颜色,并填充了它绘制的形状。我们还将绘图代码重用到一些函数中,使得通过一次函数调用绘制不同颜色的形状变得更简单。

编程难题

在接下来的实验中,你将使用海龟绘制你自己的形状。像往常一样,解决方案可以在python-for-kids.com找到。

#1:绘制一个八边形

在本章中,我们画了星星、方形和矩形。那么,如何创建一个绘制八边形的函数呢?(提示:试着让海龟转动 45 度。)你的形状应该与图 9-15 相似。

图片

图 9-15:绘制一个八边形

#2:绘制一个填充的八边形

现在你已经有了绘制八边形的函数,修改它来绘制一个填充的八边形。尝试像我们为星星所做的那样绘制一个有轮廓的八边形。它应该看起来与图 9-16 相似。

图片

图 9-16:绘制一个填充的八边形

#3:另一个绘制星星的函数

创建一个函数来绘制星星,该函数需要两个参数:大小和点数。函数的开始应该像这样:

def draw_star(size, points):

#4:重新审视四个螺旋

取出你在上一章中为编程难题#4 创建的代码(用来创建四个螺旋)并再次绘制相同的螺旋——这次尝试使用 for 循环和 if 语句来简化代码。

第十章:使用 tkinter 创建更好的图形

Image

使用 turtle 来绘图的问题是……海龟……真的……很慢。即使海龟以最快速度移动,它仍然不算快。虽然这对海龟来说不是问题,但对计算机图形来说却是个问题。

计算机图形通常需要快速移动。如果你在游戏机或电脑上玩游戏,想一想屏幕上显示的图形。二维(2D)图形是平面的:角色通常只上下或左右移动,像许多任天堂和手机游戏那样。在伪三维(3D)游戏中——即接近三维的游戏——图像看起来更加真实,但角色通常只在一个平面上移动(这也被称为等距图形)。最后,我们有了 3D 游戏,它们的图形试图模拟现实。无论游戏使用 2D、伪 3D 还是 3D 图形,它们都有一个共同点:需要在计算机屏幕上非常快速地绘图。

Image

如果你从未尝试过创建自己的动画,尝试这个简单的项目:

  1. 拿一张空白纸,在第一页的底角画一些东西(可能是一个火柴人)。

  2. 在下一页的角落画出相同的火柴人,但稍微移动它的腿。

  3. 在下一页上,再次绘制火柴人,并稍微移动一下它的腿。

  4. 逐页进行,在每页的底角画一个修改过的火柴人。

完成后,迅速翻阅页面,你应该能看到火柴人动起来了。这是所有动画的基本方法,无论是卡通还是视频游戏。先绘制一个图像,然后在图像上进行微小的改变,再次绘制它,以创造运动的错觉。要让图像看起来像是移动,你需要非常快速地显示每个——或动画的每一部分。

Python 提供了多种创建图形的方式。除了 turtle 模块外,你还可以使用外部模块(需要单独安装),以及 tkinter 模块,这个模块你应该已经在标准的 Python 安装中拥有。tkinter 模块不仅可以用于创建完整的应用程序,比如一个简单的文字处理器,还可以用于绘制图形。在本章中,我们将探索如何使用 tkinter 创建图形。

创建一个可点击的按钮

在我们的第一个示例中,我们将使用 tkinter 模块来创建一个带按钮的基本应用程序。请输入以下代码:

>>> from tkinter import *
>>> tk = Tk()
>>> btn = Button(tk, text='click me')
>>> btn.pack()

在第一行,我们导入 tkinter 模块的内容。from *module-name* import * 语句允许我们使用模块的内容,而无需使用模块名。相比之下,当我们在之前的示例中使用 import turtle 时,我们需要包含模块名称才能访问其内容,像这样:

Image

import turtle
t = turtle.Turtle()

当我们使用 import * 时,我们不需要像在 第四章 和 第九章 中那样调用 turtle.Turtle。当你使用包含大量类和函数的模块时,这非常有用,因为它减少了你需要输入的内容:

from turtle import *
t = Turtle()

在我们的按钮示例中的下一行,我们创建了一个包含 Tk 类对象的变量 tk = Tk(),就像我们为海龟创建 Turtle 对象一样。tk 对象创建了一个基本窗口,我们可以在这个窗口中添加其他内容,如按钮、输入框或绘图画布。这是 tkinter 模块提供的主类;如果不创建 Tk 类的对象,就无法进行图形或动画操作。

在第三行,我们创建了一个按钮 btn = Button,并将 tk 变量作为第一个参数传入,同时使用 text='click me' 设置按钮显示的文字。虽然我们已经将这个按钮添加到窗口中,但直到你输入 btn.pack() 这一行代码,按钮才会显示出来,这行代码告诉按钮显示出来。如果窗口中有其他按钮或对象需要显示,它还会正确地将所有元素排列到屏幕上。结果应该类似于 图 10-1。

图片

图 10-1:一个带有单个按钮的 tkinter 应用程序

现在,"Click Me" 按钮没有什么功能。你可以整天点击它,但直到我们稍微修改代码,它才会有所动作。(记得关闭你之前创建的窗口!)

首先,我们创建一个函数来打印一些文本:

>>> def hello():
        print('hello there')

然后我们修改示例,使用这个新函数:

>>> from tkinter import *
>>> tk = Tk()
>>> btn = Button(tk, text='click me', command=hello)
>>> btn.pack()

我们只对之前版本的代码进行了轻微修改,添加了 command 参数,告诉 Python 在点击按钮时使用 hello 函数。

现在,当你点击按钮时,你将看到 "hello there" 被打印到 Python Shell 中。每次点击按钮时,这个消息都会出现。在 图 10-2 中,我点击了按钮五次。

图片

图 10-2:点击按钮

这是我们第一次在代码示例中使用命名参数,在继续绘制之前,我们先简单讨论一下它们。

使用命名参数

命名参数 就像普通参数一样,只不过我们不再使用传入函数的值的特定顺序来确定哪个值属于哪个参数(第一个值是第一个参数,第二个值是第二个参数,依此类推),而是明确给值命名,这样它们就可以按任意顺序出现。

有时候,函数可能有很多参数,我们不一定需要为每个参数提供值。使用命名参数,我们只需要为需要的参数提供值。

例如,假设我们有一个名为 person 的函数,它接受两个参数:宽度和高度:

>>> def person(width, height):
        print(f'I am {width} feet wide, {height} feet high')

通常,我们可能这样调用这个函数:

>>> person(4, 3)
I am 4 feet wide, 3 feet high

使用命名参数,我们可以调用这个函数并为每个值指定参数名称:

>>> person(height=3, width=4)
I am 4 feet wide, 3 feet high

命名参数在我们深入使用 tkinter 模块时将特别有用。

创建绘图画布

按钮是很好的工具,但在我们想要在屏幕上绘图时,它们并不特别有用。当需要绘制时,我们需要一个不同的组件:画布对象,它是 Canvas 类的一个对象(由 tkinter 模块提供)。

创建画布时,我们需要将画布的宽度和高度(以像素为单位)传递给 Python。否则,代码与按钮代码相似。例如:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=500, height=500)
>>> canvas.pack()

与按钮示例一样,当你输入 tk = Tk() 时,会弹出一个窗口。在最后一行,我们用 canvas.pack() 来打包画布,这会应用画布的大小变化(宽度为 500 像素,高度为 500 像素,如代码第三行所指定)。

同样像按钮示例一样,pack 函数告诉画布在窗口中以正确的位置显示自己。如果不调用 pack,画布将无法正确显示。

Image

绘制线条

要在画布上绘制一条线,我们使用像素坐标。坐标 决定了像素在表面上的位置。在 tkinter 画布中,坐标描述了将像素放置到画布上的水平距离(从左到右)和垂直距离(从上到下)。

例如,因为我们的画布宽度是 500 像素,高度是 500 像素,所以屏幕右下角的坐标是 (500, 500)。要绘制 图 10-3 中显示的那条线,我们将使用起始坐标 (0, 0) 和结束坐标 (500, 500)。

Image

图 10-3:使用 tkinter 绘制对角线

我们通过使用 create_line 函数来指定坐标,如下所示:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=500, height=500)
>>> canvas.pack()
>>> canvas.create_line(0, 0, 500, 500)
1

create_line 函数返回 1,这是一个标识符;我们稍后会详细了解。如果我们使用 turtle 模块做同样的事,我们就需要以下代码:

>>> import turtle
>>> turtle.setup(width=500, height=500)
>>> t = turtle.Turtle()
>>> t.up()
>>> t.goto(-250, 250)
>>> t.down()
>>> t.goto(500, -500) 

在这段代码中,画布宽 500 像素,高 500 像素,所以 turtle 出现的位置是 250, 250(画布的中间)。如果我们使用 t.goto(-250, 250),我们将向左移动 250 像素并向上移动 250 像素到屏幕的左上角。当我们调用 t.goto(500, -500) 时,我们则向右移动 500 像素并向下移动 500 像素到右下角。

所以我们可以看到 tkinter 代码已经有所改进。它稍微简短且更简单了。现在让我们来看看 canvas 对象上可以使用的一些函数,这些函数能帮助我们绘制更有趣的图形。

绘制框

使用 turtle 模块,我们通过前进、转弯、前进、再转弯等方式绘制了一个框。最终,我们通过改变前进的距离,能够画出矩形或正方形框。

tkinter 模块使得绘制正方形或矩形变得更加容易。你只需要知道角落的坐标。试试以下示例(你现在可以关闭其他窗口):

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_rectangle(10, 10, 50, 50)

在这段代码中,我们使用 tkinter 创建一个宽 400 像素、高 400 像素的画布,然后在窗口的左上角绘制一个正方形,像图 10-4 那样。

Image

图 10-4:绘制一个框

我们在代码的最后一行传递给 canvas.create_rectangle 的参数是正方形的左上角和右下角的坐标。我们提供这些坐标作为距离画布左边和顶部的距离。在这种情况下,第一个坐标(左上角)是距离左边 10 像素,距离顶部 10 像素——这就是前两个数字:10,10。正方形的右下角距离左边 50 像素,距离顶部 50 像素——这就是第二组数字:50,50。

我们将这两个坐标点分别称为x1y1x2y2。要绘制一个矩形,我们可以增加第二个角点距离画布边缘的距离(增大x2参数的值),像这样:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_rectangle(10, 10, 300, 50)

在这个例子中,矩形的左上角坐标(它在屏幕上的位置)是(10,10),右下角坐标是(300,50)。结果是一个矩形,它的高度和原始正方形一样(40 像素),但宽度大得多。

Image

图 10-5:一个宽的矩形

我们还可以通过增加第二个角点距离画布顶部的距离(增大y2参数的值)来绘制一个矩形,像这样:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_rectangle(10, 10, 50, 300)

在调用 create_rectangle 函数时,我们告诉 tkinter:

  • 在画布上横向移动 10 像素(从左上角开始)。

  • 在画布上向下移动 10 像素。这是矩形的起始角。

  • 将矩形绘制到 50 像素处。

  • 绘制到底部 300 像素处。

最终结果应该类似于图 10-6。

Image

图 10-6:一个高的矩形

绘制大量矩形

让我们尝试通过导入 random 模块并创建一个函数,使用一个随机数来确定矩形左上角和右下角的坐标,从而填充画布,绘制不同大小的矩形。

我们将使用 random 模块提供的 randrange 函数。当我们给这个函数一个数字时,它会返回一个介于 0 和我们给定数字之间的随机整数。例如,调用 randrange(10)会返回一个 0 到 9 之间的数字,调用 randrange(100)会返回一个 0 到 99 之间的数字,依此类推。

要在函数中使用 randrange,首先通过选择文件 ▸ 新建文件来创建一个新窗口,并输入以下代码:

from tkinter import *
import random
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()

def random_rectangle(width, height):
    x1 = random.randrange(width)
    y1 = random.randrange(height)
 x2 = x1 + random.randrange(width)
    y2 = y1 + random.randrange(height)
    canvas.create_rectangle(x1, y1, x2, y2) 

我们首先将(random_rectangle)函数定义为接受两个参数:宽度和高度。接着,我们通过使用 randrange 函数来创建矩形的左上角坐标变量,传入宽度和高度作为参数,分别为 x1 = random.randrange(width)和 y1 = random.randrange(height)。在函数的第二行,我们的意思是:“创建一个名为 x1 的变量,并将其值设为 0 到参数宽度之间的随机数。”

接下来的两行代码为矩形的右下角创建变量,考虑到左上角的坐标(x1 或 y1),并为这些值添加一个随机数。函数的第三行实际上是在说:“通过将一个随机数加到我们已经计算的 x1 值上,创建变量 x2。”

最后,使用 canvas.create_rectangle 时,我们利用 x1、y1、x2 和 y2 这些变量在画布上绘制矩形。

为了尝试我们的 random_rectangle 函数,我们将传入画布的宽度和高度。在你刚刚输入的函数下面添加以下代码:

random_rectangle(400, 400)

保存你输入的代码(选择文件 ▸ 保存并输入文件名,例如randomrect.py),然后选择运行 ▸ 运行模块

注意

我们的 random_rectangle 函数可以在画布的边缘或底部绘制矩形。这是因为矩形的左上角可以位于画布的任何位置(甚至是右下角),而且即使绘制超出画布的宽度或高度也不会导致错误。

一旦你看到函数正常工作,可以通过创建一个循环来多次调用 random_rectangle,填满屏幕上的矩形。让我们尝试用一个 for 循环生成 100 个随机矩形。添加以下代码,保存你的工作,然后再次运行:

for x in range(0, 100):
    random_rectangle(400, 400)

这段代码虽然有点混乱,但它算是一种现代艺术(图 10-7)。

Image

图 10-7:使用 tkinter 的现代艺术

设置颜色

让我们通过颜色为我们的图形增加一些趣味。我们将修改 random_rectangle 函数,传递一个矩形的颜色作为额外的参数(fill_color)。在新窗口中输入此代码,并保存时将文件命名为colorrect.py

from tkinter import *
import random
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()

def random_rectangle(width, height, fill_color):
    x1 = random.randrange(width)
    y1 = random.randrange(height)
    x2 = random.randrange(x1 + random.randrange(width))
 y2 = random.randrange(y1 + random.randrange(height))
    canvas.create_rectangle(x1, y1, x2, y2, fill=fill_color) 

create_rectangle 函数现在接受一个 fill_color 参数,用来指定绘制矩形时使用的颜色。

我们可以像这样将命名的颜色传递给函数,以创建一堆独特颜色的矩形。如果你尝试这个例子,考虑在输入第一行后复制并粘贴,以节省输入时间。方法是选择要复制的文本,按 CTRL-C 复制,点击一个空白行,然后按 CTRL-V 粘贴。将此代码添加到colorrect.py文件中,紧接着在函数下面:

Image

random_rectangle(400, 400, 'green')
random_rectangle(400, 400, 'red')
random_rectangle(400, 400, 'blue')
random_rectangle(400, 400, 'orange')
random_rectangle(400, 400, 'yellow')
random_rectangle(400, 400, 'pink')
random_rectangle(400, 400, 'purple')
random_rectangle(400, 400, 'violet')
random_rectangle(400, 400, 'magenta')
random_rectangle(400, 400, 'cyan')

这些命名的颜色中的许多将显示你预期的颜色,但其他颜色可能会产生错误信息(这取决于你使用的是 Windows、macOS 还是 Linux)。但是,如果是一个自定义颜色,且与命名颜色不完全相同呢?回想一下在第九章,我们通过使用红色、绿色和蓝色的百分比设置了海龟笔的颜色。使用 tkinter 设置颜色组合中每种原色的量稍微复杂一点,但我们将一步步解决这个问题。

在使用 turtle 模块时,我们通过使用 90%的红色、75%的绿色和没有蓝色来创建金色。在 tkinter 中,我们可以通过以下代码创建相同的金色:

random_rectangle(400, 400, '#e5d800')

值 ffd800 前面的井号(#)告诉 Python 我们提供的是一个十六进制数字。十六进制是一种在计算机编程中常用的数字表示方式,它使用 16 为基数(0 到 9,然后是 A 到 F),而十进制的基数是 10(0 到 9)。如果你没有学过数学中的进制知识,只需要知道你可以通过字符串中的格式占位符将普通的十进制数转换为十六进制:{:x}(参见第 29 页的“在字符串中嵌入值”)。例如,要将十进制数 15 转换为十六进制,你可以这样做:

>>> print(f'{15:x}')
f

这是一个 f-string,带有一个特殊的格式修饰符(即:x),告诉 Python 将数字转换为十六进制。

为了确保我们的数字至少有两位,我们可以稍微修改格式占位符,改为:

>>> print(f'{15:02x}')
0f

这次我们使用了一个稍微不同的格式修饰符(02x),它表示我们想要十六进制转换,但保留两位数字(对于任何缺少的数字,使用 0)。

tkinter 模块提供了一种获取十六进制颜色值的简便方法。尝试在 IDLE 中运行以下代码:

from tkinter import *
from tkinter import colorchooser
tk = Tk()
tk.update()
print(colorchooser.askcolor())

这段代码显示了一个颜色选择器,如图 10-8 所示。请注意,你必须显式导入 colorchooser 模块,因为在使用from tkinter import *时,它不会自动提供。

Image

图 10-8:tkinter 颜色选择器(在你的操作系统上可能看起来不同)

当你选择一个颜色并点击确定时,一个元组将会显示出来。这个元组包含另一个包含三个数字和一个字符串的元组:

>>> print(colorchooser.askcolor())
((157, 163, 164), '#9da3a4')

这三个数字代表红色、绿色和蓝色的量。在 tkinter 中,颜色组合中每种原色的使用量由 0 到 255 之间的数字表示(这与 turtle 模块中为每种原色使用百分比不同)。元组中的字符串包含这三个数字的十六进制版本。

你可以直接复制并粘贴字符串值来使用它,或者将元组存储为变量并使用十六进制值的索引位置。

让我们使用random_rectangle函数来看看这个是如何工作的,方法是用以下代码替换colorrect.py底部所有的random_rectangle调用:

from tkinter import colorchooser
c = colorchooser.askcolor()
random_rectangle(400, 400, c[1])

你可以在 图 10-9 中看到结果。

Image

图 10-9:绘制一个紫色矩形

绘制弧形

弧形是圆周或曲线的一部分。要使用 tkinter 绘制弧形,你需要在一个矩形内绘制它,使用 create_arc 函数,代码如下:

canvas.create_arc(10, 10, 200, 100, extent=180, style=ARC)

Image

图 10-10:绘制弧形

注意

如果你已经关闭了所有 tkinter 窗口或重启了 IDLE,请确保重新导入 tkinter,并使用以下代码重新创建画布:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_arc(10, 10, 200, 100, extent=180, style=ARC)

这段代码将包含弧形的矩形的左上角放置在坐标 (10, 10) 处,即向右 10 像素,向下 10 像素,右下角放置在坐标 (200, 100) 处,即向右 200 像素,向下 100 像素。下一个参数 extent 用来指定弧形的角度。回想一下 第四章,度数是测量沿圆周行驶距离的一种方式。图 10-11 显示了两个弧形的示例,其中我们分别沿圆周行驶了 90 度和 270 度。

Image

图 10-11:90 度和 270 度弧形

以下代码会沿页面绘制几个不同的弧形,让你看到使用 create_arc 函数时不同角度的效果:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_arc(10, 10, 200, 80, extent=45, style=ARC)
>>> canvas.create_arc(10, 80, 200, 160, extent=90, style=ARC)
>>> canvas.create_arc(10, 160, 200, 240, extent=135, style=ARC)
>>> canvas.create_arc(10, 240, 200, 320, extent=180, style=ARC)
>>> canvas.create_arc(10, 320, 200, 400, extent=359, style=ARC)

结果如 图 10-12 所示。

Image

图 10-12:多个弧形

注意

我们在最后的圆形中使用 359 度,而不是 360 度,因为 tkinter 认为 360 度等同于 0 度,且不会绘制任何内容。

样式参数是你想要绘制的弧形类型。还有两种其他类型的弧形:弦和切片。与我们已经绘制的弧形几乎相同,只是两端通过一条直线连接在一起。切片顾名思义,就像你从披萨或馅饼中切下了一块。

绘制多边形

多边形是任何具有三条或更多边的形状。常见的规则多边形有三角形、正方形、矩形、五边形、六边形等,也有不规则多边形,它们的边缘不均匀,边数较多,形状奇特。

在使用 tkinter 绘制多边形时,你需要为每个顶点提供坐标。我们可以使用以下代码绘制一个三角形:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_polygon(10, 10, 100, 10, 100, 110, fill='', 
    outline='black')

这个示例通过从 xy 坐标 (10, 10) 开始绘制一个三角形,然后移动到 (100, 10),最后结束于 (100, 110)。我们将填充颜色设置为空(一个空字符串),因此三角形不会填充颜色,轮廓颜色设置为‘黑色’,所以它会用黑色线条绘制。它应该看起来像 图 10-13。

Image

图 10-13:绘制三角形

我们可以使用以下代码添加一个不规则的多边形:

canvas.create_polygon(200, 10, 240, 30, 120, 100, 140, 120, fill='',
outline='black')

这段代码从坐标(200, 10)开始,移动到(240, 30),然后到(120, 100),最后到(140, 120)。tkinter 模块会自动将线条连接回第一个坐标。结果如图 10-14 所示。

Image

图 10-14:不规则多边形

显示文本

除了绘制形状,你还可以使用 create_text 函数在画布上书写文本。该函数仅接受两个坐标——文本的xy位置——以及一个显示文本的命名参数。在以下代码中,我们像之前一样创建了画布,然后在坐标(150, 100)处显示一行文本。将此代码保存为text.py

from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
canvas.create_text(150, 100, text='There once was a man from Toulouse,')

create_text 函数可以接受其他有用的参数,例如文本填充颜色。在以下代码中,我们调用 create_text 函数,指定坐标(130, 120)、要显示的文本和红色填充颜色:

canvas.create_text(130, 120, text='Who rode around on a moose.', fill='red')

你还可以指定font,即显示文本时使用的字体,作为一个包含字体名称和文本大小的元组。例如,Times字体大小为 20 时,元组为('Times', 20)。在以下代码中,我们使用Times字体(大小为 15)、Helvetica字体(大小为 20)以及Courier字体(大小为 22 和 30)显示文本:

Image

canvas.create_text(150, 150, text='He said, "It\'s my curse,', font=('Times', 15))
canvas.create_text(200, 200, text='But it could be worse,', font=('Helvetica', 20))
canvas.create_text(220, 250, text='My cousin rides round', font=('Courier', 22))
canvas.create_text(220, 300, text='on a goose."', font=('Courier', 30))

图 10-15 显示了使用三种指定字体和五种不同大小后的结果。

Image

图 10-15:使用 tkinter 绘制文本

显示图像

要在画布上显示图像,可以先加载图像,然后使用 create_image 函数在画布对象上显示该图像。你加载的任何图像都必须位于 Python 可以访问的文件夹(或目录)中。

放置图像的最佳位置是你的主文件夹。在 Windows 中,这是c:***Users***;在 macOS 中,是/Users/;在 Ubuntu 或 Raspberry Pi 中,是/home/<your username>。 图 10-16 显示了 Windows 上的主文件夹。

Image

图 10-16:Windows 上的“主文件夹”

注意

使用 tkinter,你只能加载 GIF 图像—扩展名为.gif的图像文件。你可以显示其他类型的图像,例如 PNG* (.png) 和 JPG (.jpg),但你需要使用其他模块,如 Pillow(Python 图像库,网址为 python-pillow.org)。如果你没有 GIF 图像可以使用,试着打开一张照片,然后将其保存为 GIF 格式。在 Windows 上,你可以通过“画图”应用程序轻松完成这项操作——当然,也有很多其他方式可以将图像转换为 GIF 格式。

我们可以如下显示名为test.gif的图像。

from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
my_image = PhotoImage(file='c:\\Users\\jason\\test.gif')
canvas.create_image(0, 0, anchor=NW, image=my_image)

在前四行中,我们像之前的示例一样设置了画布。在第五行,图像被加载到 my_image 变量中。我们用文件名 c:\ Users\jason\test.gif 创建了 PhotoImage。我们需要在 Windows 文件名中使用两个反斜杠(\),因为反斜杠在 Python 字符串中是一个特殊字符(用于表示转义字符——例如,\t 是表示制表符的转义字符,\n 是表示换行符的转义字符,这在第七章中我们也使用过),而两个反斜杠只是为了表示“不想在这里使用转义字符——我要使用单个反斜杠”。

如果你将图像保存在桌面上,你应该像这样在那个文件夹中创建 PhotoImage:

my_image = PhotoImage(file='C:\\Users\\JoeSmith\\Desktop\\test.gif')

一旦图像被加载到变量中,canvas.create_image(0, 0, anchor=NW, image=my_image) 就会使用 create_image 函数显示它。坐标 (0, 0) 是图像显示的位置,而 anchor=NW(其中 NW 代表西北)告诉函数在绘制时使用图像的左上角作为起始点;否则,它默认使用图像的中心作为起始点。最后一个命名参数 image 指向已加载图像的变量。你的屏幕应该类似于图 10-17。

Image

图 10-17:显示图像

创建基础动画

我们已经介绍了如何创建不动的静态图像。现在,我们将转向创建动画。

动画不一定是 tkinter 模块的专长,但它能处理基本的动画。例如,我们可以创建一个填充的三角形,然后通过使用以下代码使其在屏幕上移动:

>>> import time
>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=200)
>>> canvas.pack()
>>> canvas.create_polygon(10, 10, 10, 60, 50, 35)
>>> for x in range(1, 61):
        canvas.move(1, 5, 0)
        tk.update()
        time.sleep(0.05)

当你运行这段代码时,三角形将开始沿着屏幕移动,直到它的路径终点,如图 10-18 所示。

Image

图 10-18:移动三角形

和之前一样,在导入 tkinter 后,我们使用前三行代码完成基本的画布显示设置。我们通过调用 canvas.create_polygon(10, 10, 10, 60, 50, 35) 函数创建三角形。

注意

当你进入这一行时,屏幕上会打印出一个数字。这是多边形的标识符。我们可以用它来在后续中引用这个形状,如下面的示例所示。

接下来,我们创建一个简单的 for 循环,从 1 计数到 61,代码为 for x in range(1, 61)。

循环中的这段代码将三角形移动到屏幕上。canvas.move 函数通过将值添加到对象的 x 和 y 坐标来移动任何绘制的对象。例如,使用 canvas.move(1, 5, 0),我们将 ID 为 1 的对象(三角形的标识符——见前述说明)向右移动 5 像素,向下移动 0 像素。为了将其移回去,我们可以使用函数调用 canvas.move(1, -5, 0)。

Image

tk.update()函数强制 tkinter 更新屏幕(重新绘制)。如果我们不使用 update,tkinter 会等到循环完成后再移动三角形,这意味着你会看到它跳到最后的位置,而不是平滑地在画布上移动。循环的最后一行,time.sleep(0.05),告诉 Python 在继续之前暂停二十分之一秒(0.05 秒)。

要让三角形在屏幕上斜着向下移动,我们可以通过调用 move(1, 5, 5)来修改这段代码。关闭画布并创建一个新文件(文件 ▸ 新建文件)来输入以下代码:

import time
from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 10, 60, 50, 35)
for x in range(0, 60):
    canvas.move(1, 5, 5)
    tk.update()
    time.sleep(0.05)

这段代码与原始代码有两个不同之处:

  • 我们将画布的高度改为 400,而不是 200,代码为 canvas = Canvas(tk, width=400, height=400)。

  • 我们通过 canvas.move(1, 5, 5)将三角形的* x y *坐标各增加了 5。

图 10-19 展示了循环结束时三角形的位置,在你保存代码并运行后。

图片

图 10-19:三角形移动到屏幕底部。

要将三角形斜着向上移回到起始位置,可以使用(-5, -5)。将以下代码添加到文件的底部:

>>> for x in range(0, 60):
        canvas.move(1, -5, -5)
        tk.update()
        time.sleep(0.05)

运行这段代码后,三角形将返回到它开始的位置。

让对象对某些事件做出反应

我们可以通过使用事件绑定让三角形对按键做出反应。事件是在程序运行时发生的事情,比如有人移动鼠标、按下键盘或关闭窗口。你可以告诉 tkinter 监视这些事件,然后做出响应。

要开始处理事件(让 Python 在事件发生时做出反应),我们首先创建一个函数。绑定的部分是我们告诉 tkinter 某个特定的函数绑定(或关联)到一个特定事件。换句话说,它将由 tkinter 自动调用来处理该事件。

例如,要让三角形在按下 ENTER 时移动,我们可以定义这个函数:

def movetriangle(event):
    canvas.move(1, 5, 0)

这个函数接受一个参数(event),tkinter 用它来将有关事件的信息发送给函数。我们通过在画布上使用 bind_all 函数来告诉 tkinter 此函数应当处理特定的事件。完整的代码现在如下——我们在 IDLE 中新建文件并保存为movingtriangle.py,然后运行它:

from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 10, 60, 50, 35)
def movetriangle(event):
    canvas.move(1, 5, 0)
canvas.bind_all('<KeyPress-Return>', movetriangle)

这个函数的第一个参数描述了我们希望 tkinter 监视的事件。在这种情况下,它被称为,即按下 ENTER 或 RETURN 键。我们告诉 tkinter 每当发生这个 KeyPress 事件时,就应该调用 movetriangle 函数。运行这段代码,点击画布,然后按下键盘上的 ENTER 键。

图片

让我们试着根据不同的按键改变三角形的移动方向,比如方向键。首先我们需要将 movetriangle 函数改为以下内容:

def movetriangle(event):
    if event.keysym == 'Up':
        canvas.move(1, 0, -3)
    elif event.keysym == 'Down':
        canvas.move(1, 0, 3)
    elif event.keysym == 'Left':
        canvas.move(1, -3, 0)
    else:
        canvas.move(1, 3, 0)

传递给 movetriangle 的事件对象包含多个变量。其中一个变量是 keysym(表示 键符号),它是一个字符串,保存了实际按下的键的值。if event.keysym == 'Up' 这一行表示,如果 keysym 变量包含字符串 "Up",我们应该调用 canvas.move,并传入参数 (1, 0, –3),正如下面的代码所示。如果 keysym 包含 "Down",如 elif event.keysym == 'Down',我们就调用带参数 (1, 0, 3) 的函数,以此类推。

记住:第一个参数是画布上绘制形状的标识编号,第二个是添加到 x(水平)坐标的值,第三个是添加到 y(垂直)坐标的值。

然后,我们告诉 tkinter,movetriangle 函数将用于处理来自四个不同键(上、下、左、右)的事件。以下是现在 movingtriangle.py 代码的样子:

from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 10, 60, 50, 35)
def movetriangle(event):
 ➊ if event.keysym == 'Up':
     ➋ canvas.move(1, 0, -3)
 ➌ elif event.keysym == 'Down':
     ➍ canvas.move(1, 0, 3)
    elif event.keysym == 'Left':
 canvas.move(1, -3, 0)
 ➎ else:
     ➏ canvas.move(1, 3, 0)
canvas.bind_all('<KeyPress-Up>', movetriangle)
canvas.bind_all('<KeyPress-Down>', movetriangle)
canvas.bind_all('<KeyPress-Left>', movetriangle)
canvas.bind_all('<KeyPress-Right>', movetriangle) 

movetriangle 函数的第一行,我们检查 keysym 变量是否包含 "Up" ➊。如果包含,我们使用 move 函数将三角形向上移动,参数为 1、0、{3 ➋。第一个参数是三角形的标识符,第二个是向右移动的量(我们不希望水平移动,因此值为 0),第三个是向下移动的量(–3 像素)。

然后我们检查 keysym 是否包含 "Down" ➌;如果是的话,我们将三角形向下移动(3 像素) ➍。最后的检查是判断值是否为 "Left";如果是的话,我们将三角形向左移动(–3 像素)。如果没有任何值匹配,最终的 else ➎ 将三角形向右移动 ➏。

现在,三角形应该会根据按下的箭头键的方向移动。

使用标识符的更多方法

每当我们使用画布的 create_function,例如 create_polygoncreate_rectangle,都会返回一个标识符。这个标识符可以与其他画布函数一起使用,就像我们之前使用 move 函数一样:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> canvas.create_polygon(10, 10, 10, 60, 50, 35)
1
>>> canvas.move(1, 5, 0)

这个例子的问题在于,create_polygon 不会总是返回 1。例如,如果你创建了其他形状,它可能返回 2、3,甚至 100(取决于你创建了多少个形状)。如果我们改变代码,将返回的值存储为一个变量,然后使用该变量(而不是仅仅引用数字 1),那么无论返回什么数字,代码都会正常工作。

>>> mytriangle = canvas.create_polygon(10, 10, 10, 60, 50, 35)
>>> canvas.move(mytriangle, 10, 0)

move 函数允许我们通过标识符在屏幕上移动对象。但其他画布函数也可以改变我们绘制的东西。例如,itemconfig 函数可以更改形状的一些参数,如填充色和轮廓色。

假设我们创建了一个红色的三角形:

>>> from tkinter import *
>>> tk = Tk()
>>> canvas = Canvas(tk, width=400, height=400)
>>> canvas.pack()
>>> mytriangle = canvas.create_polygon(10, 10, 10, 60, 50, 35,
    fill='red')

我们可以通过 itemconfig 改变三角形的颜色,并使用标识符作为第一个参数。以下代码表示“将变量 mytriangle 中标识的对象的填充颜色改为蓝色”:

>>> canvas.itemconfig(mytriangle, fill='blue')

我们还可以给三角形添加不同颜色的轮廓,同样使用标识符作为第一个参数:

>>> canvas.itemconfig(mytriangle, outline='red')

稍后我们将学习如何对图形进行其他修改,比如隐藏图形并再次显示它。当我们开始编写游戏时,改变图形的能力将非常有用。

Image

你学到了什么

在本章中,你使用了 tkinter 模块在画布上绘制简单的几何图形、显示图像并执行基本的动画。你学会了事件绑定如何使图形响应键盘按键,这对于我们接下来编写游戏时非常有用。你还学会了 tkinter 的创建函数如何返回一个标识编号,可以用来修改已绘制的图形,比如移动它们或更改它们的颜色。

编程难题

尝试以下内容,进一步探索 tkinter 模块和基础动画。访问 python-for-kids.com 下载解决方案。

#1: 填满屏幕的三角形

使用 tkinter 创建一个程序,填满屏幕的三角形。然后将代码修改为用不同颜色的(填充的)三角形来填满屏幕。

#2: 移动的三角形

修改移动三角形的代码(见第 159 页中的“创建基础动画”)使其在屏幕上向右移动,然后向下,再向左,最后回到起始位置。

#3: 移动的照片

尝试在画布上显示你的照片。确保它是一个 GIF 图像!你能让它在屏幕上移动吗?

#4: 填满屏幕的照片

拿着你在前一个谜题中使用的照片,将它缩小。

在 macOS 上,你可以使用预览来调整图像大小(选择 工具 ▸ 调整大小,输入新的宽度和高度。然后,点击 文件 ▸ 导出 保存为新文件名)。

在 Windows 上,你可以使用画图(点击 调整大小 按钮,选择水平和垂直尺寸,然后 文件 ▸ 另存为 保存为新文件名)。

在 Ubuntu 和 Raspberry Pi 上,你需要一个叫做 GIMP 的程序(如果你没有安装,请跳到第十三章中的第 203 页)——在 GIMP 中选择 图像缩放图像,然后选择 文件 ▸ 另存为 来保存为新文件名。

导入时间模块,然后使用 sleep 函数(尝试使用 time.sleep(0.5))让照片出现得更慢。

第二部分:# 弹跳!

第十一章:开始你的第一个游戏:Bounce!

Image

到目前为止,我们已经涵盖了计算机编程的基础知识。你已经学会了如何使用变量来存储信息,如何使用 if 语句进行条件判断,如何使用 for 循环来重复执行代码。你还学会了如何创建函数来重用代码,以及如何使用类和对象将代码分成更小、更易理解的块。你已经学会了如何使用 turtle 和 tkinter 模块在屏幕上绘制图形。现在,是时候用这些知识来创建你的第一个游戏了。

打击弹跳球

我们将开发一个带有弹跳球和挡板的游戏。球会在屏幕上飞来飞去,玩家需要用挡板将球弹回。如果球撞到屏幕底部,游戏就结束了。图 11-1 展示了完成游戏的预览。

Image

图 11-1: Bounce! 游戏

我们的游戏看起来可能相当简单,但代码将比我们之前编写的稍微复杂一些,因为它需要处理很多内容。例如,它需要为挡板和球进行动画处理,并检测球何时碰到挡板或墙壁。

在本章中,我们将开始通过添加游戏画布和一个弹跳球来创建游戏。在下一章中,我们将通过添加挡板来完成游戏。

创建游戏画布

要创建你的游戏,首先在 IDLE 中打开一个新文件,选择文件 新建文件。然后导入 tkinter 并创建一个画布来绘制:

   from tkinter import *
   import random
   import time
   tk = Tk()
➊ tk.title('Bounce Game')
   tk.resizable(0, 0)
   tk.wm_attributes('-topmost', 1)
   canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
   canvas.pack()
   tk.update()

这段代码与之前的例子有些不同。首先,我们通过import randomimport time导入 time 和 random 模块,稍后将在代码中使用。random 模块提供了(其中之一)用于创建随机数的函数,而 time 模块有一个有用的函数,可以让 Python 暂停当前操作一段时间。

使用tk.title('Bounce Game') ➊时,我们调用了我们通过tk = Tk()创建的 tk 对象的 title 函数,为窗口设置标题。然后,我们使用 resizable 来将窗口的大小固定。参数(0, 0)表示“窗口的大小既不能水平改变,也不能垂直改变”。接下来,我们调用wm_attributes来告诉 tkinter 将包含画布的窗口置于所有其他窗口之前(‘-topmost’)。

当我们创建一个 Canvas 对象时,与之前的例子相比,我们传入了更多的命名参数。例如,bd=0highlightthickness=0确保画布外面没有边框,这使得游戏屏幕看起来更美观。canvas.pack()这一行告诉画布根据前面给定的宽度和高度参数来调整自身大小。最后,tk.update()告诉 tkinter 为我们的游戏动画进行初始化。如果没有这一行,游戏将无法按预期工作。

在编写代码时,一定要定期保存。第一次保存时,给它取个有意义的文件名,例如paddleball.py

Image

创建 Ball 类

现在我们将创建球的类。我们将从使球能够在画布上绘制自己的代码开始。我们需要做以下几件事:

  1. 创建一个名为 Ball 的类,接受画布和我们要绘制的球的颜色作为参数。

  2. 将画布保存为一个对象变量,因为我们将在其上绘制球。

  3. 使用颜色参数的值作为填充颜色,在画布上绘制一个填充的圆形。

  4. 保存 tkinter 绘制圆形(椭圆)时返回的标识符,因为我们将用它来移动球。

  5. 将椭圆移动到画布的中央。

这段代码应该在文件的前三行后添加(在导入 time 之后):

from tkinter import *
import random
import time

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

    def draw(self):
        pass

首先,我们将类命名为 Ball。然后,我们创建一个初始化函数(如第 109 页的“初始化对象”部分所述),它接受画布和颜色作为参数。我们将对象变量 canvas 设置为同名参数的值。接着我们调用 create_oval 函数,传入五个参数:椭圆的左上角的xy坐标(10 和 10),右下角的xy坐标(25 和 25),以及椭圆的填充颜色。

create_oval 函数返回一个表示所绘制形状的标识符,我们将其存储在对象变量 id 中。我们将椭圆移动到画布的中央(坐标 245, 100)。画布知道该移动什么,因为我们使用存储的形状标识符(id)来识别它。

在 Ball 类的最后两行,我们创建了 draw 函数,使用 def draw(self),函数的主体只是 pass 关键字。目前它什么也不做,但我们很快就会为这个函数添加更多的内容。

图片

现在我们已经创建了 Ball 类,我们需要创建这个类的一个对象(记住:类描述它能做什么,但对象才是实际执行这些操作的东西)。将以下代码添加到程序底部来创建一个红色的球对象:

ball = Ball(canvas, 'red')

你可以使用运行 运行模块来运行这个程序。如果你在 IDLE 外运行,它的画布会显示一瞬间然后消失。为了防止窗口立即关闭,我们需要添加一个动画循环,这个循环被称为游戏的主循环。(IDLE 已经有一个主循环,这就是为什么在 IDLE 中运行时窗口不会消失的原因。)

主循环是程序的核心部分,通常控制程序的大部分功能。目前,我们的主循环只是告诉 tkinter 重绘屏幕。这个循环,也叫做无限循环,会一直运行下去(或者至少直到我们关闭窗口),不断地告诉 tkinter 重绘屏幕,然后通过使用 time.sleep 使程序暂停百分之一秒。我们将在程序的最后添加这段代码:

ball = Ball(canvas, 'red')

while True:
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

现在如果你运行代码,球应该会出现在画布的中央,如图 11-2 所示。

图片

图 11-2:画布中央的球

增加一些动作

现在我们已经设置好了 Ball 类,接下来是让球动起来。我们将让它移动、弹跳并改变方向。

Image

让球运动起来

要移动球,修改 draw 函数如下:

class Ball:
    def __init__(self, canvas, color):
 self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

    def draw(self):
        self.canvas.move(self.id, 0, -1)

由于 init 已经将画布参数保存为 canvas 对象变量,我们使用 self.canvas 这个变量并调用 canvas 上的 move 函数。

我们向 move 函数传递三个参数:椭圆的 id 和数字 0 和 -1。数字 0 告诉球不水平移动,而 -1 告诉球向屏幕上方移动 1 像素。

我们做出这个小改动是因为在开发过程中尝试不同的方式是有益的。试想一下,如果我们一次性编写了整个游戏代码,然后发现它无法运行,我们该从哪里开始找出问题所在?

我们还将修改程序底部的主循环。在 while 循环块中(我们的主循环),我们添加对球对象的 draw 函数的调用,如下所示:

while True:
    ball.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

如果你现在运行这段代码,球应该会向上移动并从屏幕顶部消失——这些命令更新了 _idletasks 和 update,告诉 tkinter 加快速度并绘制画布上的内容。

time.sleep 命令是对 time 模块的 sleep 函数的调用,它告诉 Python 程序暂停一百分之一秒(0.01)。这确保了我们的程序不会运行得太快,以至于在你看到球之前它就消失了。

这个循环基本上是在说:“稍微移动一下球,重新绘制屏幕上的新位置,暂停片刻,然后重新开始。”

注意

当你关闭游戏窗口时,你可能会看到 Python Shell 中写入错误信息。这是因为关闭窗口中断了 tkinter 的操作,Python 对此发出了警告。我们可以安全地忽略这些类型的错误。

你的游戏代码现在应该如下所示:

from tkinter import *
import random
import time

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

    def draw(self):
        self.canvas.move(self.id, 0, -1)

tk = Tk()
tk.title('Bounce Game')
tk.resizable(0, 0)
tk.wm_attributes('-topmost', 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
tk.update()

ball = Ball(canvas, 'red')

while True:
    ball.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

如果你运行这段代码,球将开始向上移动,并从窗口顶部飞出去。

让球弹跳起来

一个从屏幕顶部消失的球对于我们的游戏来说并没有什么用处,所以让我们让它弹跳起来。首先,我们将在 Ball 类的初始化函数中保存一些额外的对象变量,如下所示:

    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
        self.x = 0
        self.y = -1
        self.canvas_height = self.canvas.winfo_height()

我们向程序中添加了三行代码。通过 self.x = 0,我们将对象变量 x 设置为 0;通过 self.y = -1,我们将变量 y 设置为 -1。最后,我们通过调用 winfo_height 画布函数设置了对象变量 canvas_height。该函数返回画布的当前高度。

接下来,我们再次修改 draw 函数:

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[1] <= 0:
            self.y = 1 
        if pos[3] >= self.canvas_height:
            self.y = -1

我们首先通过传递 x 和 y 对象变量来修改对 canvas 的 move 函数的调用。接下来,我们通过调用 coords 画布函数创建一个名为 pos 的变量。该函数返回任何绘制在画布上的物体的当前 xy 坐标,只要你知道它的标识号码。在这个例子中,我们将对象变量 id 传递给 coords,id 包含椭圆的标识符。

coords 函数返回四个数字的坐标列表。如果我们打印调用这个函数的结果,我们会看到类似下面的输出:

print(self.canvas.coords(self.id))
[255.0, 29.0, 270.0, 44.0]

列表中的前两个数字(255.0 和 29.0)包含椭圆的左上角坐标(x1y1),第二对数字(270.0 和 44.0)是右下角的 x2y2 坐标。我们将在接下来的几行代码中使用这些值。

我们继续编写代码,检查 y1 坐标(也就是球的顶部!)是否小于或等于 0。如果是,我们将 y 对象变量设置为 1。实际上,我们是在说,如果你碰到屏幕顶部,tkinter 就会停止从垂直位置减去 1,球就会停止向上移动(这是一种简单的 碰撞检测)。

Image

接下来,我们检查 y2 坐标(也就是球的底部!)是否大于或等于 canvas_height 变量。如果是,我们将 y 对象变量重置为 -1。现在,球将停止向下移动并再次向上移动。

现在运行这段代码,球应该会在画布上上下弹跳,直到你关闭窗口。

改变球的起始方向

让球慢慢上下弹跳并不算什么游戏,所以我们来稍微增强一下,通过改变球的起始方向——即游戏开始时球的移动角度。

Image

init 函数中,修改这两行代码:

    self.x = 0
    self.y = -1

到以下代码(确保每行的开头有正确的空格数——每行前面有八个空格):

 starts = [-3, -2, -1, 1, 2, 3]
    self.x = random.choice(starts)
    self.y = 3

我们首先创建一个 starts 变量,包含六个数字的列表。然后,我们使用 random.choice 函数设置 x 变量的值,该函数从列表中返回一个随机项。通过使用这个函数,x 可以是列表中的任何一个数字,从 –3 到 3。

最后,我们将 y 改为 –3(让球开始时向上移动)。现在我们的球可以朝任意方向移动,但我们还需要做一些补充,以确保它不会从屏幕的一侧消失。将以下代码添加到 init 函数的末尾,将画布的宽度保存到一个新的 canvas_width 对象变量中:

    self.canvas_width = self.canvas.winfo_width()

我们将在绘制函数中使用这个新的对象变量,检查球是否碰到了画布的左侧或右侧:

    if pos[0] <= 0 or pos[2] >= self.canvas_width:
        self.x = self.x * -1

如果球的最左边位置小于或等于 0,或者球的最右边位置大于或等于画布的宽度,我们就会进行这个奇怪的小计算 self.x = self.x * -1。x 变量被设置为当前 x 值乘以 –1。所以如果 x 的值是 2,新值将是 –2。如果 x 的值是 –3,新值将是 3。所以当球撞到一边时,它会反弹到相反方向。我们也可以对画布的顶部和底部做类似的检查,使用画布的高度,并将 y 变量乘以 –1。现在你的绘制函数应该看起来像这样:

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.x = self.x * -1
        if pos[1] <= 0 or pos[3] >= self.canvas_height:
            self.y = self.y * -1

完整的程序应该是这样的:

from tkinter import *
import random
import time

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
        starts = [-3, -2, -1, 1, 2, 3]
        self.x = random.choice(starts)
        self.y = -3
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.x = self.x * -1
        if pos[1] <= 0 or pos[3] >= self.canvas_height:
            self.y = self.y * -1

tk = Tk()
tk.title('Bounce Game')
tk.resizable(0, 0)
tk.wm_attributes('-topmost', 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
tk.update()

ball = Ball(canvas, 'red')

while True:
    ball.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

保存并运行代码,小球应该会在屏幕上弹跳而不会消失。

你学到了什么

在这一章中,我们开始使用 tkinter 模块创建我们的第一个游戏。我们创建了一个 Ball 对象,并使它动起来在屏幕上移动。我们使用坐标来检查小球是否碰到画布的边缘,这样我们就可以让它反弹。我们还使用了 random 模块中的选择函数,以确保小球不会总是朝同一方向开始移动。在下一章中,我们将通过添加挡板来完成游戏。

编程难题

#1:改变颜色

尝试改变小球的起始颜色和画布的背景颜色——试试不同的颜色组合,看看你喜欢哪几种。

#2:闪烁的颜色

因为我们代码的底部有一个循环,所以当小球在屏幕上移动时,改变小球的颜色应该是相当简单的。我们可以在循环中添加一些代码来选择不同的颜色(可以参考我们在本章之前使用的选择函数),然后更新小球的颜色(可能通过在 Ball 类中调用一个新函数)。为了做到这一点,你需要在画布上使用 itemconfig 函数(请参见第 165 页的“更多使用标识符的方法”)。

#3:就位!

尝试修改代码,让小球从屏幕上的不同位置开始。你可以使用 random 模块使位置随机(请参见第 145 页中“绘制许多矩形”一节,了解如何使用该模块中的 randrange 函数)。但是你必须确保小球不会从离挡板太近或在挡板下方的位置开始,否则游戏就无法进行。

#4:添加挡板……?

根据我们到目前为止写的代码,你能在进入下一个章节之前弄明白如何添加挡板吗?如果你回顾一下第十章,你或许能弄明白如何在继续之前画出它。然后查看接下来的几页,看看你是否做对了!

第十二章:完成你的第一个游戏:Bounce!

Image

在上一章中,我们开始构建我们的第一个游戏Bounce!通过创建画布并将一个弹跳球添加到我们的代码中。目前,我们的球会永远在屏幕上弹跳,这并不能算是一个有趣的游戏。在这一章中,我们将为玩家添加一个挡板。我们还会给游戏加入一些随机元素,让它更具挑战性和趣味性。

添加挡板

如果没有东西能击打弹跳球,那就没有什么乐趣可言了。所以,让我们创建一个挡板吧!

我们首先在 Ball 类后面直接添加以下代码来创建一个挡板(你需要将其放在球的 draw 函数下方的一行):

Image

class Paddle:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        self.canvas.move(self.id, 200, 300)

    def draw(self):
        pass

这段新增的代码几乎与我们为 Ball 类编写的第一版代码完全相同,唯一的区别是我们调用的是 create_rectangle(而不是 create_oval),并将矩形移动到位置 200, 300(水平 200 像素,垂直 300 像素)。

接下来,在代码列表的底部创建一个 Paddle 类的对象,然后更改主循环以调用挡板的 draw 函数,如下所示:

➊ paddle = Paddle(canvas, 'blue')
   ball = Ball(canvas, 'red')

   while True:
       ball.draw()
    ➋ paddle.draw()
       tk.update_idletasks()
       tk.update()
       time.sleep(0.01)

更改可以在➊和➋看到。如果你现在运行游戏,你应该会看到弹跳球和一个静止的矩形挡板(图 12-1)。

Image

图 12-1:球和挡板

让挡板移动

为了让挡板左右移动,我们将使用事件绑定,将箭头和箭头键分别绑定到 Paddle 类中的新函数。当玩家按下左箭头键时,x 变量将被设置为−2(向左移动)。按下右箭头键时,x 变量将被设置为 2(向右移动)。

Image

第一步是将 x 对象变量添加到 Paddle 类的 init 函数中,并像在 Ball 类中那样添加一个画布宽度的变量:

    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)        
        self.canvas.move(self.id, 200, 300)
      ➊ self.x = 0
      ➋ self.canvas_width = self.canvas.winfo_width()

请参见➊和➋了解更改。现在我们将在 draw 函数之后添加用于改变方向的函数,分别是左转(turn_left)和右转(turn_right):

    def turn_left(self, evt):
        self.x = -2

    def turn_right(self, evt):
        self.x = 2

我们可以通过这两行代码在类的 init 函数中将这些函数绑定到正确的键位。在第 162 页的“让对象对某些事情作出反应”部分,我们使用了绑定方法使 Python 在按下某个键时调用函数。在这种情况下,我们将 Paddle 类的 turn_left 函数绑定到左箭头键,使用事件名称。然后我们将 turn_right 函数绑定到右箭头键,使用事件名称。我们的 init 函数现在看起来是这样的:

    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        self.canvas.move(self.id, 200, 300)
        self.x = 0
        self.canvas_width = self.canvas.winfo_width()
      ➊ self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
      ➋ self.canvas.bind_all('<KeyPress-Right>', self.turn_right)

请参见➊和➋了解更改。Paddle 类的 draw 函数与 Ball 类的 draw 函数类似:

    def draw(self):
        self.canvas.move(self.id, self.x, 0)
        pos = self.canvas.coords(self.id)
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.x = 0

我们使用画布的 move 函数通过 self.canvas.move(self.id, self.x, 0)将球拍移动到 x 变量的方向。然后,我们获取球拍的坐标,查看它是否碰到屏幕的左侧或右侧,使用 pos 中的值。与球弹跳不同,球拍应该停止移动。因此,当左侧的x坐标(pos[0])小于或等于 0(<= 0)时,我们将 x 变量设置为 0,使用 self.x = 0。同样地,当右侧的x坐标(pos[2])大于或等于画布宽度(>= self.canvas_width)时,我们也将 x 变量设置为 0。

注意

如果你现在运行程序,可能需要点击画布,才能让游戏识别左右箭头键的操作。点击画布会使画布获得焦点,这意味着它知道在有人按下键盘时应该接管控制。

查找球何时碰到球拍

在我们代码的这一部分,球不会碰到球拍。实际上,球会直接穿过球拍。球需要知道何时碰到球拍,就像它需要知道何时碰到墙壁一样。

图片

我们可以通过在绘制函数中添加代码来解决这个问题(在该函数中,我们已有检查墙壁的代码),但更好的做法是将这些代码移到新的函数中,把事情拆分成更小的部分。如果我们把太多代码放在一个地方(例如在一个函数内部),就会使代码变得更难理解。让我们进行必要的更改。

首先,我们修改球的 init 函数,以便可以将球拍对象作为参数传递:

class Ball:
  ➊ def __init__(self, canvas, paddle, color):
        self.canvas = canvas
     ➋ self.paddle = paddle
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
        starts = [-3, -2, -1, 1, 2, 3]
        self.x = random.choice(starts)
        self.y = -3
        self.canvas_height = self.canvas.winfo_height() 
        self.canvas_width = self.canvas.winfo_width()

注意,我们改变了 init 函数的参数,加入了球拍 ➊。然后,我们将球拍参数赋值给对象变量 paddle ➋。

保存了球拍对象后,我们需要更改创建球对象的代码。这个更改位于程序的底部,就在主 while 循环之前:

paddle = Paddle(canvas, 'blue')
ball = Ball(canvas, paddle, 'red')

while True:
    ball.draw()
    paddle.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

要判断球是否碰到球拍,我们需要一些比之前添加的检查墙壁的代码更复杂的代码。我们将这个函数命名为 hit_paddle,并在 Ball 类的绘制函数中调用它,那里我们检查球是否碰到屏幕底部:

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[1] <= 0 or pos[3] >= self.canvas_height:
            self.y = self.y * -1
      ➊ if self.hit_paddle(pos) == True:
          ➋ self.y = self.y * -1
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.x = self.x * -1

在新添加的代码中,如果 hit_paddle 返回 True ➊,我们通过将 y 对象变量设置为其值乘以-1 ➋(与球碰到画布顶部或底部时一样)来改变球的方向。通过这段代码,我们基本上是在说:“如果球(self)碰到球拍,那么我们就反转它的垂直方向。”

我们可以将顶部、底部和球拍的检查合并到一个 if 语句中——但如果我们将它们分开,新的程序员会更容易理解这段代码。

还不要尝试运行游戏;我们还需要创建 hit_paddle 函数。让我们现在就做这个。在 Ball 类的绘制函数前面添加 hit_paddle 函数:

    def hit_paddle(self, pos):
        paddle_pos = self.canvas.coords(self.paddle.id)
        if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
            if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
                return True
        return False

首先,我们定义一个带有 pos 参数的函数。这个参数包含球的当前坐标。然后,我们获取挡板的坐标,并将其存储在 paddle_pos 变量中。

接下来,我们有了 if-then 语句的第一部分,它表示:“如果球的右侧大于挡板的左侧,并且球的左侧小于挡板的右侧……”在这里,pos[2] 包含球的右侧的 x 坐标,pos[0] 包含球的左侧的 x 坐标。变量 paddle_pos[0] 包含挡板左侧的 x 坐标,paddle_pos[2] 包含挡板右侧的 x 坐标。图 12-2 显示了球即将击中挡板时这些坐标的样子。

图片

图 12-2:球即将击中挡板—显示水平坐标

球正在朝着挡板下落,但在这种情况下,您可以看到球的右侧 (pos[2]) 还没有越过挡板的左侧 (那就是 paddle_pos[0])。

接下来,我们检查球的底部 (pos[3]) 是否在挡板的顶部 (paddle_pos[1]) 和底部 (paddle_pos[3]) 之间。在图 12-3 中,您可以看到球的底部 (pos[3]) 还没有击中挡板的顶部 (paddle_pos[1])。

图片

图 12-3:球即将击中挡板—显示垂直坐标

因此,基于当前球的位置,hit_paddle 函数会返回 False。

注意

为什么我们需要检查球的底部是否在挡板的上下边缘之间?为什么不直接检查球的底部是否击中了挡板的顶部?因为每次我们在画布上移动球时,都会以 3 像素为单位跳跃。如果我们只检查球是否到达挡板的顶部(pos[1]),可能会跳过该位置。这样,球会继续移动,并会穿过挡板而停不下来。

添加一个随机因素

现在是时候将我们的程序变成一个游戏,而不仅仅是一个弹跳的球和一个挡板。游戏需要一个随机因素,或者一种让玩家失去的方式。在我们当前的游戏中,球会永远弹跳,因此没有任何东西可以失去。

图片

我们将通过添加代码来完成游戏,使得当球击中画布底部时游戏结束(换句话说,一旦球触地)。

首先,我们将 hit_bottom 对象变量添加到 Ball 类的 init 函数的底部:

        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        self.hit_bottom = False

然后我们更改程序底部的主循环,如下所示:

while True:
  ➊ if ball.hit_bottom == False:
        ball.draw()
        paddle.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

现在,循环不断检查 hit_bottom ➊,以查看球是否已经碰到屏幕底部。只有当球没有碰到底部时,代码才会继续移动球和挡板,正如前面的 if 语句所示。游戏在球和挡板停止移动时结束。(我们不再对它们进行动画处理。)

最后的修改是对 Ball 类的 draw 函数进行的:

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[1] <= 0:
            self.y = self.y * -1
     ➊ if pos[3] >= self.canvas_height:
            self.hit_bottom = True
        if self.hit_paddle(pos) == True:
            self.y = self.y * -1
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.y = self.y * -1

我们修改了 if 语句,检查球是否击中屏幕底部(即,球的位置是否大于或等于 canvas_height) ➊。如果是这样,在接下来的行中,我们将 hit_bottom 设置为 True,而不是改变 y 变量的值,因为一旦球击中屏幕底部,就不需要让球反弹了。

现在运行游戏,当你未能用球拍击中球时,屏幕上的所有动作应该停止。当球触及画布底部时,游戏应该结束,如图 12-4 所示。

Image

图 12-4:球击中屏幕底部

你的程序现在应该看起来像下面的代码。如果你在让游戏运行时遇到问题,请检查你的输入是否与这段代码一致:

from tkinter import *
import random
import time

class Ball:
    def __init__(self, canvas, paddle, color):
        self.canvas = canvas
        self.paddle = paddle
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
        starts = [-3, -2, -1, 1, 2, 3]
        self.x = random.choice(starts)
        self.y = -3
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        self.hit_bottom = False

    def hit_paddle(self, pos):
 paddle_pos = self.canvas.coords(self.paddle.id)
        if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
            if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
                return True
        return False

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)
        if pos[1] <= 0:
            self.y = self.y * -1
        if pos[3] >= self.canvas_height:
            self.hit_bottom = True
        if self.hit_paddle(pos) == True:
            self.y = self.y * -1
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.y = self.y * -1

class Paddle:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        self.canvas.move(self.id, 200, 300)
        self.x = 0
        self.canvas_width = self.canvas.winfo_width()
        self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        self.canvas.bind_all('<KeyPress-Right>', self.turn_right)

    def draw(self):
        self.canvas.move(self.id, self.x, 0)
        pos = self.canvas.coords(self.id)
        if pos[0] <= 0 or pos[2] >= self.canvas_width:
            self.x = 0

    def turn_left(self, evt):
        self.x = -2

    def turn_right(self, evt):
        self.x = 2

tk = Tk()
tk.title('Bounce Game')
tk.resizable(0, 0)
tk.wm_attributes('-topmost', 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
tk.update()

paddle = Paddle(canvas, 'blue')
ball = Ball(canvas, paddle, 'red')

while True:
    if ball.hit_bottom == False:
        ball.draw()
        paddle.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01) 

你学到了什么

在本章中,我们完成了使用 tkinter 模块创建我们的第一个游戏。我们为游戏中的球拍创建了类,并使用坐标来检查球何时击中球拍或游戏画布的墙壁。我们使用事件绑定将左右箭头键绑定到球拍的移动,并使用主循环调用 draw 函数来进行动画。最后,我们修改了代码,添加了一个随机元素,使得当玩家未能击中球并且球触及画布底部时,游戏结束。

Image

编程难题

目前,我们的游戏相当简单。你可以做很多修改来创建一个更有趣的游戏。尝试通过以下方式改进你的代码,然后将你的答案与* python-for-kids.com*上的解决方案进行比较。

#1:延迟游戏开始

我们的游戏启动很快,你需要点击画布才能让它识别键盘上的左右箭头键。你能否在游戏开始时添加延迟,以便给玩家足够的时间点击画布?或者更好的是,你能否添加鼠标点击的事件绑定,只有在点击时才开始游戏?

提示 1:你已经为 Paddle 类添加了事件绑定,所以可以从那里开始。

提示 2:左键鼠标的事件绑定是字符串’’。

#2:一个合适的“游戏结束”

游戏结束时一切冻结,这对玩家来说不太友好。当球触及画布底部时,尝试添加“游戏结束”文字。你可以使用 create_text 函数,但你可能也会发现命名参数 state 很有用(它的取值包括 normal 和 hidden)。请查看第 165 页的“更多使用标识符的方法”中的 itemconfig。作为额外挑战,添加一个延迟,这样文字就不会立刻出现。

#3:加速球

在网球中,当球击中你的球拍时,球有时会比到达时的速度飞得更快,这取决于你挥拍的力度。在我们的游戏中,无论球拍是否在移动,球的速度都是相同的。尝试修改程序,使得球拍的速度传递给球的速度。

#4: 记录玩家的得分

那么,如何记录得分呢?每次球击中球拍时,得分应该增加。尝试在画布的右上角显示得分。你可能想回顾一下第 165 页中“更多使用标识符的方法”部分的 itemconfig 来获取一些提示。

第三部分:# 小人先生冲向出口

第十三章:为《Mr. Stick Man》游戏创建图形

图片

在创建游戏(或任何程序)时,制定一个计划是个好主意。你的计划应包括游戏的概述,以及主要的元素和角色。当开始编程时,这个描述将帮助你集中注意力,确保你知道自己在开发什么。你的游戏可能不会完全按照最初的描述来实现——这是可以预料的!在本章中,我们将开始开发一个有趣的游戏,名为Mr. Stick Man races for the Exit

Mr. Stick Man 游戏计划

以下是我们新游戏的描述:

  • 秘密特工 Mr. Stick Man 被困在 Dr. Innocuous 的巢穴里;你需要帮助他通过顶楼的出口逃脱。

  • 游戏中有一个可以从左到右跑并跳起来的火柴人。每层楼有一些平台,玩家需要让他跳到这些平台上。

  • 目标是在游戏结束之前,抵达出口的门。

图片

根据这个描述,我们需要几个图像,分别用于 Mr. Stick Man、平台和门。我们将编写代码将这些元素组合在一起,但在此之前,我们需要为游戏创建图形。这样,我们在下一章时就有了可以使用的素材。

我们可以像在Bounce!游戏中一样使用图形来绘制这些元素,但那样的图形比较简单。相反,我们将创建精灵。

精灵是游戏中移动的物体——通常是某种角色。精灵通常是预渲染的,意味着它们是在程序运行之前就被绘制好的,而不是像我们在Bounce!游戏中那样,由程序通过多边形动态生成的。在这个游戏中,Mr. Stick Man、平台和门将会是精灵。为了创建这些图像,我们需要安装一个图形程序。

获取 GIMP

有很多图形程序可供选择,但对于这个游戏,我们需要一个支持透明度(有时也叫做alpha 通道)的程序,这样可以让图像的部分区域在屏幕上不显示颜色。我们需要透明部分的图像,因为当一张图像在屏幕上移动时,它可能会经过或接近另一张图像,这时候我们不希望一张图像的背景遮挡了另一张图像的内容。例如,在图 13-1 中,背景的棋盘图案代表了透明区域。

图片

图 13-1:GIMP 中的透明背景

如果我们复制整个图像并将它粘贴到另一个图像上面(也叫做叠加),背景就不会遮挡任何内容。这一点在图 13-2 中得到了展示。

图片

图 13-2:图像叠加

GIMP(www.gimp.org),即GNU 图像处理程序,是一个免费的图形程序,支持透明图像,适用于 Linux、macOS 和 Windows 系统。请按照以下步骤下载并安装它。

  • 如果你使用的是 Windows 或 macOS,可以在 GIMP 项目页面找到安装程序,网址是 www.gimp.org/downloads/

  • 如果你使用的是 Ubuntu,可以通过打开 Ubuntu 软件中心并在搜索框中输入gimp来安装 GIMP。当“GNU 图像处理程序”出现在搜索结果中时,点击安装按钮。

  • 如果你使用的是 Raspberry Pi,最简单的安装 GIMP 方法是使用命令行。打开终端并输入以下命令进行安装(此方法在 Ubuntu 上也适用):

    sudo apt install gimp
    

你还应该为你的游戏创建一个文件夹。为此,在桌面上任何空白区域右键点击并选择新建文件夹(在 Ubuntu 上,选项是创建新文件夹;在 macOS 上,是新建文件夹)。将文件夹命名为stickman

创建游戏元素

一旦你安装好图形程序,你就可以开始绘制了。我们将为游戏元素创建这些图像:

  • 可以左右奔跑和跳跃的火柴人图像

  • 平台的图像,三种不同的尺寸

  • 门的图像:一扇开着的和一扇关着的

  • 游戏背景图像(因为单一的白色或灰色背景会让游戏显得很无聊)

在我们开始绘制之前,我们需要准备带透明背景的图像。

准备透明图像

要设置一个透明背景的图像,启动 GIMP 并按照以下步骤操作:

  1. 选择文件新建

  2. 在对话框中,设置图像宽度为 27 像素,高度为 30 像素。

  3. 选择图层透明度添加 Alpha 通道

  4. 选择选择全选

  5. 选择编辑剪切

最终的结果应该是一个填充了深灰色和浅灰色棋盘格的图像,如图 13-3 所示(已放大)。

图像

图 13-3:放大透明背景

现在我们可以开始创建我们的秘密特工:火柴人了。

绘制火柴人

要绘制我们的第一张火柴人图像,在 GIMP 工具箱中点击画笔工具,然后在画笔工具栏中选择看起来像小点的画笔(称为Pixel),如图 13-4 所示。

图像

图 13-4:GIMP 工具箱

我们将为火柴人绘制三张不同的图像(或),展示他向右奔跑和跳跃。我们将使用这些帧来为火柴人做动画,就像我们在第十章中做的动画一样。

如果你放大查看这些图像,它们可能看起来像是图 13-5。

图像

图 13-5:放大火柴人

你的图像不需要完全相同,但它们应该展示火柴人在运动的三个不同姿势。每张图像应为 27 像素宽,30 像素高。

右侧奔跑的火柴人

首先,我们将为火柴人向右奔跑绘制一系列帧。按以下方式创建第一张图像:

  1. 绘制第一张图像(图 13-5 中最左边的图像)。

  2. 选择 文件另存为

  3. 在对话框中,输入 figure-R1.gif 作为文件名。然后点击标有 选择文件类型 的小加号 (+) 按钮。

  4. 在出现的列表中选择 GIF 图像

  5. 将文件保存到之前创建的 stickman 文件夹中(点击 浏览其他文件夹 查找正确的文件夹)。

按照相同的步骤,为下一个小人角色创建一个 27 x 30 像素的图像,如 图 13-5 中所示。将此图像保存为 figure-R2.gif。重复此过程,创建最后一张图像,并将其保存为 figure-R3.gif

图片

小人角色跑向左侧

我们可以不必重新绘制小人角色向左移动的画面,而是使用 GIMP 翻转小人角色向右移动的帧。

在 GIMP 中,按顺序打开每个图像,然后选择 工具变换工具翻转。当你点击图像时,你应该能看到它从一侧翻转到另一侧,如 图 13-6 所示。将图像保存为 figure-L1.giffigure-L2.giffigure-L3.gif

图片

图 13-6:翻转的小人角色

现在我们已经为小人角色创建了六个图像,但我们仍然需要为平台、门和背景创建图像。

绘制平台

我们将创建三种不同大小的平台:100 像素宽,10 像素高;66 像素宽,10 像素高;以及 32 像素宽,10 像素高。你可以根据自己的喜好绘制平台,但要确保它们的背景是透明的,就像小人图像一样。

图片

图 13-7 显示了当我们放大时平台可能的样子。

图片

图 13-7:放大平台

与小人图像一样,将这些图像保存到 stickman 文件夹中。将最大的平台命名为 platform1.gif,中等大小的命名为 platform2.gif,最小的平台命名为 platform3.gif

绘制门

门的大小应与小人角色的大小成比例(27 像素宽,30 像素高)。我们需要两张图像:一张是关闭的门,另一张是打开的门。这些门可能像 图 13-8 所示(放大后的效果)。

图片

图 13-8:放大门的效果

要创建这些图像,请按照以下步骤操作:

  1. 点击前景色框(在 GIMP 工具箱的底部),以显示颜色选择器。

  2. 选择你希望用于门的颜色。图 13-9 显示了一个选择了黄色的示例。

  3. 选择桶形工具(在工具箱中显示已选中),并用你选择的颜色填充屏幕。图片

    图 13-9:GIMP 显示背景颜色选择器

  4. 将前景颜色更改为黑色。

  5. 选择铅笔或画笔工具(这两种工具都在桶形工具的右侧),并绘制门和门把手的黑色轮廓。

  6. 将这些文件保存到stickman文件夹,并命名为door1.gifdoor2.gif

绘制背景

我们需要创建的最后一张图像是背景。我们将把这张图像的宽度设为 100 像素,高度设为 100 像素。它不需要透明背景,因为我们将用单一的颜色填充它,作为游戏中所有其他元素的背景“壁纸”。

要创建背景,选择文件新建,并输入图像尺寸为 100 像素宽,100 像素高。为恶棍的巢穴选择一个合适的邪恶颜色。我选择了较暗的粉色调。

你可以用花朵、条纹、星星等装饰你的壁纸——随你为游戏选择任何你喜欢的风格。例如,要在壁纸上添加星星,选择另一种颜色,选择铅笔工具,绘制第一颗星星。然后使用选择工具在星星周围选择一个框,并复制粘贴到图像的其他位置(选择编辑复制,然后编辑粘贴)。你应该能够通过点击已粘贴的图像并拖动它来调整位置。图 13-10 展示了一个带有星星的例子,并且选择工具已在工具箱中被选中。

Image

图 13-10:GIMP 选择工具

一旦你对你的图像满意,保存图像为background.gif并放入stickman文件夹中。

透明度

随着我们创建了这些图形,你可以更好地理解为什么我们的图像(除了背景)需要透明度。如果我们将 Mr. Stick Man 放在背景壁纸前面,并且他没有透明背景,我们的游戏就会像图 13-11 那样。

Image

图 13-11:没有透明度的小人

Mr. Stick Man 的白色背景遮挡了部分壁纸。如果我们使用透明图像,就能得到图 13-12。

Image

图 13-12:带透明度的小人

除了人物自己遮住的部分,背景中的内容不会被小人图像遮挡。这看起来专业多了!

你学到了什么

在本章中,你学会了如何为游戏编写基本的计划,并弄清楚从哪里开始。因为在制作游戏之前,我们需要图形元素,所以我们使用图形程序创建了基本框架。在这个过程中,你学会了如何使这些图像的背景透明,以免它们遮挡屏幕上的其他图像。

Image

在下一章中,我们将为游戏创建一些类。

第十四章:开发 Mr. Stick Man 游戏

Image

现在我们已经创建了 Mr. Stick Man Races for the Exit 的图像,可以开始编写代码了。上一章对游戏的描述让我们知道需要什么:一个可以跑步和跳跃的火柴人,以及他必须跳到的平台。我们将编写代码来显示火柴人并让它在屏幕上移动,同时显示平台。但在编写这些代码之前,我们需要先创建画布来显示我们的背景图像。

创建 Game 类

首先,我们将创建一个名为 Game 的类,它将作为我们程序的主要控制器。Game 类将有一个 init 函数用于初始化游戏,还有一个 mainloop 函数用于执行动画。

设置窗口标题并创建画布

init 函数的第一部分,我们将设置窗口标题并创建画布。正如你所看到的,这部分代码与我们在第十一章为 Bounce! 编写的代码类似。在 IDLE 中打开一个新文件,输入以下代码,并将文件保存为 stickmangame.py。确保将文件保存在我们在第十三章中创建的 stickman 文件夹中:

from tkinter import *
import random
import time  

class Game:
    def __init__(self):
        self.tk = Tk()
        self.tk.title('Mr. Stick Man Races for the Exit')
        self.tk.resizable(0, 0)
        self.tk.wm_attributes('-topmost', 1)
        self.canvas = Canvas(self.tk, width=500, height=500,
                             highlightthickness=0)
        self.canvas.pack()
        self.tk.update()
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()

在程序的前半部分(从 tkinter import * 到 self.tk.wm_attributes 的代码行),我们创建了 tk 对象,然后通过 self.tk.title 设置窗口标题为(“Mr. Stick Man Races for the Exit”)。我们通过调用 resizable 函数使窗口 固定(以防止窗口大小被调整),然后通过 wm_attributes 函数将窗口移到所有其他窗口的前面。

接下来,我们通过 self.canvas = Canvas 行创建了画布,并调用了 tk 对象的 pack 和 update 函数。最后,我们为 Game 类创建了两个变量 height 和 width,用于存储画布的高度和宽度(我们使用 winfo_height 和 winfo_width 函数来获取画布的尺寸)。

注意

在代码行 self.canvas = Canvas 中的反斜杠 () 仅用于分隔一行较长的代码。虽然在此情况下并不需要它,但为了提高可读性,我在此包含了它,因为整行代码无法在页面上显示完整。

完成 init 函数

现在,在你刚才创建的 stickman game.py 文件中输入其余的 init 函数代码。此代码将加载背景图片,并将其显示在画布上:

        self.tk.update()
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        self.bg = PhotoImage(file='background.gif')
        w = self.bg.width()
        h = self.bg.height()
     ➊ for x in range(0, 5):
         ➋ for y in range(0, 5):
                self.canvas.create_image(x * w, y * h, 
                        image=self.bg, anchor='nw')
        self.sprites = []
        self.running = True

在以 self.bg 开头的代码行中,我们创建了一个名为 bg 的变量,它包含一个 PhotoImage 对象——我们在第十三章的第 210 页中创建的背景图片文件 background.gif。接下来,我们将图片的宽度和高度存储在 w 和 h 变量中。PhotoImage 类的 width 和 height 函数返回加载后的图片大小。

接下来是函数内部的两个循环。为了理解它们的作用,想象你有一个小的橡皮图章、一个印泥垫和一张大纸。你如何用这个图章将纸张填满彩色的小方块呢?你可以随机地把图章盖在纸上,直到纸被填满。结果会很乱,而且需要花费一段时间,但最终会填满页面。或者你可以从一列开始盖印,然后回到顶部,开始在下一列上盖印,正如图 14-1 所示。

Image

图 14-1:在页面上盖印

我们在前一章创建的背景图像就是我们的“印章”。我们知道画布的宽度是 500 像素,高度也是 500 像素,而且我们创建了一个 100 像素见方的背景图像。这意味着我们需要五列和五行才能填满屏幕上的图像。我们使用一个 for 循环➊来计算横向的列数,再用另一个 for 循环➋来计算纵向的行数。

接下来,我们将第一个循环变量 x 乘以图像的宽度(x * w),以确定绘制的横向位置,然后将第二个循环变量 y 乘以图像的高度(y * h),以计算绘制的纵向位置。我们使用画布对象(self.canvas.create_image)的 create_image 函数,根据这些坐标将图像绘制到屏幕上。

最后,我们创建了 sprites 变量,它保存一个空列表,和 running 变量,它包含 True 布尔值。稍后我们将在游戏代码中使用这些变量。

创建 mainloop 函数

我们将使用 Game 类中的 mainloop 函数来动画化我们的游戏。这个函数看起来和我们在第十一章中为Bounce!游戏创建的主循环(或动画循环)非常相似。

我们的函数如下:

        for x in range(0, 5):
            for y in range(0, 5):
                self.canvas.create_image(x * w, y * h, 
                        image=self.bg, anchor='nw')
        self.sprites = []
        self.running = True

    def mainloop(self):
        while True:
           if self.running == True:
               for sprite in self.sprites:
                   sprite.move()
            self.tk.update_idletasks()
            self.tk.update()
            time.sleep(0.01)

我们创建了一个 while 循环,它会一直运行直到游戏窗口关闭(while True 是一个无限循环,我们第一次在第 175 页看到)。接下来,我们检查 running 变量是否等于 True。如果是,我们就遍历 sprites 列表中的所有精灵(self.sprites),并为每个精灵调用 move 函数。(我们还没有创建任何精灵,所以如果现在运行程序,这段代码不会有任何效果,但它以后会很有用。)

函数的最后三行强制 tk 对象重新绘制屏幕,并休眠片刻,就像我们在Bounce!游戏中所做的那样。

你可以运行这段代码,添加以下两行(注意没有缩进)并保存文件:

Image

g = Game()
g.mainloop()

注意

确保将这段代码添加到游戏文件的底部。同时,确保你的图像和 Python 文件都在你在第十三章中创建的stickman文件夹中。

这段代码创建了一个 Game 类的对象,并将其保存为 g 变量。然后,我们在这个新对象上调用 mainloop 函数来绘制屏幕。

保存程序后,在 IDLE 中通过 运行运行模块 来执行它。一个窗口应该会出现,背景图像填充画布,如图 14-2 所示。

Image

图 14-2:游戏背景

有了这个,我们为我们的游戏添加了一个漂亮的背景,并创建了一个动画循环,用于绘制精灵(在我们创建精灵后)。

创建 Coords 类

现在我们将创建一个类,用于指定精灵在游戏屏幕上的位置。这个类将存储游戏中任何组件的左上角(x1y1)和右下角(x2y2)坐标。

图 14-3 展示了如何使用这些坐标记录小人图像的位置。

Image

图 14-3:x 和 y 坐标在小人图像中的位置

我们的新类 Coords 将只包含一个 init 函数,我们将四个参数(x1, y1, x2 和 y2)传递给它。将这段代码放在 stickmangame.py 文件的开头:

class Coords:
    def __init__(self, x1=0, y1=0, x2=0, y2=0):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2

注意到每个参数都作为同名的对象变量(x1, y1, x2 和 y2)保存。我们很快会使用这个类的对象。

检测碰撞

一旦我们知道如何存储游戏精灵的位置,我们就需要一种方法来判断一个精灵是否与另一个精灵发生了碰撞,比如当小人跳动并撞到平台时。为了简化这个问题,我们可以将其分解为两个较小的问题:检查精灵是否在垂直方向发生碰撞,以及检查精灵是否在水平方向发生碰撞。然后,我们可以将这两个解决方案结合起来,查看精灵是否在任何方向上发生碰撞!

水平碰撞的精灵

首先,我们将创建 within_x 函数,来判断一组 x 坐标(x1x2)是否与另一组 x 坐标(同样是 x1x2)相交。直接在 Coords 类下方添加以下代码:

class Coords:
    def __init__(self, x1=0, y1=0, x2=0, y2=0):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2

def within_x(co1, co2):
 ➊ if co1.x1 > co2.x1 and co1.x1 < co2.x2:
        return True
 ➋ elif co1.x2 > co2.x1 and co1.x2 < co2.x2:
        return True
    elif co2.x1 > co1.x1 and co2.x1 < co1.x2:
        return True
    elif co2.x2 > co1.x1 and co2.x2 < co1.x2:
        return True
    else:
        return False

within_x 函数接收两个参数 co1 和 co2,它们都是 Coords 对象。我们首先检查第一个坐标对象(co1.x1)的最左边位置是否位于第二个坐标对象的最左边位置(co2.x1)和最右边位置(co2.x2)之间 ➊。如果是,返回 True。

Image

让我们来看看两条有重叠的 x 坐标的线条,以理解它是如何工作的。图 14-4 中的每条线从 x1 开始,到 x2 结束。

Image

图 14-4:重叠的水平 (x) 坐标

图中的第一条线(co1)从像素位置 50(x1)开始,到 100(x2)结束。第二条线(co2)从位置 40 开始,到 150 结束。在这种情况下,由于第一条线的 x1 位置位于第二条线的 x1 和 x2 位置之间,因此该函数中的 if 语句对于这两组坐标是成立的。

在第一个 elif 语句 ➋ 中,我们检查第一条线的最右位置(co1.x2)是否在第二条线的最左位置(co2.x1)和最右位置(co2.x2)之间。如果是,我们返回 True。接下来的两个 elif 语句做的几乎是一样的:它们检查第二条线(co2)的最左和最右位置与第一条线(co1)的位置是否重叠。

如果没有任何 if 语句匹配,我们会进入 else 并返回 False。这实际上是在说:“不,这两个坐标对象在水平方向上没有重叠。”

要查看该函数如何工作的示例,请回顾 图 14-4。第一个坐标对象的 x1x2 位置分别是 50 和 100,第二个坐标对象的 x1x2 位置分别是 40 和 150。以下是我们调用已创建的 within_x 函数时发生的情况:

>>> c1 = Coords(50, 50, 100, 100)
>>> c2 = Coords(40, 40, 150, 150)
>>> print(within_x(c1, c2))
True

该函数返回 True。这是确定一个精灵是否与另一个精灵发生碰撞的第一步。例如,当我们为小人和平台创建类时,我们就能判断它们的 x 坐标是否互相交叉。

在一个函数中有很多返回相同值的 ifelif 语句并不是最佳实践。为了解决这个问题,我们可以通过将每个条件用括号括起来,并用 or 关键字将它们连接,来简化 within_x 函数。为了使函数更简洁、代码行数更少,可以将其改为如下形式:

def within_x(co1, co2):
    if (co1.x1 > co2.x1 and co1.x1 < co2.x2) \
            or (co1.x2 > co2.x1 and co1.x2 < co2.x2) \
            or (co2.x1 > co1.x1 and co2.x1 < co1.x2) \
            or (co2.x2 > co1.x1 and co2.x2 < co1.x2):
        return True
    else:
        return False

为了将 if 语句扩展到多行,以避免出现包含所有条件的长行,我们使用反斜杠(\),如上所示。

精灵垂直碰撞

我们还需要知道精灵是否发生了垂直碰撞。within_y 函数与 within_x 函数非常相似。为了创建它,我们检查第一个坐标的 y1 位置是否与第二个坐标的 y1y2 位置发生重叠,然后反过来进行检查。

within_x 函数下面添加以下函数。这次我们将使用代码的简短版本(而不是大量的 if 语句):

Image

def within_y(co1, co2):
    if (co1.y1 > co2.y1 and co1.y1 < co2.y2) \
            or (co1.y2 > co2.y1 and co1.y2 < co2.y2) \
            or (co2.y1 > co1.y1 and co2.y1 < co1.y2) \
            or (co2.y2 > co1.y1 and co2.y2 < co1.y2):
        return True
    else:
        return False

我们的 within_xwithin_y 函数看起来非常相似,因为最终它们执行的操作是类似的。

将所有内容整合在一起:我们的最终碰撞检测代码

一旦我们确定了一个 x 坐标是否与另一个 x 坐标发生重叠,并且对 y 坐标做了同样的判断,我们就可以编写函数来判断一个精灵是否撞上了另一个精灵以及撞击在哪一侧。我们将通过 collided_leftcollided_rightcollided_topcollided_bottom 函数来实现这一点。

collided_left 函数

在我们刚刚创建的两个 within 函数下面,添加以下 collided_left 函数的代码:

def collided_left(co1, co2):
    if within_y(co1, co2):
        if co1.x1 >= co2.x1 and co1.x1 <= co2.x2:
            return True
    return False

该函数告诉我们第一个坐标对象的左侧(x1 值)是否与另一个坐标对象发生碰撞。

该函数接受两个参数:co1(第一个坐标对象)和 co2(第二个坐标对象)。我们使用within_y函数检查这两个坐标对象是否在垂直方向上发生了交叉。毕竟,如果小人漂浮在平台上方,检查他是否与平台碰撞就没有意义了(如图 14-5 所示)。

Image

图 14-5:小人位于平台上方

然后,我们检查第一个坐标对象的最左侧位置(co1.x1)是否已经碰到第二个坐标对象的 x2 位置(co2.x2)。如果是这样,它应该小于或等于 x2 位置。我们还检查它是否没有超过 x1 位置。如果它碰到了侧面,我们返回 True。如果没有任何 if 语句为真,我们返回 False。

Image

collided_right 函数

collided_right 函数看起来与 collided_left 非常相似:

def collided_right(co1, co2):
    if within_y(co1, co2):
        if co1.x2 >= co2.x1 and co1.x2 <= co2.x2:
            return True
    return False

与 collided_left 类似,我们检查y坐标是否发生交叉,使用within_y函数。然后,我们检查第一个坐标对象的 x2 值是否介于第二个坐标对象的 x1 和 x2 位置之间,如果是,返回 True。否则,返回 False。

collided_top 函数

collided_top 函数与我们刚刚添加的两个函数非常相似:

def collided_top(co1, co2):
    if within_x(co1, co2):
        if co1.y1 >= co2.y1 and co1.y1 <= co2.y2:
            return True
    return False

这次,我们检查坐标是否在水平方向上发生了交叉,使用within_x函数。接下来,我们检查第一个坐标的最顶部位置(co1.y1)是否已经超过第二个坐标的 y2 位置,但没有超过其 y1 位置。如果是这样,我们返回 True(第一个坐标的顶部已经碰到第二个坐标)。

collided_bottom 函数

我们的最后一个函数,collided_bottom,稍微有点不同:

def collided_bottom(y, co1, co2):
    if within_x(co1, co2):
        y_calc = co1.y2 + y
     ➊ if y_calc >= co2.y1 and y_calc <= co2.y2:
            return True
     return False

这个函数接受一个额外的参数 y,这是我们加到第一个坐标 y 位置的值。我们的 if 语句检查坐标是否在水平方向上发生了交叉(就像在 collided_top 函数中一样)。接下来,我们将 y 参数的值加到第一个坐标的 y2 位置,并将结果存储在 y_calc 变量中。如果新计算的值介于第二个坐标的 y1 和 y2 值之间 ➊,我们返回 True,因为坐标 co1 的底部已经碰到坐标 co2 的顶部。然而,如果没有任何 if 语句为真,我们返回 False。

我们需要额外的 y 参数,因为小人可能会从平台上掉下来。与其他碰撞函数不同,我们需要能够测试他是否会在底部发生碰撞,而不是是否已经碰撞。如果他从平台上走下来并继续漂浮在空中,我们的游戏就不太现实;因此,当他行走时,我们会检查他是否与左侧或右侧的物体发生碰撞。当我们检查他下面时,我们查看他是否会与平台发生碰撞;如果不会,他就需要摔下去了!

创建 Sprite 类

我们游戏项目的父类 Sprite 提供了两个函数:move 用于移动精灵,coords 用于返回精灵当前在屏幕上的位置。我们将 Sprite 类的代码添加到 collided_bottom 函数下面,如下所示:

class Sprite:
    def __init__(self, game):
 self.game = game
        self.endgame = False
        self.coordinates = None

    def move(self):
        pass

    def coords(self):
        return self.coordinates 

Sprite 类的 init 函数接受一个参数 game,它将是游戏对象。我们需要它,以便我们创建的任何精灵都能访问游戏中其他精灵的列表。我们将 game 参数存储为对象变量。

然后,我们将对象变量 endgame 存储起来,用于表示游戏结束。(目前它被设置为 False。)最后一个对象变量 coordinates 被设置为 None(无)。

move 函数在这个父类中什么都不做,因此我们在该函数的主体中使用 pass 关键字。coords 函数仅仅返回对象变量 coordinates。

所以我们的 Sprite 类有一个什么都不做的 move 函数和一个不返回坐标的 coords 函数。这听起来并不是很有用,对吧?然而,任何以 Sprite 作为父类的类都会拥有 move 和 coords 函数。所以,在游戏的主循环中,当我们遍历精灵列表时,调用 move 函数不会导致任何错误,因为每个精灵都有这个函数。

Image

注意

在编程中,具有功能不多的函数的类是相当常见的。从某种意义上讲,它们是一种约定,确保类的所有子类提供相同类型的功能,即使在某些情况下,子类中的函数什么都不做。

添加平台

现在我们将添加平台。我们的平台对象类 PlatformSprite 将是 Sprite 的子类。这个类的 init 函数将接受一个 game 参数(就像 Sprite 父类一样),还需要图像、x 和 y 位置以及图像的宽度和高度。下面是 PlatformSprite 类的代码,直接位于 Sprite 类下面:

class PlatformSprite(Sprite):
    def __init__(self, game, photo_image, x, y, width, height):
        Sprite.__init__(self, game)
        self.photo_image = photo_image
        self.image = game.canvas.create_image(x, y, 
                image=self.photo_image, anchor='nw')
        self.coordinates = Coords(x, y, x + width, y + height)

当我们定义 PlatformSprite 类时,给它一个参数:父类的名称(Sprite)。init 函数有七个参数:self、game、photo_image、x、y、width 和 height。

我们使用 self 和 game 作为参数值来调用父类 Sprite 的 init 函数,因为除了 self 参数外,Sprite 类的 init 函数只接受一个参数:game。

此时,如果我们创建一个 PlatformSprite 对象,它将拥有父类(game、endgame 和 coordinates)中的所有对象变量,仅仅因为我们调用了 Sprite 中的 init 函数。

Image

接下来,我们将 photo_image 参数保存为对象变量,并使用游戏对象的 canvas 变量通过 create_image 在屏幕上绘制图像。

最后,我们创建一个 Coords 对象,将 x 和 y 参数作为前两个参数。然后,我们将宽度和高度参数添加到这些参数中,作为后两个参数。

尽管在 Sprite 父类中坐标变量被设置为 None,但我们在 PlatformSprite 子类中将其更改为实际的 Coords 对象,包含平台图像在屏幕上的当前位置。

添加一个平台对象

让我们给游戏添加一个平台,看看它的样子。修改游戏文件的最后两行(stickmangame.py):

   g = Game()
➊ platform1 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                              0, 480, 100, 10)
➋ g.sprites.append(platform1)
   g.mainloop()

我们创建了一个 PlatformSprite 类的对象,将我们的游戏变量(g)和一个 PhotoImage 对象(它使用我们的第一个平台图像,platform1.gif) ➊ 传递给它。我们还传递了绘制平台的位置(横向 0 像素,纵向 480 像素,接近画布底部),以及图像的高度和宽度(宽 100 像素,高 10 像素)。我们将这个精灵添加到游戏对象中的精灵列表 ➋。

如果你现在运行游戏,你应该能看到屏幕左下角有一个平台,像是图 14-6。

Image

图 14-6:显示一个平台

添加多个平台

让我们添加许多平台。每个平台将具有不同的 xy 坐标,因此它们会分散在屏幕上。使用以下代码:

g = Game()
platform1 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           0, 480, 100, 10)
platform2 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           150, 440, 100, 10)
platform3 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           300, 400, 100, 10)
platform4 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           300, 160, 100, 10)
platform5 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           175, 350, 66, 10)
platform6 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           50, 300, 66, 10)
platform7 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           170, 120, 66, 10)
platform8 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           45, 60, 66, 10)
platform9 = PlatformSprite(g, PhotoImage(file='platform3.gif'), 
                           170, 250, 32, 10)
platform10 = PlatformSprite(g, PhotoImage(file='platform3.gif'), 
                            230, 200, 32, 10)
g.sprites.append(platform1)
g.sprites.append(platform2)
g.sprites.append(platform3)
g.sprites.append(platform4)
g.sprites.append(platform5)
g.sprites.append(platform6)
g.sprites.append(platform7)
g.sprites.append(platform8)
g.sprites.append(platform9)
g.sprites.append(platform10)
g.mainloop()

我们首先创建了大量的 PlatformSprite 对象,将它们保存为变量 platform1、platform2、platform3 等,直到 platform10。然后我们将每个平台添加到我们在 Game 类中创建的精灵变量中。如果你现在运行游戏,它应该看起来像是图 14-7。

Image

图 14-7:显示所有平台

我们已经创建了游戏的基础!现在我们准备添加我们的主角——火柴人先生。

你学到了什么

在本章中,你创建了 Game 类并将背景图像绘制到屏幕上。你学会了如何通过创建 within_x 和 within_y 函数来判断一个水平或垂直位置是否在另外两个水平或垂直位置的范围内。接着,你利用这些函数创建了新函数来判断一个坐标对象是否与另一个坐标对象发生碰撞。我们将在接下来的章节中使用这些函数,当我们为“火柴人先生”添加动画并需要检测他在画布上移动时是否与平台发生碰撞。

我们还创建了一个父类 Sprite 以及它的第一个子类 PlatformSprite,用来将平台绘制到画布上。

编程难题

以下编程难题是一些可以用来尝试游戏背景图像的方式。在 python-for-kids.com 检查你的答案。

#1:棋盘格

尝试修改 Game 类,使背景图像像棋盘格一样绘制,如图 14-8 所示。

Image

图 14-8:背景为棋盘格

#2:双图像棋盘格

一旦你弄明白如何创建棋盘格效果,试着使用两张交替的图片。创建另一张壁纸图像(使用你的图形程序),然后修改 Game 类,让它显示一个由两张交替图像组成的棋盘,而不是一张图像和空白背景。

#3: 书架与灯

你可以创建不同的壁纸图像,使游戏背景更有趣。创建背景图像的副本;然后画一个简单的书架、一张带灯的桌子或一扇窗户。通过修改 Game 类,让它显示一些不同的壁纸图像,将这些图像随机分布在屏幕上。

#4: 随机背景

作为两张图像棋盘格的替代方案,尝试创建五张不同的背景图像。你可以将它们画成一个重复的背景图案(1, 2, 3, 4, 5, 1, 2, 3, 4, 5,依此类推),或者你可以将它们随机绘制。

提示:如果你导入了 random 模块并将图片放在一个列表中,可以尝试使用 random.choice() 随机选择一张图片。

第十五章:创建 Mr. Stick Man

Image

在本章中,我们将创建 Mr. Stick Man Races for the Exit 的主角。这将是迄今为止我们编写的最复杂的代码,因为 Mr. Stick Man 需要向左和向右奔跑、跳跃、在碰到平台时停下,并在跑出平台边缘时下落。我们将使用事件绑定来响应左右箭头键,使 stick figure 向左和向右奔跑,当玩家按下空格键时让他跳跃。

初始化 Stick Figure

我们的新 stick figure 类的__init__函数与游戏中其他类的函数非常相似。我们首先给新类命名——StickFigureSprite——并将此类分配给父类 Sprite:

class StickFigureSprite(Sprite):
    def __init__(self, game):
        Sprite.__init__(self, game)

这段代码与第十四章中的 PlatformSprite 类很相似,除了我们没有使用任何额外的参数(除了 self 和 game)。这是因为,与 PlatformSprite 类不同,在游戏中只会使用一个 StickFigureSprite 对象。

加载 Stick Figure 图像

因为屏幕上有很多平台对象,我们将平台图像作为参数传递给 PlatformSprite 的 __init__ 函数(有点像在说:“这里,PlatformSprite,画图时用这张图像。”)。但由于只有一个 stick figure 对象,将图像加载到 sprite 外部再作为参数传递并不合理。StickFigureSprite 类将知道如何加载它自己的图像。

Image

__init__函数的接下来几行代码加载了三张左侧图像(用于让 stick figure 向左奔跑的动画)和三张右侧图像(用于让 stick figure 向右奔跑的动画)。我们需要现在就加载这些图像,因为我们不希望每次在屏幕上显示 stick figure 时都重新加载它们(这样会浪费时间并让游戏运行变慢):

class StickFigureSprite(Sprite):
    def __init__(self, game):
        Sprite.__init__(self, game)
     ➊ self.images_left = [
            PhotoImage(file='figure-L1.gif'),
            PhotoImage(file='figure-L2.gif'),
            PhotoImage(file='figure-L3.gif')
        ]
     ➋ self.images_right = [
            PhotoImage(file='figure-R1.gif'),
            PhotoImage(file='figure-R2.gif'),
            PhotoImage(file='figure-R3.gif')
        ]
     ➌ self.image = game.canvas.create_image(200, 470,
                image=self.images_left[0], anchor='nw')

这段代码加载了三张左侧图像(我们将用它们来让 stick figure 向左奔跑),以及三张右侧图像(我们将用它们来让 stick figure 向右奔跑)。

我们创建了对象变量 images_left ➊ 和 images_right ➋。每个变量都包含我们在第十章中创建的 PhotoImage 对象的列表,显示了 stick figure 面朝左和右的图像。

我们使用 images_left[0] 在 (200, 470) 的位置通过画布的 create_image 函数绘制第一张图像 ➌,这将把 stick figure 放置在游戏屏幕的中间,画布的底部。create_image 函数返回一个标识图像的编号。我们将这个标识符存储在对象变量 image 中,以便后续使用。

设置变量

__init__函数的下一个部分设置了我们将在后续代码中使用的更多变量:

        self.image = game.canvas.create_image(200, 470, 
                image=self.images_left[0], anchor='nw')
     ➊ self.x = -2
     ➋ self.y = 0
        self.current_image = 0
        self.current_image_add = 1
        self.jump_count = 0 
        self.last_time = time.time()
        self.coordinates = Coords()

对象变量 x ➊ 和 y ➋ 将存储我们在 stick figure 移动时会增加的水平坐标(x1x2)或垂直坐标(y1y2)。

正如你在第十一章中学到的,要使用 tkinter 模块制作动画,我们需要在对象的 x 或 y 位置上添加值来移动它在画布上。通过将 x 设置为-2,y 设置为 0,我们在代码中稍后会从 x 位置减去 2,而垂直位置不做任何变化,这样人物就会向左移动。

注意

记住,负的 x 数值意味着在画布上向左移动,正的 x 数值意味着向右移动;负的 y 数值意味着向上移动,正的 y 数值意味着向下移动。

接下来,我们创建对象变量current_image,用来存储当前显示在屏幕上的图像的索引位置。我们的左向图像列表images_left包含figure-L1.giffigure-L2.giffigure-L3.gif。它们分别是索引位置 0、1 和 2。

current_image_add变量将包含我们添加到current_image中存储的索引位置的数值,以获得下一个索引位置。例如,如果索引位置 0 的图像被显示,我们加 1 得到下一个索引位置 1 的图像,再加 1 得到列表中的最后一张图像,即索引位置 2 的图像。(你将在下一章看到我们如何使用这个变量来进行动画。)

jump_count变量是我们在人物跳跃时使用的计数器。last_time变量将记录我们在动画中更改人物图像的最后时间。我们使用时间模块的 time 函数来存储当前时间。

最后,我们将坐标对象变量设置为一个没有初始化参数的 Coords 类对象(x1、y1、x2 和 y2 都是 0)。与平台不同的是,人物的坐标会发生变化,因此我们稍后会设置这些值。

绑定按键

__init__函数的最后部分,绑定函数将一个按键绑定到我们代码中需要在按键按下时执行的某个操作:

        self.jump_count = 0 
        self.last_time = time.time()
        self.coordinates = Coords()
        game.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        game.canvas.bind_all('<KeyPress-Right>', self.turn_right)
        game.canvas.bind_all('<space>', self.jump)

我们将绑定到turn_left函数,将绑定到turn_right函数,将绑定到jump函数。现在我们需要创建这些函数,使人物能够移动。

转动人物向左和向右

turn_leftturn_right函数确保人物没有在跳跃,然后设置对象变量 x 的值来使人物左右移动。(我们的游戏不允许人物在空中改变方向。)

Image

        game.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        game.canvas.bind_all('<KeyPress-Right>', self.turn_right)
        game.canvas.bind_all('<space>', self.jump)

    def turn_left(self, evt):
        if self.y == 0:
         ➊ self.x = -2

    def turn_right(self, evt):
        if self.y == 0:
         ➋ self.x = 2

当玩家按下左箭头键时,Python 会调用turn_left函数,并传递一个包含玩家所做操作信息的对象作为参数。这个对象叫做事件对象,我们将其参数命名为evt

注意

事件对象对我们来说并不重要,但我们需要将它作为函数的参数,否则会出现错误,因为 Python 期望它在那儿。事件对象包含像 x y 的鼠标位置(对于鼠标事件),一个表示按下的特定键的代码(对于键盘事件),以及其他信息。对于这个游戏,这些信息都没有用,所以我们可以安全地忽略它。

要判断小人是否在跳跃,我们检查 y 对象变量的值。如果值不为 0,表示小人正在跳跃。在这段代码中,如果 y 的值为 0,我们将 x 设置为-2,表示向左跑 ➊,或者将 x 设置为 2,表示向右跑 ➋。我们使用-2 和 2,是因为将值设置为-1 或 1 不足以让小人快速穿越屏幕。

一旦小人的动画正常工作,尝试改变这个值,看看会有什么不同。

让小人跳跃

跳跃函数和 turn_left、turn_right 函数非常相似:

    def turn_right(self, evt):
        if self.y == 0:
            self.x = 2

    def jump(self, evt):
        if self.y == 0:
            self.y = -4
            self.jump_count = 0

这个函数再次接收一个 evt 参数(事件对象),我们可以忽略它,因为我们不需要有关事件的任何更多信息(和之前一样)。如果调用了这个函数,我们就知道空格键被按下了。

因为我们希望小人只有在不再跳跃的情况下才进行跳跃,我们检查 y 是否等于 0。如果小人没有跳跃,我们将 y 设置为-4(让他向屏幕上方跳跃),并将 jump_count 设置为 0。我们使用 jump_count 来确保小人不会永远跳跃。相反,我们让他跳跃一定次数后,再让他下落,就像重力在拉他一样。我们将在下一章添加这段代码。

Image

到目前为止我们做了什么

让我们回顾一下目前为止我们游戏中各个类和函数的定义,以及它们应该在文件中的位置。

在程序的顶部,你应该有你的导入语句,接着是 Game 和 Coords 类。Game 类将用于创建一个对象,作为我们游戏的主控制器,而 Coords 类的对象用于保存游戏中物体的位置(比如平台和小人):

from tkinter import *
import random
import time

class Game:
    --snip--
class Coords:
    --snip--

接下来,你应该有 within 函数(用于判断一个精灵的坐标是否在另一个精灵的区域内),Sprite 父类(这是我们游戏中所有精灵的父类),PlatformSprite 类,以及 StickFigureSprite 类的开头。我们使用 PlatformSprite 类创建平台对象,让小人跳跃其上。我们还创建了一个 StickFigureSprite 类的对象,表示游戏中的主角:

def within_x(co1, co2):
     --snip--
def within_y(co1, co2):
     --snip--
def collided_left(co1, co2):
     --snip--
def collided_right(co1, co2):
     --snip--
def collided_top(co1, co2):
     --snip--
def collided_bottom(y, co1, co2):
     --snip--
class Sprite:
     --snip--
class PlatformSprite(Sprite):
     --snip--
class StickFigureSprite(Sprite):
     --snip--

最后,在程序的末尾,你应该有一段代码来创建游戏中到目前为止的所有对象:游戏对象本身和平台。最后一行是调用 mainloop 函数的地方:

g = Game()
platform1 = PlatformSprite(g, PhotoImage(file='platform1.gif'),
                           0, 480, 100, 10)
...
g.sprites.append(platform1)
...
g.mainloop()

如果你的代码看起来有点不同,或者你在让它正常工作时遇到问题,你可以随时跳到第十六章的结尾,查看整个游戏的完整代码。

你学到了什么

在本章中,我们开始了火柴人类的工作。目前,如果我们创建了这个类的一个对象,它其实不会做太多事情,除了加载它需要的图像以便动画化火柴人,并设置一些稍后在代码中使用的对象变量。这个类包含了几个函数,用于根据键盘事件(当玩家按下左箭头、右箭头或空格键时)来改变这些对象变量的值。

在下一章,我们将完成我们的游戏。我们将为StickFigureSprite类编写函数,以显示和动画化火柴人,并将他移动到屏幕上。我们还将添加出口(门),火柴人先生正试图到达那里。

第十六章:完善《小人先生》游戏

图片

在前面的三章中,我们一直在开发我们的游戏:《小人先生竞速逃生》。我们创建了图像,然后编写代码添加背景图像、平台和小人。在本章中,我们将填补缺失的部分,给小人添加动画并添加门。你可以在本章末尾找到完整游戏的代码清单。如果你在编写某些代码时迷失了方向或感到困惑,可以将你的代码与该清单进行比较,看看你可能出了什么问题。

动画化小人

到目前为止,我们已经为小人创建了一个基本类,加载了我们将使用的图像,并将按键绑定到一些函数。但如果你此时运行游戏,我们的代码不会做出任何特别有趣的事情。

现在我们将向我们在第十五章中创建的 StickFigureSprite 类添加剩余的函数:animate、move 和 coords。animate 函数将绘制不同的小人图像;move 函数将确定角色需要移动的位置;coords 函数将返回小人当前位置。(与平台精灵不同,我们需要在小人移动时重新计算其在屏幕上的位置。)

图片

创建动画函数

首先,我们将添加动画函数,该函数需要检查是否有移动,并相应地更改图片。

检查是否移动

我们不希望在动画中改变小人图像的速度太快,否则其运动看起来不真实。想象一下在记事本角落绘制的翻页动画——如果翻页速度太快,你可能无法完全呈现你所绘制的效果。

动画函数的前半部分检查小人是向左还是向右跑,然后利用 last_time 变量来决定是否更换当前图片。这个变量将帮助我们控制动画的速度。这个函数会紧接着我们在第十五章(第 238 页)中为 StickFigureSprite 类添加的跳跃函数之后。

    def animate(self):
        if self.x != 0 and self.y == 0:
            if time.time() - self.last_time > 0.1:
                self.last_time = time.time()
                self.current_image += self.current_image_add
                if self.current_image >= 2:
                    self.current_image_add = -1
                if self.current_image <= 0:
                    self.current_image_add = 1

在第一个 if 语句中,我们检查 x 是否不为 0,以确定小人是否在移动(向左或向右),并检查 y 是否为 0,以确认小人没有跳跃。如果此 if 语句为 True,我们需要为小人添加动画;如果不为 True,则说明小人静止不动,因此无需继续绘制。如果小人没有移动,我们就跳出函数,后面的代码将被忽略。

然后,我们通过用time.time()获取当前时间,并减去 last_time 变量的值,计算出自上次调用 animate 函数以来经过的时间。这个计算用于决定是否绘制序列中的下一张图像。如果结果大于 0.1 秒,我们继续执行代码块。我们将 last_time 变量设置为当前时间,基本上是重置计时器,为下一次图像更改开始计时。

接下来,我们将对象变量 current_image_add 的值加到当前图像索引位置的变量 current_image 中。记住,我们在第十五章的火柴人__init__函数中创建了 current_image_add 变量(见第 235 页),所以当 animate 函数第一次被调用时,变量的值已经设置为 1。

然后,我们检查 current_image 中的索引位置的值是否大于或等于 2;如果是,我们将 current_image_add 的值改为-1。最后两行的过程类似;一旦达到 0,我们需要重新开始计数。

注意

如果你在弄清楚如何缩进这段代码时遇到困难,给你一个提示:在“if self.x”这一行开头有 8 个空格,而在最后一行有 20 个空格。

为了帮助你理解到目前为止函数中发生的事情,想象一下你在地板上有一排彩色块。你把手指从一个块移动到下一个块,每个你手指指向的块都有一个数字(1,2,3,4,依此类推)——这就是 current_image 变量。你手指指向的块的编号(它一次指向一个块)是存储在 current_image_add 变量中的数字。当你的手指朝着块的方向向前移动时,每次你都在加 1,而当它到达末端并向下移动时,你在减去 1(即加-1)。

我们添加到 animate 函数中的代码执行了这个过程,但不同的是,我们在列表中存储了三个方向的火柴人图像,而不是彩色块。这些图像的索引位置分别是 0、1 和 2。当我们动画展示火柴人时,一旦到达最后一张图像,我们开始倒计时;当我们到达第一张图像时,我们需要重新开始计数。因此,我们创造了一个奔跑人物的效果。

表 16-1 展示了我们如何通过 animate 函数中计算的索引位置,遍历图像列表。

表 16-1: 动画中的图像位置

位置 0 位置 1 位置 2 位置 1 位置 0 位置 1
计数增加 计数增加 计数增加 计数减少 计数减少 计数增加
Image Image Image Image Image Image

更改图像

在 animate 函数的下一部分,我们使用计算出的索引位置更改当前显示的图像:

    def animate(self):
    ...
        if self.x < 0:
            if self.y != 0:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_left[2])
         ➊ else:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_left[self.current_image])
        elif self.x > 0:
            if self.y != 0:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_right[2])
            else:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_right[self.current_image])

首先,如果 x 小于 0,小人正在向左移动,那么 Python 会进入代码块,检查 y 是否不等于 0(意味着小人在跳跃)。如果 y 不等于 0(小人正在跳跃),我们会使用 canvas 的 itemconfig 函数将显示的图像更改为我们的左向图像列表中的最后一张图像 images_left[2]。由于小人正在跳跃,我们将使用显示他全程奔跑的图像,让动画看起来更加逼真,正如你在 图 16-1 中看到的那样。

Image

图 16-1:跳跃图像

如果小人没有跳跃(也就是说,y 等于 0),else 块 ➊ 会使用 itemconfig 将显示的图像更改为当前图像索引变量 current_image 中的内容。

在 elif 语句中,我们检查小人是否在向右跑(x 大于 0),然后 Python 进入该代码块。这个代码与第一个块非常相似,再次检查小人是否跳跃,并在跳跃时绘制正确的图像,只不过这次使用的是 images_right 列表。

获取小人位置

因为我们需要确定小人当前在屏幕上的位置(因为他会移动),所以 coords 函数与其他 Sprite 类函数有所不同。我们将使用 canvas 的 coords 函数来确定小人所在的位置,然后使用这些值来设置我们在 第十五章 开头的 init 函数中创建的坐标变量的 x1、y1 和 x2、y2 值。将以下代码添加到 animate 函数后面:

    def coords(self):
        xy = self.game.canvas.coords(self.image)
        self.coordinates.x1 = xy[0]
        self.coordinates.y1 = xy[1]
        self.coordinates.x2 = xy[0] + 27
        self.coordinates.y2 = xy[1] + 30
        return self.coordinates

当我们在 第十四章 中创建 Game 类时,其中一个对象变量是 canvas。我们使用这个 canvas 变量的 coords 函数(通过 self.game.canvas.coords),它接受画布上某个绘制物体的标识符,并返回一个包含 xy 位置的两个数字的列表。在这里,我们使用存储在变量 current_image 中的标识符,并将返回的列表存储在变量 xy 中。然后我们使用这两个值来设置小人的坐标。xy[0] 的值(即列表中的第一个数字)成为我们的 x1 坐标,xy[1] 的值(列表中的第二个数字)成为我们的 y1 坐标。所以那是小人图像的左上角位置。

因为我们创建的所有小人图像都是 27 像素宽、30 像素高,所以我们可以通过将宽度和高度分别加到 xy[0] 和 xy[1] 值上来确定 x2 和 y2 应该是什么(那是小人的右下角位置)。

因此,如果 self.game.canvas.coords(self.image) 返回 [270, 350],我们将得到以下值:

  • self.coordinates.x1 的值将为 270

  • self.coordinates.y1 的值将为 350

  • self.coordinates.x2 的值将为 297

  • self.coordinates.y2 的值将为 380

最后,在函数的最后一行,我们返回刚刚更新的对象变量坐标。

让小人移动

StickFigureSprite 类的最后一个函数 move 负责实际控制游戏角色在屏幕上的移动。它还需要能够告诉我们角色是否碰到什么物体。

开始移动函数

以下代码是移动函数的第一部分。这将跟在 coords 之后:

    def move(self):
        self.animate()
        if self.y < 0:
            self.jump_count += 1
            if self.jump_count > 20:
                self.y = 4
        if self.y > 0:
            self.jump_count -= 1

第一行(self.animate())调用我们在本章前面创建的函数,如果需要,它会更改当前显示的图像。然后,我们检查 y 的值是否小于 0。如果小于 0,我们知道小人正在跳跃,因为负值会将他往上移动。(记住,0 是在画布的顶部,画布的底部是像素位置 500。)

接下来,我们给 jump_count 加 1。我们希望小人跳起来,但不能让他一直在屏幕上浮动(毕竟是在跳),所以我们使用这个变量来计算我们执行了多少次移动函数——如果计数达到 20,我们应该把 y 值设为 4,让小人再次开始下落。

然后我们检查 y 的值是否大于 0(这意味着角色必须在下落);如果是,我们从 jump_count 中减去 1,因为当计数到 20 时,我们需要重新向下计数。(把手慢慢向空中抬起,同时数到 20,然后再从 20 开始倒数,把手放回去,你就能明白计算小人上下跳跃的原理了。)

Image

在接下来的几行 move 函数中,我们调用 coords 函数,它告诉我们角色在屏幕上的位置,然后将结果存储在 co 变量中。接着我们创建变量 left、right、top、bottom 和 falling。在这个函数的后续部分,我们将使用这些变量:

        co = self.coords()
        left = True
        right = True
        top = True
        bottom = True
        falling = True

注意,每个变量都被设置为布尔值 True。我们将使用这些作为标志,检查角色是否碰到屏幕上的物体或正在下落。

小人是否碰到画布的底部或顶部?

move 函数的下一个部分检查我们的角色是否碰到画布的底部或顶部。请添加以下代码:

        if self.y > 0 and co.y2 >= self.game.canvas_height:
            self.y = 0
            bottom = False
        elif self.y < 0 and co.y1 <= 0:
            self.y = 0
            top = False

如果角色正在屏幕上方下落,y 将大于 0,因此我们需要确保它还没有碰到底部(否则它会从屏幕底部消失)。为此,我们检查其y2位置(小人底部)是否大于或等于游戏对象的 canvas_height 变量。如果是,我们将 y 的值设为 0,以停止小人下落,然后将 bottom 变量设为 False,这样剩下的代码就知道我们不再需要检查小人是否触底了。

确定棒人是否撞到屏幕顶部的过程与确定他是否撞到底部非常相似。为此,我们首先检查棒人是否在跳跃(y 小于 0),然后检查他的 y1 位置是否小于或等于 0,这意味着他撞到了画布顶部。如果两个条件都为真,我们将 y 设置为 0 以停止移动。我们还将 top 变量设置为 False,告知其余代码我们不再需要检查棒人是否撞到顶部。

棒人是否撞到画布的边缘?

我们几乎按照与前面代码完全相同的过程来判断棒人是否撞到画布的左右两侧,具体如下:

        if self.x > 0 and co.x2 >= self.game.canvas_width:
            self.x = 0
            right = False
        elif self.x < 0 and co.x1 <= 0:
            self.x = 0
            left = False

这个 if 语句基于以下事实:如果 x 大于 0,我们知道棒人在向右跑。我们还可以通过查看 x2 位置(co.x2)是否大于或等于存储在 canvas_width 中的画布宽度,来判断他是否撞到屏幕的右侧。如果任一条件为真,我们将 x 设置为 0(以停止棒人跑动),并将 right 或 left 变量设置为 False。

与其他精灵发生碰撞

一旦确定棒人是否撞到屏幕的两侧,我们需要检查他是否撞到屏幕上的其他物体。我们使用以下代码循环遍历存储在游戏对象中的精灵列表,查看棒人是否撞到其中任何一个:

        for sprite in self.game.sprites:
            if sprite == self:
                continue
            sprite_co = sprite.coords()
            if top and self.y < 0 and collided_top(co, sprite_co):
                self.y = -self.y
                top = False

在 for 语句中,我们循环遍历精灵列表,依次将每个精灵赋值给变量 sprite。然后我们说,如果精灵等于 self(这是另一种说法,“如果这个精灵实际上是我”),我们就不需要确定棒人是否发生碰撞,因为他只会撞到自己。如果 sprite 变量等于 self,我们使用 continue 跳到列表中的下一个精灵(continue 只是告诉 Python 忽略代码块中的其余代码,继续循环)。

接下来,我们通过调用其 coords 函数并将结果存储在 sprite_co 变量中来获取新精灵的坐标。

最终的 if 语句检查以下内容:

  • 棒人没有撞到画布顶部(top 变量仍为真)。

  • 棒人正在跳跃(y 值小于 0)。

  • 棒人的顶部与精灵列表中的精灵发生了碰撞(使用我们在 第 224 页 创建的 collided_top 函数)。

Image

如果这些条件都为真,我们希望精灵重新开始下落,因此我们将 y 变量的值反转(self.y 变为 -self.y)。top 变量设置为 False,因为一旦棒人撞到顶部,我们就不需要继续检查碰撞。

底部碰撞

循环的下一部分检查我们的角色底部是否撞到什么东西:

            if bottom and self.y > 0 and collided_bottom(self.y,
                    co, sprite_co):
                self.y = sprite_co.y1 - co.y2
                if self.y < 0:
                    self.y = 0
                bottom = False
                top = False

我们从三个相似的检查开始:底部变量是否仍然设置,角色是否在下落(y 大于 0),以及我们角色的底部是否碰到精灵。如果这三个检查都为真,我们将小人底部的 y 值(y2)从精灵顶部的 y 值(y1)中减去。这个看起来可能有些奇怪,下面我们来讨论一下为什么要这样做。

假设我们的游戏角色已经从平台上掉下来。每当 mainloop 函数运行时,他都会下移 4 像素,直到小人脚下距离下一个平台只有 3 像素。假设小人底部(y2)的位置是 57,平台顶部(y1)的位置是 60。在这种情况下,collided_bottom 函数会返回 True,因为它的代码会将 y 的值(即 4)加到小人的 y2 变量上,得到 61。

然而,我们并不希望小人一看到他会碰到平台或屏幕底部就停止下落,因为那就像是从台阶上跳下来却在半空中停住,离地面只有一英寸。虽然那可能是一个有趣的特技,但在我们的游戏中看起来不太合适。相反,如果我们将角色的 y2 值(57)从平台的 y1 值(60)中减去,我们得到 3,这就是小人应该下落的距离,以便能够正确地落在平台上。

我们继续确保计算不会导致负数(if self.y < 0:);如果是负数,我们将 y 设置为 0。(如果允许这个数值为负,小人就会飞回去,我们不希望那样发生。)

最后,我们将顶部和底部标志设置为 False,这样我们就不再需要检查小人是否与其他精灵在顶部或底部发生碰撞。

接下来,我们再做一次“底部”检查,查看小人是否已经跑出了平台的边缘。以下是该 if 语句的代码:

            if bottom and falling and self.y == 0 \
                    and co.y2 < self.game.canvas_height \
                    and collided_bottom(1, co, sprite_co):
                falling = False

要将下落变量设置为 False,我们必须确保以下五个元素都为真:

  • 底部标志被设置为 True。

  • 小人应该正在下落(下落标志仍然为 True)。

  • 小人并未开始下落(y 为 0)。

  • 精灵的底部没有碰到底部屏幕(它小于画布的高度)。

  • 小人已经碰到了平台的顶部(collided_bottom 返回 True)。

然后,我们将下落变量设置为 False,停止角色继续向下掉落。

注意

你可以通过简单地引用变量来检查布尔变量的值是否为 True。例如,如果 bottom == True 且 falling == True,可以简化为 if bottom 和 falling(就像我们上面做的那样)。

检查左右

我们已经检查了小人是否碰到了精灵的底部或顶部。现在我们需要检查他是否碰到了左右两侧,可以使用以下代码:

            if left and self.x < 0 and collided_left(co, sprite_co):
                self.x = 0
                left = False
            if right and self.x > 0 and collided_right(co, sprite_co):
                self.x = 0
                right = False

首先,我们检查是否仍需要检查左侧的碰撞(left 仍然设置为 True)以及火柴人是否在向左移动(x 小于 0)。我们还检查火柴人是否与精灵发生碰撞,使用 collided_left 函数。如果这三个条件都成立,我们将 x 设置为 0(使火柴人停止跑动),并将 left 设置为 False,这样我们就不再检查左侧的碰撞了。

图片

对右侧碰撞的代码类似。我们再次将 x 设置为 0,并将 right 设置为 False,以停止检查右侧的碰撞。

现在,经过四个方向碰撞的检查,我们的 for 循环应该是这样的:

        for sprite in self.game.sprites:
            if sprite == self:
                continue
            sprite_co = sprite.coords()
            if top and self.y < 0 and collided_top(co, sprite_co):
                self.y = -self.y
                top = False
            if bottom and self.y > 0 and collided_bottom(self.y,
                    co, sprite_co):
                self.y = sprite_co.y1 - co.y2
                if self.y < 0:
                    self.y = 0
                bottom = False
                top = False
            if bottom and falling and self.y == 0 \
                    and co.y2 < self.game.canvas_height \
                    and collided_bottom(1, co, sprite_co):
                falling = False
            if left and self.x < 0 and collided_left(co, sprite_co):
                self.x = 0
                left = False
            if right and self.x > 0 and collided_right(co, sprite_co):
                self.x = 0
                right = False

我们只需要向移动函数中再添加几行,如下所示:

        if falling and bottom and self.y == 0 \
                and co.y2 < self.game.canvas_height:
            self.y = 4
        self.game.canvas.move(self.image, self.x, self.y)

我们检查掉落和底部变量是否都设置为 True。如果是这样,我们就已经遍历了列表中的每一个平台精灵,而没有在底部发生碰撞。

这行中的最终检查确定我们的角色底部是否小于画布的高度——也就是说,是否在地面上方(画布底部)。如果火柴人没有与任何物体碰撞并且在地面上方,他就站在空中,因此他应该开始掉落(换句话说,他已经跑出了平台的边缘)。为了让他跑出任何平台的边缘,我们将 y 设置为 4。

最后,我们根据在 x 和 y 变量中设置的值将图像在屏幕上移动。我们遍历精灵并检查碰撞,可能意味着我们已经将两个变量都设置为 0,因为火柴人已经在左边和底部发生碰撞。在这种情况下,调用画布的移动函数实际上什么也不做。

也有可能是火柴人走出了平台的边缘。如果发生这种情况,y 将被设置为 4,火柴人将向下掉落。

呼,那个函数真长!

测试我们的火柴人精灵

创建了 StickFigureSprite 类之后,让我们通过在调用 mainloop 函数之前添加以下两行来进行测试:

sf = StickFigureSprite(g)
g.sprites.append(sf)

我们创建一个 StickFigureSprite 对象,并用 sf 变量标记它。像对待平台一样,我们将这个新变量添加到存储在游戏对象中的精灵列表中。

现在运行程序。你应该发现火柴人能够跑动、从平台跳到平台并掉落!

退出!

我们游戏中唯一缺少的就是出口。我们将通过创建一个门的精灵、添加检测门的代码以及给程序添加门对象来完成这个部分。

创建 DoorSprite 类

我们需要创建一个新类:DoorSprite。代码的开头如下所示:

class DoorSprite(Sprite):
    def __init__(self, game, photo_image, x, y, width, height):
        Sprite.__init__(self, game)
        self.photo_image = photo_image
        self.image = game.canvas.create_image(x, y, 
                image=self.photo_image, anchor='nw')
        self.coordinates = Coords(x, y, x + (width / 2), y + height)
        self.endgame = True

DoorSprite 类的 init 函数有自我、游戏对象、photo_image 对象、x 和 y 坐标以及图像的宽度和高度等参数。我们像其他精灵类一样调用 Sprite.init

图片

然后,我们像处理 PlatformSprite 一样,使用相同名称的对象变量保存参数 photo_image。我们使用画布的 create_image 函数创建显示图像,并使用该函数返回的标识号将其保存到对象变量 image 中。

接下来,我们将门精灵的坐标设置为 x 和 y 参数(它们变成门的x1y1位置),然后计算x2y2位置。我们通过将宽度的一半(宽度变量除以 2)加到 x 参数来计算x2位置。例如,如果 x 是 10(x1 坐标也是 10),而宽度是 40,那么 x2 坐标就是 30(10 加上 40 的一半)。

为什么使用这个让人困惑的小计算呢?因为与平台不同,在平台上我们希望小人碰到平台的边缘时立刻停止,但我们希望他停在门前。你会在玩游戏并到达门时看到这一点。

x1位置不同,y1位置的计算非常简单。我们只需将高度变量的值加到 y 参数上,其他的就没什么了。

最后,我们将 endgame 对象变量设置为 True。这表示当小人到达门时,游戏结束。

检测门

现在我们需要修改 StickFigureSprite 类中 move 函数的代码,该函数决定小人何时与左侧或右侧的精灵发生碰撞。以下是第一次修改:

 if left and self.x < 0 and collided_left(co, sprite_co):
                self.x = 0
                left = False
                if sprite.endgame:
                    self.game.running = False

我们检查小人是否与一个设置了 endgame 变量为 True 的精灵发生了碰撞。如果发生了碰撞,我们将 running 变量设置为 False,一切停止——游戏结束。

我们将这些相同的代码行添加到检查右侧碰撞的代码中。以下是代码:

            if right and self.x > 0 and collided_right(co, sprite_co):
                self.x = 0
                right = False
                if sprite.endgame:
                    self.game.running = False

添加门对象

我们对游戏代码的最终补充是为门创建一个对象。我们将在主循环之前添加这个对象。在创建小人对象之前,我们将创建一个门对象,然后将其添加到精灵列表中。以下是代码:

g.sprites.append(platform7)
g.sprites.append(platform8)
g.sprites.append(platform9)
g.sprites.append(platform10)
door = DoorSprite(g, PhotoImage(file='door1.gif'), 45, 30, 40, 35)
g.sprites.append(door)
sf = StickFigureSprite(g)
g.sprites.append(sf)
g.mainloop()

我们使用游戏对象的变量 g 创建一个门对象,后面跟一个 PhotoImage(我们在第十三章中创建的门图像)。我们将 x 和 y 参数设置为 45 和 30,将门放在屏幕顶部附近的一个平台上,并设置宽度和高度为 40 和 35。我们像处理游戏中其他精灵一样将门对象添加到精灵列表中。

你可以看到当小人到达门时的效果。他停在门前,而不是门旁边,正如图 16-2 所示。

图片

图 16-2:到达门

最终游戏

我们的游戏完整代码现在已经有 200 多行了。以下是游戏的完整代码。如果你在运行游戏时遇到问题,可以将每个函数(以及每个类)与这段代码进行对比:

from tkinter import *
import random
import time

class Coords:
    def __init__(self, x1=0, y1=0, x2=0, y2=0):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2

def within_x(co1, co2):
    if (co1.x1 > co2.x1 and co1.x1 < co2.x2) \
            or (co1.x2 > co2.x1 and co1.x2 < co2.x2) \
            or (co2.x1 > co1.x1 and co2.x1 < co1.x2) \
            or (co2.x2 > co1.x1 and co2.x2 < co1.x2):
        return True
    else:
        return False

def within_y(co1, co2):
    if (co1.y1 > co2.y1 and co1.y1 < co2.y2) \
            or (co1.y2 > co2.y1 and co1.y2 < co2.y2) \
            or (co2.y1 > co1.y1 and co2.y1 < co1.y2) \
            or (co2.y2 > co1.y1 and co2.y2 < co1.y2):
        return True
    else:
        return False

def collided_left(co1, co2):
    if within_y(co1, co2):
        if co1.x1 >= co2.x1 and co1.x1 <= co2.x2:
            return True
    return False

def collided_right(co1, co2):
    if within_y(co1, co2):
        if co1.x2 >= co2.x1 and co1.x2 <= co2.x2:
            return True
    return False

def collided_top(co1, co2):
    if within_x(co1, co2):
        if co1.y1 >= co2.y1 and co1.y1 <= co2.y2:
            return True
    return False

def collided_bottom(y, co1, co2):
    if within_x(co1, co2):
        y_calc = co1.y2 + y
        if y_calc >= co2.y1 and y_calc <= co2.y2:
            return True
    return False

class Sprite:
    def __init__(self, game):
        self.game = game
        self.endgame = False
        self.coordinates = None
    def move(self):
        pass
    def coords(self):
        return self.coordinates

class PlatformSprite(Sprite):
    def __init__(self, game, photo_image, x, y, width, height):
        Sprite.__init__(self, game)
        self.photo_image = photo_image
        self.image = game.canvas.create_image(x, y, 
                image=self.photo_image, anchor='nw')
        self.coordinates = Coords(x, y, x + width, y + height)

class StickFigureSprite(Sprite):
    def __init__(self, game):
        Sprite.__init__(self, game)
        self.images_left = [
            PhotoImage(file='figure-L1.gif'),
            PhotoImage(file='figure-L2.gif'),
            PhotoImage(file='figure-L3.gif')
        ]
        self.images_right = [
            PhotoImage(file='figure-R1.gif'),
            PhotoImage(file='figure-R2.gif'),
            PhotoImage(file='figure-R3.gif')
        ]
        self.image = game.canvas.create_image(200, 470, 
                image=self.images_left[0], anchor='nw')
        self.x = -2
        self.y = 0
        self.current_image = 0
        self.current_image_add = 1
        self.jump_count = 0
        self.last_time = time.time()
        self.coordinates = Coords()
        game.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        game.canvas.bind_all('<KeyPress-Right>', self.turn_right)
        game.canvas.bind_all('<space>', self.jump)

    def turn_left(self, evt):
        if self.y == 0:
            self.x = -2

    def turn_right(self, evt):
        if self.y == 0:
            self.x = 2

    def jump(self, evt):
        if self.y == 0:
            self.y = -4
            self.jump_count = 0

    def animate(self):
        if self.x != 0 and self.y == 0:
            if time.time() - self.last_time > 0.1:
                self.last_time = time.time()
                self.current_image += self.current_image_add
                if self.current_image >= 2:
                    self.current_image_add = -1
                if self.current_image <= 0:
                    self.current_image_add = 1
        if self.x < 0:
            if self.y != 0:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_left[2])
            else:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_left[self.current_image])
        elif self.x > 0:
            if self.y != 0:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_right[2])
            else:
                self.game.canvas.itemconfig(self.image, 
                        image=self.images_right[self.current_image])

    def coords(self):
        xy = self.game.canvas.coords(self.image)
        self.coordinates.x1 = xy[0]
        self.coordinates.y1 = xy[1]
        self.coordinates.x2 = xy[0] + 27
        self.coordinates.y2 = xy[1] + 30
        return self.coordinates

    def move(self):
        self.animate()
        if self.y < 0:
            self.jump_count += 1
            if self.jump_count > 20:
                self.y = 4
        if self.y > 0:
            self.jump_count -= 1
        co = self.coords()
        left = True
        right = True
        top = True
        bottom = True
        falling = True
        if self.y > 0 and co.y2 >= self.game.canvas_height:
            self.y = 0
            bottom = False
        elif self.y < 0 and co.y1 <= 0:
            self.y = 0
            top = False
        if self.x > 0 and co.x2 >= self.game.canvas_width:
            self.x = 0
            right = False
        elif self.x < 0 and co.x1 <= 0:
            self.x = 0
            left = False

        for sprite in self.game.sprites:
            if sprite == self:
                continue
            sprite_co = sprite.coords()
            if top and self.y < 0 and collided_top(co, sprite_co):
                self.y = -self.y
                top = False
            if bottom and self.y > 0 and collided_bottom(self.y, 
                    co, sprite_co):
                self.y = sprite_co.y1 - co.y2
                if self.y < 0:
                    self.y = 0
                bottom = False
                top = False
            if bottom and falling and self.y == 0 \
                    and co.y2 < self.game.canvas_height \
                    and collided_bottom(1, co, sprite_co):
                falling = False
            if left and self.x < 0 and collided_left(co, sprite_co):
                self.x = 0
                left = False
                if sprite.endgame:
                    self.game.running = False
            if right and self.x > 0 and collided_right(co, sprite_co):
                self.x = 0
                right = False
                if sprite.endgame:
                    self.game.running = False

        if falling and bottom and self.y == 0 \
                and co.y2 < self.game.canvas_height:
            self.y = 4
        self.game.canvas.move(self.image, self.x, self.y)

class DoorSprite(Sprite):
    def __init__(self, game, photo_image, x, y, width, height):
        Sprite.__init__(self, game)
        self.photo_image = photo_image
        self.image = game.canvas.create_image(x, y, 
                image=self.photo_image, anchor='nw')
        self.coordinates = Coords(x, y, x + (width / 2), y + height)
        self.endgame = True

class Game:
    def __init__(self):
        self.tk = Tk()
        self.tk.title('Mr. Stick Man Races for the Exit')
        self.tk.resizable(0, 0)
        self.tk.wm_attributes('-topmost', 1)
        self.canvas = Canvas(self.tk, width=500, height=500, 
                             highlightthickness=0)
        self.canvas.pack()
        self.tk.update()
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        self.bg = PhotoImage(file='background.gif')
        w = self.bg.width()
        h = self.bg.height()
        for x in range(0, 5):
            for y in range(0, 5):
                self.canvas.create_image(x * w, y * h, 
                        image=self.bg, anchor='nw')
        self.sprites = []
        self.running = True

    def mainloop(self):
        while True:
            if self.running == True:
                for sprite in self.sprites:
                    sprite.move()
            self.tk.update_idletasks()
            self.tk.update()
            time.sleep(0.01)

g = Game()
platform1 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           0, 480, 100, 10)
platform2 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           150, 440, 100, 10)
platform3 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           300, 400, 100, 10)
platform4 = PlatformSprite(g, PhotoImage(file='platform1.gif'), 
                           300, 160, 100, 10)
platform5 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           175, 350, 66, 10)
platform6 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           50, 300, 66, 10)
platform7 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           170, 120, 66, 10)
platform8 = PlatformSprite(g, PhotoImage(file='platform2.gif'), 
                           45, 60, 66, 10)
platform9 = PlatformSprite(g, PhotoImage(file='platform3.gif'), 
                           170, 250, 32, 10)
platform10 = PlatformSprite(g, PhotoImage(file='platform3.gif'), 
                            230, 200, 32, 10)
g.sprites.append(platform1)
g.sprites.append(platform2)
g.sprites.append(platform3)
g.sprites.append(platform4)
g.sprites.append(platform5)
g.sprites.append(platform6)
g.sprites.append(platform7)
g.sprites.append(platform8)
g.sprites.append(platform9)
g.sprites.append(platform10)
door = DoorSprite(g, PhotoImage(file='door1.gif'), 45, 30, 40, 35)
g.sprites.append(door)
sf = StickFigureSprite(g)
g.sprites.append(sf)
g.mainloop()

你学到了什么

在这一章中,我们完成了我们的游戏,小棍人跑向出口。我们为动画小棍人创建了一个类,并编写了函数来移动他在屏幕上的位置,并在他移动时进行动画处理(通过图像切换来营造奔跑的错觉)。我们使用了基本的碰撞检测来判断他是否撞到画布的左右边缘,或者是否撞到其他精灵,如平台或门。我们还添加了碰撞代码,以判断他是否撞到屏幕的顶部或底部,并确保当他跑出平台的边缘时,会相应地跌落下来。我们添加了代码来判断小棍人是否到达门口,这样游戏就结束了。

图片

编程难题

还有很多事情可以做,以改善游戏。我们可以添加代码,使其看起来更专业,也更有趣。尝试添加以下功能,然后将你的代码与* python-for-kids.com*上的解决方案进行比较。

#1:“你赢了!”

类似于我们在第十二章中完成的Bounce!游戏中的“游戏结束”文字,当小棍人到达门口时,添加“你赢了!”的文字。

#2:动画门的实现

在第十三章中,我们为门创建了两个图像:一个是开着的,另一个是关着的。当小棍人到达门时,门的图像应该切换为开门状态,小棍人应该消失,而门的图像应该恢复为关门状态。这将产生小棍人退出并关闭门的错觉。你可以通过修改 DoorSprite 类和 StickFigureSprite 类来实现这一点。

图片

#3:移动平台

尝试添加一个新的类,名为 MovingPlatformSprite。这个平台应该左右移动,使得小棍人更难到达顶部的门。你可以选择一些平台设置为移动的,其他平台保持静态,具体取决于你希望游戏的难度有多大。

#4:台灯作为精灵

不再使用我们在第十四章的第三个编程难题中添加的书架和台灯作为背景图片,而是尝试添加一个小棍人需要跳过的台灯。它将不再是游戏背景的一部分,而是一个与平台或门类似的精灵。

图片

第十七章:后记:接下来该做什么

Image

你在 Python 之旅中已经学到了一些基本的编程概念,但还有很多内容等待你去发现——无论是用 Python 还是使用其他编程语言。虽然 Python 非常有用,但并非每个任务都是最适合使用它的,因此不要害怕尝试其他方式来编程。

如果你想继续使用 Python,并寻找更高级的书籍阅读,Python 维基(书籍页面)是一个不错的起点:wiki.python.org/moin/PythonBooks/

如果你只想探索 Python 能做些什么,它有很多内置模块。(这就是 Python 的“电池附带”理念。查阅 Python 文档,获取完整的模块列表:docs.python.org/3/py-modindex.html。)此外,还有大量由世界各地程序员免费提供的模块。例如,你可以尝试 Pygame (www.pygame.org) 用于游戏开发,或者尝试 Jupyter Notebooks (jupyter.org),这是一个基于 web 的环境,可以在浏览器中编辑和运行 Python 代码。这些模块及其他模块可以在 pypi.org 上浏览。要安装这些模块,你需要使用一个名为 pip 的工具,我们将在接下来简要介绍。

在 Windows 上安装 Python pip

只要你已经安装了 Python 3.10 或更新版本,pip 应该会默认安装。要在 Windows 上安装 pip,如果你使用的是较旧版本的 Python,可以从 bootstrap.pypa.io/get-pip.py 下载脚本 get-pip.py

将文件保存到你的主文件夹,然后打开命令提示符(点击开始并在搜索框中输入 cmd)。要安装,输入python get-pip.py

Image

在 Ubuntu 上安装 Python pip

要在 Ubuntu Linux 上安装 pip,你需要系统管理员密码。打开终端并输入以下命令(确保根据需要更改以下命令中的版本号):

sudo apt install python3.10-distutils
sudo apt install curl
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.10 get-pip.py

使用前两个命令时,可能会出现已经安装的错误信息;这个错误可以安全忽略。

Image

在 Raspberry Pi 上安装 Python pip

要在 Raspberry Pi 上安装 pip,打开终端并输入以下命令:

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.10 get-pip.py

你可以安全地忽略显示的任何警告信息。

Image

在 macOS 上安装 Python pip

只要你已经安装了 Python 3.10 或更新版本,pip 应该会默认安装。如果你使用的是较旧版本的 Python,可能需要通过打开终端并运行以下命令来安装 pip:

easy_install-3.9 pip

根据你安装的 Python 版本,可能需要输入不同版本的 easy_install(例如,easy_install-3.7)。

Image

尝试 PyGame

要了解PyGame的工作原理,首先使用 pip 进行安装。打开命令提示符(在 Windows 上,搜索框中输入 cmd;在 Ubuntu、Raspberry Pi 或 macOS 上,搜索终端),然后输入以下命令(你的版本号可能不同):

pip3.10 install pygame

注意

根据你的 Windows 和 Python 版本,这可能无法正常工作。如果你遇到如下错误:

'pip' 不是内部或外部命令,

可操作的程序或批处理文件。

那么尝试运行以下命令:

cd %HOMEPATH%
AppData\Local\Programs\Python\Python310\python -m pip install pygame

使用 PyGame 编写代码比使用 tkinter 稍微复杂。例如,在第十章中,我们通过使用 tkinter 并运行以下代码显示了图像:

from tkinter import *
tk = Tk()
canvas = Canvas(tk, width=400, height=400)
canvas.pack()
myimage = PhotoImage(file='c:\\Users\\jason\\test.gif')
canvas.create_image(0, 0, anchor=NW, image=myimage)

要使用 PyGame 显示图像,可以使用以下代码:

import pygame
pygame.init()
display = pygame.display.set_mode((500, 500))
img = pygame.image.load('c:\\Users\\jason\\test.gif')
display.blit(img, (0, 0))
pygame.display.flip()

导入 pygame 模块后,我们调用 init 函数;这会初始化该模块,以便可以使用。然后我们设置显示,传递一个元组用于宽度和高度(宽 500 像素,高 500 像素)。注意,这里的额外括号非常重要:首先是函数本身的括号(set_mode(...)),然后是元组的括号(500, 500)。

然后我们加载图像,其引用(或标签)是变量 img(记住,根据你使用的操作系统,可能需要更改该图像的路径)。接下来,我们使用 blit 函数将图像绘制到显示器上,传递 img 变量和一个包含图像左上角位置的元组(0, 0)。图像暂时不会显示在窗口中;最后一行中的下一个函数——我们翻转显示——实际上会导致图像出现。这一行有效地告诉 Pygame 重绘显示窗口。

注意

如果你在 IDLE 外运行这段代码,你需要在最后添加几行额外的代码:

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            raise SystemExit

这段代码是为了防止窗口在显示图像后立即关闭。

其他游戏和图形编程

如果你想在游戏或图形编程方面做得更多,你会发现有许多选择,不仅仅是 Python。以下是一些例子:

  • Scratch (scratch.mit.edu),一个用于开发游戏和动画的工具。(它是基于块的可视化编程语言,因此与使用 Python 编程有很大不同。)

  • Construct3 (www.construct.net),一个浏览器中创建游戏的商业工具。

  • Game Maker Studio (www.yoyogames.com),另一个商业工具,用于创建游戏。

  • Godot (godotengine.org),一个免费的游戏引擎,用于创建 2D 和 3D 图形。

  • Unity (unity.com),另一个创建游戏的商业工具。

  • Unreal Engine (www.unrealengine.com),另一个创建游戏的商业工具。

在线搜索将揭示大量资源,帮助你入门这些选项,或者至少展示如果你将来继续编程,可能会实现的事情。

其他编程语言

如果你对其他编程语言感兴趣,可以考虑了解一些最受欢迎的语言:JavaScript、Java、C#、C、C++、Ruby、Go、Rust 和 Swift(尽管还有很多其他语言)。我们将简要介绍这些语言,并看看每种语言中的 Hello World 程序(就像我们在 第一章 中开始的 Python 版本)是如何写的。请注意,这些语言并非专门为初学者设计,大多数与 Python 有显著不同。

JavaScript

JavaScript (dev.java/) 是一种常用于网页、游戏编程和其他活动的编程语言。你可以轻松地创建一个简单的 HTML 页面(用于创建网页的语言),其中包含一个 JavaScript 程序,并在浏览器中运行它,而无需使用 shell、命令行或其他工具。

在 JavaScript 中,打印“Hello World”的示例会根据你是在浏览器还是 shell 中运行它而有所不同。在 shell 中,你可以键入以下内容:

print('Hello World');

在浏览器中,它可能看起来像这样:

<html>
    <body>
        <script type="text/javascript">
            alert("Hello World");
        </script>
 </body>
</html>

Java

Java (www.oracle.com/technetwork/java/index.html) 是一种中等复杂的编程语言,拥有一个庞大的内置模块库(称为 packages)。Java 是 Android 移动设备上使用的编程语言,你可以在大多数操作系统上使用它。

这是在 Java 中打印“Hello World”的示例:

public class HelloWorld {
    public static final void main(String[] args) {
        System.out.println("Hello World");
    }
}

C#

C# (docs.microsoft.com/en-us/dotnet/csharp/programming-guide),发音为“C sharp”,是一种适用于 Windows 的中等复杂的编程语言,语法上与 Java 和 JavaScript 非常相似。它是 Microsoft .NET 平台的一部分。

这是在 C# 中打印“Hello World”的示例:

public class Hello
{
   public static void Main()
   {
      System.Console.WriteLine("Hello World");
   }
}

C/C++

C (www.cprogramming.com) 和 C++ (www.stroustrup.com/C++.html) 是复杂的编程语言,适用于所有操作系统。你会发现这两种语言都有免费的版本和商业版本。两者(尽管 C++ 比 C 更加如此)都有陡峭的学习曲线(换句话说,它们不一定适合初学者)。例如,你会发现你需要手动编写 Python 提供的一些功能(比如告诉计算机你需要使用一块内存来存储一个对象)。许多商业游戏和游戏主机都是使用某种形式的 C 或 C++ 编写的。

在 C 中打印“Hello World”的示例如下:

#include <stdio.h>
int main ()
{
  printf ("Hello World\n");
}

C++ 中的示例可能是这样的:

#include <iostream>
int main()
{
  std::cout << "Hello World\n";
  return 0;
}

Ruby

Ruby (www.ruby-lang.org) 是一种免费的编程语言,适用于所有主要的操作系统。它主要用于创建网站,特别是使用 Ruby on Rails 框架。(框架 是一组支持开发特定类型应用程序的库。)

这是一个使用 Ruby 打印“Hello World”的示例:

puts "Hello World"

Go

Go (golang.org) 是一种类似于 C 的编程语言,但稍微简单一些。

你可以通过以下方式使用 Go 打印“Hello World”:

package main
import "fmt"
func main() {
 fmt.Println("Hello World")
}

Rust

Rust (www.rust-lang.org) 是一种最初由 Mozilla Research(开发 Firefox 浏览器的团队)开发的语言。

一个简单的 Rust 程序来打印“Hello World”可能像这样:

fn main() {
    println!("Hello World")
}

Swift

Swift (swift.org) 是 Apple 为其设备(iOS、macOS 等)开发的语言,因此如果你使用 Apple 产品,它最为合适。

我们可以像这样用 Swift 打印“Hello World”:

import Swift
print("Hello World")

结束语

无论你选择继续使用 Python 还是尝试其他编程语言,你都应该会发现本书中介绍的概念对你有帮助。即使你不再从事计算机编程,理解一些基本概念也能帮助你进行各种活动,无论是在学校还是以后工作中。

祝你好运,享受编程的乐趣!

图片

第十八章:A

Python 关键字

图片

在 Python(以及大多数编程语言)中,关键字是具有特殊意义的单词。它们是编程语言本身的一部分,因此不能用于其他任何用途。例如,如果你试图将关键字用作变量,或以错误的方式使用它们,Python 控制台会显示奇怪的错误信息。本附录描述了每个 Python 关键字。你可以将它作为一个方便的参考,继续编程时使用。

and

关键字and用于在语句中将两个表达式连接在一起(例如 if 语句),表示这两个表达式都必须为真。以下是一个示例:

if age > 12 and age < 20:
    print('Beware the teenager!!!!')

这段代码意味着变量 age 的值必须大于 12 且小于 20,消息才会被打印出来。

as

关键字as用于为导入的模块指定一个别名。例如,假设你有一个非常长的模块名称:

i_am_a_python_module_that_is_not_very_useful.

每次使用该模块时都需要输入这个模块名称会非常烦人:

>>> import i_am_a_python_module_that_is_not_very_useful
>>> i_am_a_python_module_that_is_not_very_useful.do_something()

I have done something that is not useful.
>>> i_am_a_python_module_that_is_not_very_useful.do_something_else()

I have done something else that is not useful!!

相反,你可以在导入模块时给它一个新的、更短的名字,然后只需使用这个新名字(就像昵称一样),如下所示:

>>> import i_am_a_python_module_that_is_not_very_useful as notuseful
>>> notuseful.do_something()

I have done something that is not useful.
>>> notuseful.do_something_else()

I have done something else that is not useful!!

Assert

assert关键字用于表示某个值必须为真。这是捕获代码中的错误和问题的另一种方式,通常用于更高级的程序中(这也是我们在Python for Kids中不使用assert的原因)。以下是一个简单的 assert 语句:

>>> mynumber = 10
>>> assert mynumber < 5
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    assert mynumber < 5
AssertionError

在这个示例中,我们断言变量 mynumber 的值小于 5。它并不是,所以 Python 会显示一个错误(称为 AssertionError)。

ASYNC

async关键字用于定义一种叫做原生协程的东西。这是异步编程中的一个高级概念(即并行做多件事,或者做一些延迟的事情)。

Await

await关键字也用于异步编程(与async类似)。

Break

break关键字用于停止某段代码的执行。你可以在 for 循环中使用break,像这样:

age = 10
for x in range(1, 100):
    print(f'counting {x}')
    if x == age:
        print('end counting')
        break

由于变量 age 在这里被设置为 10,代码将打印出以下内容:

counting 1
counting 2
counting 3
counting 4
counting 5
counting 6
counting 7
counting 8
counting 9
counting 10
end counting

一旦变量 x 的值达到 10,代码将打印文本“end counting”,然后跳出循环。

Class

class关键字用于定义一种对象类型,比如车辆、动物或人类。类可以有一个叫做__init__的函数,用于执行对象创建时所需的所有任务。例如,创建 Car 类的对象时可能需要一个颜色变量:

>>> class Car:
        def __init__(self, color):
            self.color = color

>>> car1 = Car('red')
>>> car2 = Car('blue')
>>> print(car1.color)
red
>>> print(car2.color)
blue

Continue

continue关键字是“跳到”循环中的下一个项,这样循环块中的其余代码就不会被执行。与break不同,我们不是跳出循环——我们只是继续执行下一个项。例如,如果我们有一个物品列表,并且希望跳过以b开头的项,可以使用以下代码:

>>> my_items = ['apple', 'aardvark', 'banana', 'badger',
        'clementine', 'camel']
>>> for item in my_items:
        if item.startswith('b'):
            continue
        print(item)

apple
aardvark
clementine
camel

我们首先创建我们的项目清单,然后使用for循环遍历这些项目,并为每个项目执行一段代码。如果项目以字母b开头,我们会跳过该项目。否则,我们打印出该项目。

定义

def关键字用于定义一个函数。例如,我们可以创建一个函数,将一段年份转化为等效的分钟数:

>>> def minutes(years):
        return years * 365 * 24 * 60
>>> minutes(10)
5256000

删除

del关键字用于删除某个东西。例如,如果你在日记中有一个生日愿望清单,但后来改变了对某个愿望的想法,你可能会把它从清单中划掉,并添加一个新的愿望:

图片

在 Python 中,原始列表看起来像这样:

what_i_want = ['remote controlled car', 'new bike', 'computer game']

你可以通过使用del和你想删除的项目的索引来移除计算机游戏。然后,你可以通过append函数添加新项目:

del what_i_want[2]
what_i_want.append('roboreptile')

然后打印出新的列表:

print(what_i_want)
['remote controlled car', 'new bike', 'roboreptile']

否则

elif关键字作为if语句的一部分使用。参见if关键字的描述,了解示例。

否则

else关键字作为if语句的一部分使用。参见if关键字的描述,了解示例。

除外

except关键字用于捕捉在比较复杂的代码中可能出现的问题。

最后

finally关键字用于确保发生错误时,某些代码能够运行(通常用于清理代码留下的任何残余)。这个关键字用于更高级的编程。

循环

for关键字用于创建一个执行特定次数的代码循环。这里有一个例子:

for x in range(0, 5):
    print(f'x is {x}')

这个for循环执行代码块(打印语句)五次,结果如下输出:

x is 0
x is 1
x is 2
x is 3
x is 4

导入模块时,你可以使用from关键字仅导入你需要的部分。例如,第四章介绍的turtle模块有一个叫做Turtle的类,我们使用它来创建一个 Turtle 对象(该对象包括海龟移动的画布)。下面是如何导入整个turtle模块并使用Turtle类:

import turtle
t = turtle.Turtle()

你也可以单独导入 Turtle 类,然后直接使用它(完全不需要引用 turtle 模块):

from turtle import Turtle
t = Turtle()

你可能这么做是为了,下次查看该程序顶部时,可以看到你正在使用的所有函数和类(这在导入许多模块的较大程序中特别有用)。然而,如果你选择这么做,你将无法使用你没有导入的模块部分。例如,time模块有localtimegmtime函数,但如果你只导入了localtime,然后尝试使用gmtime,你会得到一个错误:

>>> from time import localtime
>>> print(localtime())
(2019, 1, 30, 20, 53, 42, 1, 30, 0)
>>> print(gmtime())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'gmtime' is not defined

错误信息“gmtime未定义”意味着 Python 对gmtime函数一无所知,这是因为你没有导入它。

如果一个特定模块有多个你想使用的函数,而你不想通过模块名来引用它们(例如,time.localtime,或 time.gmtime),你可以使用星号(*)导入模块中的所有内容,像这样:

>>> from time import *
>>> print(localtime())
(2021, 1, 30, 20, 57, 7, 1, 30, 0)
>>> print(gmtime())
(2021, 1, 30, 13, 57, 9, 1, 30, 0)

这种形式从 time 模块中导入了所有内容,你现在可以按名称引用各个函数。

全局

程序中作用域的概念在第七章中介绍。作用域是指变量的可见性。如果一个变量在函数外部定义,通常可以在函数内部看到(它是可见的)。另一方面,如果变量在函数内部定义,通常无法在函数外部看到。global关键字是此规则的一个例外。定义为全局变量的变量可以在任何地方看到。下面是一个示例:

>>> def test():
        global a
        a = 1
        b = 2

你认为当你调用print(a)然后运行print(b)时会发生什么?第一个会正常工作,但第二个会显示一个错误消息:

>>> test()
>>> print(a)
1
>>> print(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined

变量 a 已经被更改为在函数内部具有全局作用域,因此它在函数执行完毕后仍然可见;但变量 b 仍然仅在函数内部可见。(在设置变量值之前,必须使用global关键字。)

如果

if关键字用于对某件事做出决策。它也可以与elseelif(else if)一起使用。一个 if 语句就是在说:“如果某个条件为真,那么执行某个操作。”下面是一个示例:

if toy_price > 1000:
    print('That toy is overpriced')
elif toy_price > 100:
    print('That toy is expensive')
else:
    print('I can afford that toy')

这个 if 语句表示,如果玩具价格超过$1,000,显示一条消息说它被高价出售;否则,如果玩具价格超过$100,显示一条消息说它很贵。如果这两个条件都不成立,则应该显示消息:“我能买得起这个玩具。”

导入

import关键字告诉 Python 加载一个模块,以便可以使用。例如,以下代码告诉 Python 使用sys模块:

import sys

in关键字用于表达式中,查看某个项目是否在一组项目中。例如,数字 1 是否可以在一个数字列表(一个集合)中找到?

>>> if 1 in [1,2,3,4]:
        print('number is in the list')
number is in the list

以下是如何判断字符串’pants’是否在服装列表中的示例:

>>> clothing_list = ['shorts', 'undies', 'boxers', 'long johns',
                 'knickers']
>>> if 'pants' in clothing_list:
        print('pants is in the list')
    else:
        print('where are my pants?')
where are my pants?

is关键字有点像等于操作符(==),用于判断两个东西是否相等(例如,10 == 10 为真,10 == 11 为假)。然而,is==之间有一个根本的区别。如果你在比较两个对象(如列表),==可能返回真,而is则可能不返回真(即使你认为这些对象是相同的)。这是一个高级编程概念。

Lambda

lambda关键字用于创建匿名或内联函数。这个关键字用于更高级的程序中。

nonlocal

nonlocal关键字用于将一个变量包含到一个函数的作用域中,当它在函数外部声明时。这个关键字用于更高级的程序中。

not

如果某个条件为真,not关键字使其变为假。例如,如果我们创建一个变量 a 并将其设置为 True 的值,然后使用not打印该变量的值,结果如下:

>>> a = True
>>> print(not a)
False

同样地,对于一个 False 值,我们得到 True:

>>> b = False
>>> print(not b)
True

这看起来似乎不是很有用,直到你开始在 if 语句中使用这个关键字。例如,要检查一个项目是否不在列表中,我们可以写如下代码:

>>> clothing_list = ['shorts', 'undies', 'boxers', 'long johns',
                 'knickers']
>>> if 'pants' not in clothing_list:
        print('You really need to buy some pants')
You really need to buy some pants

或者

or 关键字将两个条件连接在一起,表示至少一个条件应为真。下面是一个示例:

if dino == 'Tyrannosaurus' or dino == 'Allosaurus':
    print('Carnivores')
elif dino == 'Ankylosaurus' or dino == 'Apatosaurus':
    print('Herbivores')

在这种情况下,如果变量 dino 的值是暴龙(Tyrannosaurus)或异特龙(Allosaurus),程序会打印出“食肉动物”。如果是甲龙(Ankylosaurus)或梁龙(Apatosaurus),程序会打印出“食草动物”。

pass

有时候在开发程序时,你可能只想写一些小片段来尝试一些功能。这样做的问题是,如果你没有代码块,不能使用 if 语句来检查条件是否为真;同样,如果没有执行体,你也不能使用 for 循环。例如,以下代码是可以正常工作的:

>>> age = 15
>>> if age > 10:
        print('older than 10')
older than 10

但是如果你没有为 if 语句提供代码块(主体),你会得到一个错误信息:

>>> age = 15
>>> if age > 10:
File "<stdin>", line 2
    ^
IndentationError: expected an indented block after 'if' statement on line 1

这是 Python 显示的错误信息,当你在某些语句后面缺少代码块时(如果你使用的是 IDLE,它甚至不允许你输入这样的代码)。在这种情况下,你可以使用 pass 关键字写一个语句,但不提供与之对应的代码块。

举个例子,假设你想在一个 for 循环中使用 if 语句。也许你还没决定在 if 语句中写什么——或许你会使用 print 函数,或者加入 break,或者其他什么操作。此时,你可以使用 pass,代码仍然能正常工作(即使它还没完全做你想做的事)。以下是我们再次使用 pass 关键字的 if 语句:

>>> age = 15
>>> if age > 10:
        pass

以下代码展示了 pass 关键字的另一种用法:

>>> for x in range(0, 7):
>>>     print(f'x is {x}')
>>>     if x == 4:
            pass

x is 0
x is 1
x is 2
x is 3
x is 4
x is 5
x is 6

每次 Python 执行循环体中的代码时,它仍然会检查 x 变量是否等于 4,但因为没有提供其他操作,它什么也不做,所以它会打印出从 0 到 7 的每一个数字。稍后,你可以在 if 语句的代码块中添加其他代码,将 pass 关键字替换为其他内容,比如 break:

>>> for x in range(0, 7):
        print(f'x is {x}')
        if x == 4:
            break

x is 1
x is 2
x is 3
x is 4

pass 关键字最常见的用法是,在创建函数时,如果你还不想写出函数的代码,可以先使用它。

raise

raise 关键字可以用来触发一个错误。听起来可能有点奇怪,但在高级编程中,它实际上非常有用。

return

return 关键字用于从函数中返回一个值。例如,你可以创建一个函数来计算你到上一个生日为止活了多少秒:

def age_in_seconds(age_in_years):
    return age_in_years * 365 * 24 * 60 * 60

当你调用这个函数时,返回的值可以赋值给另一个变量,或者可以直接打印出来:

>>> seconds = age_in_seconds(9)
>>> print(seconds)
283824000
>>> print(age_in_seconds(12))
378432000

try

try 关键字开始一个代码块,这个块以 except 和 finally 关键字结束。try/except/finally 代码块一起用于处理程序中的错误,例如确保程序向用户显示有用的消息,而不是一个难懂的 Python 错误。它们在高级程序中非常有用。

while

while 关键字有点像 for,不过 for 循环遍历一个范围(数字),而 while 循环在某个表达式为真时会一直运行。使用 while 循环时要小心——如果表达式始终为真,循环将永远不会结束(这叫做 无限循环)。以下是一个例子:

>>> x = 1
>>> while x == 1:
        print('hello')

如果你运行这段代码,它会永远循环下去,或者至少直到你关闭 Python Shell 或按下 CTRL-C 来中断它。然而,以下代码会打印出“hello”九次(每次将变量 x 加 1,直到 x 不再小于 10):

>>> x = 1
>>> while x < 10:
        print('hello')
        x = x + 1

with

with 关键字与一种特殊的对象一起使用,用来创建一个代码块,类似于 try 和 finally 关键字,然后管理该对象的资源。这个关键字通常用于高级程序中。

yield

yield 关键字有点像 return,但它与一种叫做 生成器 的特定对象一起使用。生成器根据请求生成值,因此在这方面,range 函数的行为像一个生成器。

第十九章:B

Python 的内建函数

Image

Python 拥有丰富的编程工具箱,包括大量现成的函数和模块,供你使用。这些内建工具可以让编写程序变得更加轻松。

正如你在第七章中看到的,模块需要先导入才能使用。Python 的内建函数无需先导入;它们在 Python Shell 启动时就已经可以使用了。在本附录中,我们将查看一些有用的内建函数,并重点介绍其中一个:open 函数,它允许你打开文件进行读写。

使用内建函数

让我们来看一些 Python 程序员常用的内建函数。我将描述它们的功能和用法,然后展示它们如何在你的程序中发挥作用。

abs 函数

abs 函数返回一个数字的绝对值,即数字的值去掉符号后的结果。例如,10 的绝对值是 10,–10 的绝对值也是 10。

要使用 abs 函数,只需调用它并将一个数字或变量作为参数,像这样:

>>> print(abs(10))
10
>>> print(abs(-10))
10

你可能会使用 abs 函数来计算游戏中角色的移动的绝对值,不管角色是向哪个方向移动。例如,假设角色向左走了 3 步(负 3 或–3),然后向右走了 10 步(正 10)。

如果我们不关心方向(正数或负数),那么这些数字的绝对值就是 3 和 10。你可能会在一个棋盘游戏中用到这个,其中你掷两个骰子,然后根据骰子的总数决定角色最大可以朝任意方向移动多少步。现在,如果我们将步数存储在一个变量中,我们就可以使用以下代码判断角色是否在移动。我们可能希望在玩家决定移动时显示一些信息(在这个例子中,我们将根据步数显示“角色正在移动很远”或“角色正在移动”):

Image

>>> steps = -3
>>> if abs(steps) > 5:
        print('Character is moving far')
    elif abs(steps) != 0:
        print('Character is moving')

如果没有使用 abs,if 语句可能看起来是这样的:

>>> steps = 10
>>> if steps < -5 or steps > 5:
        print('Character is moving far')
    elif steps != 0:
        print('Character is moving')

如你所见,使用 abs 使得 if 语句更简洁易懂。

all 函数

all 函数如果列表(或任何其他类型的集合)中的所有项都为 True,则返回 True。简单来说,这意味着列表中所有项的值都不是 0、None、空字符串('')或布尔值 False。

所以,如果列表中的所有项都是非零数字,all 会返回 True:

>>> mylist = [1,2,5,6]
>>> all(mylist)
True

但如果任何值为 0,它将返回 False:

>>> mylist = [1, 2, 3, 0]
>>> all(mylist)
False

不仅仅是数字—包含 None 的混合列表也会返回 False:

>>> mylist = [100, 'a', None, 'b', True, 'zzz', ' ']
>>> all(mylist)
False

如果去掉 None,我们再试一次同样的例子:

>>> mylist = [100, 'a', 'b', True, 'zzz', ' ']
>>> all(mylist)
True

any 函数

any 函数与 all 函数类似,不同之处在于如果任何一个值的结果为 True,它就会返回 True。让我们用相同的例子来测试这些数字:

>>> mylist = [1, 2, 5, 6]
>>> any(mylist)
True

我们的混合列表包含零、None、空字符串和 False,它的表现和 all 一样:

>>> mylist = [0, False, None, "", 0, False, '']
>>> any(mylist)
False

但是,如果我们对列表做一个小的改动——比如添加一个非零数字如 100——我们就会得到 True 了:

>>> mylist = [0, False, None, "", 0, False, '', 100]
>>> any(mylist)
True

bin 函数

bin 函数将数字转换为 二进制表示。二进制超出了本书的范围,但简而言之,它是由 1 和 0 组成的计数系统,是计算机中几乎一切的基础。以下是一个简单的例子,将一些数字转换为二进制:

>>> bin(100)
'0b1100100'
>>> bin(5)
'0b101'

bool 函数

bool 的名字是 Boolean(布尔型)的缩写,程序员用这个词来描述一种数据类型,它只能拥有两种可能的值:通常是 True 或 False。

bool 函数接受一个参数,并根据其值返回 True 或 False。当对数字使用 bool 时,0 返回 False,但其他任何数字返回 True。下面是你可能如何对不同数字使用 bool 的示例:

>>> print(bool(0))
False
>>> print(bool(1))
True
>>> print(bool(1123.23))
True
>>> print(bool(-500))
True

当你将 bool 用于其他值时,比如字符串,如果字符串没有值(换句话说,值是 None 或空字符串),则返回 False。否则,返回 True,如下所示:

>>> print(bool(None))
False
>>> print(bool('a'))
True
>>> print(bool(' '))
True
>>> print(bool('What do you call a pig doing karate? Pork Chop!'))
True

bool 函数对于不包含任何值的列表、元组和映射将返回 False,当它们包含值时则返回 True:

>>> my_silly_list = []
>>> print(bool(my_silly_list))
False
>>> my_silly_list = ['s', 'i', 'l', 'l', 'y']
>>> print(bool(my_silly_list))
True

当你需要决定某个值是否已设置时,可能会使用 bool。例如,如果我们要求使用我们程序的用户输入他们的出生年份,我们的 if 语句可以使用 bool 来测试他们输入的值:

>>> year = input('Year of birth: ')
Year of birth:
>>> if not bool(year):
        print('You need to enter a value for your year of birth')

You need to enter a value for your year of birth

本例的第一行使用 input 函数将用户输入的内容存储为变量 year。在下一行按下 ENTER(不输入任何其他内容)会使变量 year 存储一个空字符串。(我们在 第七章 中也使用过 sys.stdin.readline(),这是另一种做法。)

Image

在下一行中,if 语句检查变量的布尔值。由于用户在本例中没有输入任何内容,bool 函数返回 False。if 语句使用了 not 关键字,它的意思是“如果函数没有返回 True,就执行这个”,所以代码在下一行输出:你需要输入你的出生年份。

可调用函数

可调用函数仅仅告诉你某个对象是否是函数(换句话说,它能否被调用?)。以下代码返回 False……

>>> callable('peas')
False

……因为字符串 'peas' 不是一个函数。但以下代码将返回 True……

>>> callable(bin)
True

……因为 bin 是一个函数。以下代码也将返回 True:

>>> class People:
        def run(self):
            print('running')

>>> callable(People.run)
True

People 类有一个名为 run 的函数。如果我们检查该类函数是否可调用(它是可调用的),我们会得到 True。此外,如果我们创建一个该类的对象,然后检查该对象的函数(p.run)是否可调用,再次得到 True:

>>> p = People()
>>> callable(p.run)
True

chr 函数

你在 Python 中输入的每个字符都有一个底层的数字编码来标识它。例如,字符’a’的数字值是 97。大写字母’A’的数字值是 65。chr 函数接受一个数字参数并返回相应的字符。所以我们可以试试值 97 和 65:

>>> chr(97)
'a'
>>> chr(65)
'A'

我们可以试试更多随机数字,比如 22283,它是汉字字符集中的一个字符:

>>> chr(22283)

或者 949,这是希腊字母epsilon

>>> chr(949)
'ε'

或者 8595,这其实并不是一个字符——它是一个指向下方的箭头:

>>> chr(8595)
'↓'

dir 函数

dir 函数(directory的简称)返回关于任何值的信息。基本上,它告诉你可以与该值一起使用的函数,按字母顺序排列。

例如,要显示列表值可用的函数,请输入以下内容:

>>> dir(['a', 'short', 'list'])
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__',
'__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__',
'__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__',
'__sizeof__', '__str__', '__subclasshook__', 'append', 'count',
'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

dir 函数几乎可以用于任何东西,包括字符串、数字、函数、模块、对象和类。但有时它返回的信息可能并不十分有用。例如,如果你对数字 1 调用 dir,它会显示一堆特殊函数(那些前后都带有下划线的函数),这些函数是 Python 本身使用的,这其实并不实用(你通常可以忽略它们中的大部分):

>>> dir(1)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', 
'__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', 
'__getnewargs__', '__gt__', '__hash__', '__index__', '__init__',
'__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__',
'__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', 
'__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', 
'__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', 
'__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', 
'__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', 
'__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 
'as_integer_ratio', 'bit_count', 'bit_length',
'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator',
'real', 'to_bytes']

dir 函数在你有一个变量并希望快速了解可以对它执行什么操作时非常有用。例如,运行 dir 来查看包含字符串值的变量 popcorn,你会得到字符串类提供的函数列表(所有字符串都是字符串类的成员):

>>> popcorn = 'I love popcorn!'
>>> dir(popcorn)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
'__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', 
'__rmul__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 
'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index',
'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 
'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 
'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition',
'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 
'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith',
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

此时,你可以使用 help 来获取列表中任何函数的简短描述。以下是运行 help 获取上面函数的示例:

>>> help(popcorn.upper)
Help on built-in function upper:

upper() method of builtins.str instance
    Return a copy of the string converted to uppercase.

返回的信息可能有点让人困惑,所以我们来仔细看看。第一行告诉你 upper 是一个字符串实例(对象)的内置函数。第二行则告诉你它的作用是什么(返回字符串的大写副本)。

divmod 函数

辅助函数 divmod 接受两个参数(两个数字,分别表示被除数和除数),然后返回这两个数字相除的结果,以及这两个数字执行模除操作的结果。除法是计算一个数字能被分成多少个第二个数字的部分的数学运算。例如,我们可以将六个球分成两球一组多少次?

图片

答案是:我们可以将其除三次。

图片

模除操作几乎是相同的,除了模除返回的是除法后的余数。所以,以上将六个球除以二的结果是零(因为没有剩余)。如果我们再加一个球呢?如果我们将七个球分成两组,除法的结果仍然是三,但会剩下一个球。

图片

这就是divmod作为一个包含两个数字的元组返回的结果:除法的结果和取模操作的结果。让我们先试试 6 和 2:

>>> divmod(6, 2)
(3, 0)

然后是 7 和 2:

>>> divmod(7, 2)
(3, 1)

eval函数

eval函数(即求值的缩写)将一个字符串作为参数,并像运行 Python 表达式一样执行它。例如,eval('print("wow")')将实际执行语句print("wow")

eval函数仅适用于简单的表达式,例如以下内容:

>>> eval('10*5')
50

跨越多行的表达式(如if语句)通常不会求值,如下例所示:

>>> eval('''if True:
        print("this won't work at all")''')

Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    eval('''if True:
  File "<string>", line 1
    if True:
    ^^
SyntaxError: invalid syntax

eval函数通常用于将用户输入转换为 Python 表达式。例如,你可以编写一个简单的计算器程序,该程序读取输入到 Python 中的方程式并计算(求值)答案。

图片

因为用户输入作为字符串读取,Python 需要将其转换为数字和运算符,才能进行任何计算。eval函数使这种转换变得简单:

>>> your_calculation = input('Enter a calculation: ')

Enter a calculation: 12*52
>>> eval(your_calculation)
624

在这个示例中,我们使用input函数来读取用户输入到your_calculation变量中。在下一行,我们输入表达式 12*52。我们使用eval来运行这个计算,结果将在最后一行打印出来。

exec函数

exec函数类似于eval,只是你可以用它来运行更复杂的程序。虽然eval返回一个值(你可以将其保存在变量中),但exec不会返回值。以下是一个示例:

>>> my_small_program = '''print('ham')
print('sandwich')'''
>>> exec(my_small_program)
ham
sandwich

在前两行中,我们创建了一个包含两个打印语句的多行字符串变量,然后使用exec来运行这个字符串。

你可以使用exec来运行 Python 程序从文件中读取的迷你程序——程序中的程序!这在编写长且复杂的应用程序时非常有用。例如,你可以创建一个决斗机器人游戏,在这个游戏中,两台机器人在屏幕上移动并试图相互攻击。玩家为自己的机器人提供迷你 Python 程序作为指令。决斗机器人游戏将读取这些脚本并使用exec来运行。

float函数

float函数将一个字符串或数字转换为浮点数,即带有小数点的数字(也称为实数浮动数)。例如,数字 10 是一个整数(也称为整数),但 10.0、10.1 和 10.253 都是浮点数。如果你在编写一个简单的程序来计算货币金额,可能会使用浮点数。浮点数在图形程序(如 3D 游戏)中也很常用,用来计算如何以及在哪里在屏幕上绘制物体。

图片

你可以通过调用float函数将一个字符串转换为浮点数:

>>> float('12')
12.0

你也可以在字符串中使用小数点:

>>> float('123.456789')
123.456789

你可能会使用 float 来将用户输入的值转换为适当的数字,这在你需要将用户输入的值与其他值进行比较时特别有用。例如,要检查某个人的年龄是否超过某个数字,我们可以这样做:

>>> your_age = input('Enter your age: ')

Enter your age: 20
>>> age = float(your_age)
>>> if age > 13:
        print(f'You are {age - 13} years too old')

You are 7.0 years too old

input 函数

input 函数用于读取使用你程序的人的输入——他们输入的所有内容,直到按下 ENTER 键为止。结果以字符串形式返回供你使用。你可以提示程序的用户输入某些内容,像这样:

>>> s = input('Tell me a play on words:\n')
Tell me a play on words:
A hedgehog went to see a play about a plucky young girl, but left
dis-a-pointed
>>> print(s)
A hedgehog went to see a play about a plucky young girl, but left
dis-a-pointed

或者没有消息:

>>> s = input()
A hedgehog went to see a play about a plucky young girl, but left
dis-a-pointed

在这两种情况下,input 函数的结果是相同的:一个包含文本的字符串。更多关于使用返回值的例子,请参见之前关于 float 函数的部分。

int 函数

int 函数将字符串或数字转换为整数(或整型),这基本上意味着小数点后面的所有内容都会被舍去。例如,下面是如何将一个浮点数转换为普通整数:

>>> int(123.456)
123

这个例子将字符串转换为整数:

>>> int('123')
123

但是,尝试将包含浮点数的字符串转换为整数时,会收到错误消息。例如,在这里我们尝试使用 int 函数将包含浮点数的字符串转换:

>>> int('123.456')
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
    int('123.456')
ValueError: invalid literal for int() with base 10: '123.456'

如你所见,结果是一个 ValueError 错误消息。

len 函数

Image

len 函数返回一个对象的长度,或者在字符串的情况下,返回字符串中的字符数。例如,要获取“this is a test string”的长度,可以这样做:

>>> len('this is a test string')
21

当与列表或元组一起使用时,len 会返回该列表或元组中的项目数量:

>>> creature_list = ['unicorn', 'cyclops', 'fairy', 'elf', 'dragon', 
                     'troll']
>>> print(len(creature_list))
6

与 dict(或字典)一起使用时,len 也会返回字典中的项目数量:

>>> enemies = {'Batman' : 'Joker',
               'Superman' : 'Lex Luthor', 
               'Spiderman' : 'Green Goblin'}
>>> print(len(enemies))
3

len 函数在处理循环时特别有用。例如,我们可以用它来显示列表中元素的索引位置,如下所示:

>>> fruit = ['apple', 'banana', 'clementine', 'dragon fruit']
>>> length = len(fruit)
>>> for x in range(0, length):
        print(f'the fruit at index {x} is {fruit[x]}')

the fruit at index 0 is apple
the fruit at index 1 is banana
the fruit at index 2 is clementine
the fruit at index 3 is dragon fruit

首先,我们将列表的长度存储在变量 length 中,然后使用该变量在 range 函数中创建我们的循环。当我们循环遍历列表中的每一项时,我们会打印一条显示项的索引位置和值的消息。如果你有一个字符串列表,并且想打印列表中的每第二个或第三个项目,也可以使用 len 函数。

list 函数

如果你调用没有任何参数的 list,你将得到一个空的列表对象作为响应。在这一点上,list() 和使用方括号没有区别。我们可以通过测试这两个列表是否相等(==)来检查这是否确实如此。

>>> l1 = list()
>>> l2 = []
>>> l1 == l2
True

虽然这看起来并不是特别有用,但列表也可以用来将某些类型的 Python 对象(称为可迭代对象)转换为列表。最简单的例子是使用 range 函数(该函数在第 317 页中描述)与列表配合使用:

>>> list(range(0, 10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

max 和 min 函数

max 函数返回列表、元组或字符串中的最大项。例如,下面是如何在数字列表中使用它:

>>> numbers = [5, 4, 10, 30, 22]
>>> print(max(numbers))
30

你也可以完全一样地操作字符串,或者字符串列表:

Image

>>> strings = 'stringSTRING'
>>> print(max(strings))
t
>>> strings = ['s', 't', 'r', 'i', 'n', 'g', 'S', 'T', 'R', 'I', 'N', 'G']
>>> print(max(strings))
t

字母按字母顺序排列,但小写字母在大写字母之后,所以 t 比 T 大。但你不必仅仅使用列表、元组或字符串。你还可以直接调用 max 函数,并将你想要比较的项作为参数输入括号中:

>>> print(max(10, 300, 450, 50, 90))
450

min 函数的作用与 max 函数类似,不同的是它返回列表、元组或字符串中的最小项。以下是我们使用 min 而不是 max 的数字列表示例:

>>> numbers = [5, 4, 10, 30, 22]
>>> print(min(numbers))
4

假设你正在和四个玩家一起玩猜数字游戏,每个玩家都必须猜测一个比你数字小的数字。如果有任何玩家猜测的数字超过你的数字,所有玩家都输了;如果他们都猜得更小,他们就赢了。我们可以使用 max 快速检查是否有任何猜测数字过大,如下所示:

>>> guess_this_number = 61
>>> player_guesses = [12, 15, 70, 45]
>>> if max(player_guesses) > guess_this_number:
        print('Boom! You all lose')
    else:
        print('You win')

Boom! You all lose

在这个示例中,我们通过变量 guess_this_number 存储要猜测的数字。队员们的猜测存储在列表 player_guesses 中。if 语句检查最大猜测值是否与 guess_this_number 中的数字相符,如果有任何玩家猜测的数字超过了该数字,我们就打印消息“Boom! 你们都输了。”

ord 函数

ord 函数基本上是 chr 函数的反操作:chr 将数字转换为字符,而 ord 告诉你一个字符的数字编码。以下是一些示例:

>>> ord('a')
97
>>> ord('A')
65
>>> ord('![Image](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/py-kid-2e/img/316fig01.jpg)')
22283

pow 函数

pow 函数接受两个数字并计算一个数字(我们称之为 x)到另一个数字(我们称之为 y)的幂。基本上,pow 会将 x 乘以它自身 y 次。例如,2 的 3 次方(在数学术语中这是 2³)就是 2 * 2 * 2(或在数学符号中表示为 2 × 2 × 2),结果是 8(2 * 2 是 4,4 * 2 是 8)。另一个例子:3 的 3 次方(3³)是 27。让我们看看这在代码中的表现:

>>> pow(2, 3)
8
>>> pow(3, 3)
27

range 函数

range 函数主要用于 for 循环中,以特定次数循环执行某段代码。传递给 range 的前两个参数被称为 startstop。你在之前使用 len 函数与循环配合的示例中已经看过带有这两个参数的 range。

Image

range 生成的数字从第一个参数给定的数字开始,到第二个参数减去 1 的数字结束。例如,以下代码展示了当我们打印 range 创建的 0 到 5 之间的数字时发生了什么:

>>> for x in range(0, 5):
        print(x)

0
1
2
3
4

range 函数实际上返回一个特殊的对象,称为 迭代器,它会重复某个操作若干次。在这种情况下,每次调用它时,它都会返回下一个较大的数字。

你可以通过使用 list 函数将迭代器转换为列表。如果你在调用 range 时打印返回值,你也会看到它包含的数字:

>>> print(list(range(0, 5)))
[0, 1, 2, 3, 4]

你还可以向 range 函数添加一个第三个参数,称为步长(step)。如果未包含步长值,则默认步长为 1。但是当我们传入数字 2 作为步长时会发生什么呢?以下是结果:

>>> count_by_twos = list(range(0, 30, 2))
>>> print(count_by_twos)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]

列表中的每个数字都是前一个数字加 2,列表以数字 28 结尾,它比 30 少 2。你也可以使用负步长:

>>> count_down_by_twos = list(range(40, 10, -2))
>>> print(count_down_by_twos)
[40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 20, 18, 16, 14, 12]

sum 函数

sum 函数将列表中的项加总并返回总和。以下是一个示例:

>>> my_list_of_numbers = list(range(0, 500, 50))
>>> print(my_list_of_numbers)
[0, 50, 100, 150, 200, 250, 300, 350, 400, 450]
>>> print(sum(my_list_of_numbers))
2250

在第一行,我们使用 range 函数创建一个 0 到 500 之间的数字列表,步长为 50。接下来,我们打印该列表以查看结果。最后,将 my_list_of_numbers 变量传递给 sum 函数,并使用 print(sum(my_list_of_numbers)) 输出该列表中所有项的总和,结果为 2250。

在 Python 中打开文件

Python 的内置 open 函数用于打开文件,以便你可以对其执行一些有用的操作(例如显示文件内容)。如何告诉函数打开哪个文件,取决于你的操作系统。请先阅读 Windows 文件的示例,如果你使用的是 Mac 或 Ubuntu 系统,再阅读针对这些系统的部分。首先,在你的主目录下创建一个名为 test.txt 的纯文本文件——在 Windows 上,你可以使用记事本;在 Ubuntu Linux 或 Raspberry Pi 上,使用 TextEditor;在 macOS 上,使用 TextEdit(但在 TextEdit 中,你需要选择 格式使文本格式化)。你可以随意在文件中放入内容。

打开一个 Windows 文件

如果你使用的是 Windows,请输入以下代码以打开 test.txt

>>> test_file = open('c:\\Users\\<your username>\\test.txt')
>>> text = test_file.read()
>>> print(text)
There once was a boy named Marcelo
Who dreamed he ate a marshmallow
He awoke with a start
As his bed fell apart
And he found he was a much rounder fellow

在第一行,我们使用 open 函数,它返回一个文件对象,提供操作文件的功能。我们与 open 函数一起使用的参数是一个字符串,告诉 Python 文件的存放位置。如果你使用的是 Windows 系统,并且将 test.txt 文件保存在 C: 盘的用户文件夹中,那么你需要指定文件的位置为 c:\Users<你的用户名>\test.txt。(不要忘记将 <你的用户名> 替换为你的实际用户名!)

Windows 文件名中的两个反斜杠告诉 Python 反斜杠只是反斜杠本身,而不是某种命令。(正如你在 第三章 中所读到的,反斜杠在 Python 中有特殊意义,尤其是在字符串中。)我们将文件对象保存到 test_file 变量中。

在第二行,我们使用 read 函数,它由文件对象提供,用于读取文件内容并将其存储在 text 变量中。最后,我们打印变量以显示文件的内容。

打开 macOS 文件

如果你使用的是 macOS,你需要在第一行输入一个不同的位置来打开 test.txt,与 Windows 示例中的位置不同。在字符串中使用你保存文本文件时选择的用户名。例如,如果用户名是 sarahwinters,则 open 参数应该像这样:

>>> test_file = open('/Users/sarahwinters/test.txt')

打开 Ubuntu 或 Raspberry Pi 文件

如果你使用的是 Ubuntu Linux 或 Raspberry Pi,你需要在 Windows 示例的第一行输入一个不同的位置来打开 test.txt。使用你保存文本文件时点击的用户名。例如,如果用户名是 jacob,那么打开参数应该像这样:

>>> test_file = open('/home/jacob/test.txt')

写入文件

open 返回的文件对象除了 read 外,还有其他函数。我们可以通过在调用 open 时使用第二个参数——字符串 ’w’——来创建一个新的空文件(这个参数告诉 Python,我们想写入文件对象,而不是从中读取):

>>> test_file = open('c:\\Users\\rachel\\myfile.txt', 'w')

现在我们可以使用 write 函数向这个新文件添加信息:

>>> test_file = open('c:\\Users\\rachel\\myfile.txt', 'w')
>>> test_file.write('What is green and loud? A froghorn!')
20

最后,我们需要告诉 Python 当我们写入完文件时,使用 close 函数:

>>> test_file = open('c:\\Users\\rachel\\myfile.txt', 'w')
>>> test_file.write('What is green and loud? A froghorn!')
>>> test_file.close()

现在,如果你用文本编辑器打开文件,你应该会看到它包含了文本:“什么东西既绿色又响亮?一只青蛙号角!”或者,你也可以用 Python 再次读取它:

Image

>>> test_file = open('c:\\Users\\rachel\\myfile.txt')
>>> print(test_file.read())
What is green and loud? A froghorn!

第二十章:C

故障排除

Image

在本附录中,你将找到一些解决 Python 较少见问题的方法。如果你使用的是某些操作系统的较旧版本,可能会遇到这些问题。

在 Ubuntu 上导入 Turtle 时的 “TK” 错误

如果你正在使用较旧版本的 Ubuntu Linux,并且在导入 turtle 时遇到错误,可能需要安装一个叫做 tkinter 的软件。为此,请打开 Ubuntu 软件中心并在搜索框中输入 python-tk。窗口中应显示“Tkinter—使用 Python 编写 Tk 应用程序”。点击 安装 来安装此软件包。如果你使用的是较新版的 Ubuntu,通常不需要这样做——如果可能,建议让电脑的拥有者为你更新系统。

使用 Turtle 时的属性错误

一些新程序员在尝试使用 turtle 时,会遇到奇怪的 属性 错误:

>>> import turtle
>>> t = turtle.Turtle()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'turtle' has no attribute 'Turtle'

这个错误最常见的原因是你在主文件夹中创建了一个名为 turtle.py 的文件。在这种情况下,当你输入 import turtle 时,实际上是导入了你自己创建的文件,而不是 Python 的 turtle 模块。如果你删除或重命名该文件,正确的模块应该会正常导入。

运行 Turtle 时的问题

如果你在使用 turtle 模块时遇到问题,并且 turtle 窗口似乎没有正常工作,可以尝试使用 Python 控制台而不是 Python Shell,如下所示:

  • 在 Windows 中,输入 Python 在搜索框中并点击应用列表中的 Python 3.1x。你也可以使用 Windows 命令提示符(点击 Windows 图标并在搜索框中输入 cmd)。打开命令提示符后,你需要输入 python.exe 程序的路径。如果你安装了 Python 3.10,路径可能类似于:AppData\Local\Programs\Python \Python310\python.exe。然而,这取决于你安装的 Python 版本,因此此方法通常应该是最后的选择(你可以在 图 C-1 中查看运行结果)。Image

    图 C-1:从 Windows 命令提示符运行 Python 控制台

  • 在 macOS 中,点击屏幕右上角的聚光灯搜索图标(应该像一个放大镜),然后在输入框中输入 终端。终端打开后,输入 python3。

  • 在 Ubuntu Linux 中,从 显示应用程序 菜单打开终端并输入 python3.10(请注意,版本号可能不同)。

  • 在 Raspberry Pi 中,点击屏幕顶部菜单栏的终端图标,或者在 附件 菜单中点击 终端,然后输入 /usr/local/opt/python-3.10.0(此方法仅在你按照 第一章 的安装说明进行安装时有效;请注意,版本号可能不同)。

Python 控制台类似于 Python Shell(IDLE),但它没有语法高亮(彩色文本)、便捷的保存选项和其他有益的功能。然而,如果你在 Python Shell 中运行 turtle 时遇到问题,使用 Python 控制台可能会有所帮助。

类不接受任何参数

一些读者常见的错误是 TypeError,通常在第十一章首次出现。你可能会看到类似以下的错误:

b = Ball(canvas, 'red')
Traceback (most recent call last):
  File "/usr/lib/python3.10/idlelib/run.py", line 573, in runcode
    exec(code, self.locals)
  File "<pyshell#4>", line 1, in <module>
TypeError: Ball() takes no arguments 

造成这种情况的原因通常是缺少下划线。Ball 类首先是这样定义的:

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

然而,如果你在输入 __init__ 函数时错误地使用了单个下划线(init),Python 将不再将其识别为初始化函数。这就是为什么调用 Ball(...) 并传入任何参数会导致错误——Python 认为没有初始化函数可调用(实际上,它会为你创建一个没有参数的默认初始化函数)。

posted @ 2025-11-27 09:17  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报